Merge pull request #35 from hecrj/feature/scrollables

Scrollable widget
This commit is contained in:
Héctor Ramón 2019-11-03 05:19:12 +01:00 committed by GitHub
commit 0ea911ae36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 907 additions and 113 deletions

View file

@ -14,6 +14,7 @@ mod radio;
mod row;
pub mod button;
pub mod scrollable;
pub mod slider;
pub mod text;
@ -26,6 +27,9 @@ pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
pub use scrollable::Scrollable;
pub use checkbox::Checkbox;
pub use column::Column;
pub use image::Image;

View file

@ -0,0 +1,151 @@
use crate::{Align, Column, Length, Point, Rectangle};
#[derive(Debug)]
pub struct Scrollable<'a, Element> {
pub state: &'a mut State,
pub height: Length,
pub max_height: Length,
pub align_self: Option<Align>,
pub content: Column<Element>,
}
impl<'a, Element> Scrollable<'a, Element> {
pub fn new(state: &'a mut State) -> Self {
Scrollable {
state,
height: Length::Shrink,
max_height: Length::Shrink,
align_self: None,
content: Column::new(),
}
}
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, units: u16) -> Self {
self.content = self.content.spacing(units);
self
}
/// Sets the padding of the [`Scrollable`].
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
/// Sets the width of the [`Scrollable`].
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
/// Sets the height of the [`Scrollable`].
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Scrollable`].
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn max_width(mut self, max_width: Length) -> Self {
self.content = self.content.max_width(max_width);
self
}
/// Sets the maximum height of the [`Scrollable`] in pixels.
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn max_height(mut self, max_height: Length) -> Self {
self.max_height = max_height;
self
}
/// Sets the alignment of the [`Scrollable`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
/// Sets the horizontal alignment of the contents of the [`Scrollable`] .
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn align_items(mut self, align_items: Align) -> Self {
self.content = self.content.align_items(align_items);
self
}
/// Adds an element to the [`Scrollable`].
///
/// [`Scrollable`]: struct.Scrollable.html
pub fn push<E>(mut self, child: E) -> Scrollable<'a, Element>
where
E: Into<Element>,
{
self.content = self.content.push(child);
self
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
pub scrollbar_grabbed_at: Option<Point>,
offset: u32,
}
impl State {
pub fn new() -> Self {
State::default()
}
pub fn scroll(
&mut self,
delta_y: f32,
bounds: Rectangle,
content_bounds: Rectangle,
) {
if bounds.height >= content_bounds.height {
return;
}
self.offset = (self.offset as i32 - delta_y.round() as i32)
.max(0)
.min((content_bounds.height - bounds.height) as i32)
as u32;
}
pub fn scroll_to(
&mut self,
percentage: f32,
bounds: Rectangle,
content_bounds: Rectangle,
) {
self.offset = ((content_bounds.height - bounds.height) * percentage)
.max(0.0) as u32;
}
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
let hidden_content =
(content_bounds.height - bounds.height).round() as u32;
self.offset.min(hidden_content).max(0)
}
pub fn is_scrollbar_grabbed(&self) -> bool {
self.scrollbar_grabbed_at.is_some()
}
}

75
examples/scroll.rs Normal file
View file

@ -0,0 +1,75 @@
use iced::{
button, scrollable, Align, Application, Button, Column, Element, Image,
Justify, Length, Scrollable, Text,
};
pub fn main() {
env_logger::init();
Example::default().run()
}
#[derive(Default)]
struct Example {
item_count: u16,
scroll: scrollable::State,
add_button: button::State,
}
#[derive(Debug, Clone, Copy)]
pub enum Message {
AddItem,
}
impl Application for Example {
type Message = Message;
fn update(&mut self, message: Message) {
match message {
Message::AddItem => {
self.item_count += 1;
}
}
}
fn view(&mut self) -> Element<Message> {
let content = (0..self.item_count)
.fold(
Scrollable::new(&mut self.scroll)
.spacing(20)
.padding(20)
.align_items(Align::Center),
|scrollable, i| {
if i % 2 == 0 {
scrollable.push(lorem_ipsum().width(Length::Units(600)))
} else {
scrollable.push(
Image::new(format!(
"{}/examples/resources/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
.width(Length::Units(400)),
)
}
},
)
.push(
Button::new(&mut self.add_button, Text::new("Add item"))
.on_press(Message::AddItem)
.padding(20)
.border_radius(5),
);
Column::new()
.height(Length::Fill)
.justify_content(Justify::Center)
.padding(20)
.push(content)
.into()
}
}
fn lorem_ipsum() -> Text {
Text::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi in dui vel massa blandit interdum. Quisque placerat, odio ut vulputate sagittis, augue est facilisis ex, eget euismod felis magna in sapien. Nullam luctus consequat massa, ac interdum mauris blandit pellentesque. Nullam in est urna. Aliquam tristique lectus ac luctus feugiat. Aenean libero diam, euismod facilisis consequat quis, pellentesque luctus erat. Praesent vel tincidunt elit.")
}

View file

@ -1,7 +1,7 @@
use iced::{
button, slider, text::HorizontalAlignment, Align, Application, Background,
Button, Checkbox, Color, Column, Element, Image, Justify, Length, Radio,
Row, Slider, Text,
button, scrollable, slider, text::HorizontalAlignment, Align, Application,
Background, Button, Checkbox, Color, Column, Element, Image, Justify,
Length, Radio, Row, Scrollable, Slider, Text,
};
pub fn main() {
@ -14,6 +14,7 @@ pub fn main() {
pub struct Tour {
steps: Steps,
scroll: scrollable::State,
back_button: button::State,
next_button: button::State,
debug: bool,
@ -23,6 +24,7 @@ impl Tour {
pub fn new() -> Tour {
Tour {
steps: Steps::new(),
scroll: scrollable::State::new(),
back_button: button::State::new(),
next_button: button::State::new(),
debug: false,
@ -88,11 +90,13 @@ impl Application for Tour {
};
Column::new()
.width(Length::Fill)
.height(Length::Fill)
.align_items(Align::Center)
.justify_content(Justify::Center)
.push(element)
.push(
Scrollable::new(&mut self.scroll)
.align_items(Align::Center)
.push(element),
)
.into()
}
}
@ -134,6 +138,7 @@ impl Steps {
width: 300,
slider: slider::State::new(),
},
Step::Scrollable,
Step::Debugger,
Step::End,
],
@ -195,6 +200,7 @@ enum Step {
width: u16,
slider: slider::State,
},
Scrollable,
Debugger,
End,
}
@ -265,6 +271,7 @@ impl<'a> Step {
Step::Text { .. } => true,
Step::Image { .. } => true,
Step::RowsAndColumns { .. } => true,
Step::Scrollable => true,
Step::Debugger => true,
Step::End => false,
}
@ -289,6 +296,7 @@ impl<'a> Step {
} => {
Self::rows_and_columns(*layout, spacing_slider, *spacing).into()
}
Step::Scrollable => Self::scrollable().into(),
Step::Debugger => Self::debugger(debug).into(),
Step::End => Self::end().into(),
}
@ -502,20 +510,7 @@ impl<'a> Step {
) -> Column<'a, StepMessage> {
Self::container("Image")
.push(Text::new("An image that tries to keep its aspect ratio."))
.push(
// This should go away once we unify resource loading on native
// platforms
if cfg!(target_arch = "wasm32") {
Image::new("resources/ferris.png")
} else {
Image::new(format!(
"{}/examples/resources/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
}
.width(Length::Units(width))
.align_self(Align::Center),
)
.push(ferris(width))
.push(Slider::new(
slider,
100.0..=500.0,
@ -528,6 +523,33 @@ impl<'a> Step {
)
}
fn scrollable() -> Column<'a, StepMessage> {
Self::container("Scrollable")
.push(Text::new(
"Iced supports scrollable content. Try it out! Find the \
button further below.",
))
.push(
Text::new(
"Tip: You can use the scrollbar to scroll down faster!",
)
.size(16),
)
.push(Column::new().height(Length::Units(4096)))
.push(
Text::new("You are halfway there!")
.size(30)
.horizontal_alignment(HorizontalAlignment::Center),
)
.push(Column::new().height(Length::Units(4096)))
.push(ferris(300))
.push(
Text::new("You made it!")
.size(50)
.horizontal_alignment(HorizontalAlignment::Center),
)
}
fn debugger(debug: bool) -> Column<'a, StepMessage> {
Self::container("Debugger")
.push(Text::new(
@ -555,6 +577,21 @@ impl<'a> Step {
}
}
fn ferris(width: u16) -> Image {
// This should go away once we unify resource loading on native
// platforms
if cfg!(target_arch = "wasm32") {
Image::new("resources/ferris.png")
} else {
Image::new(format!(
"{}/examples/resources/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
}
.width(Length::Units(width))
.align_self(Align::Center)
}
fn button<'a, Message>(
state: &'a mut button::State,
label: &str,

View file

@ -11,4 +11,4 @@ repository = "https://github.com/hecrj/iced"
iced_core = { version = "0.1.0-alpha", path = "../core" }
stretch = "0.2"
twox-hash = "1.5"
raw-window-handle = "0.1"
raw-window-handle = "0.3"

View file

@ -299,6 +299,7 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<B>,
renderer: &Renderer,
) {
let mut original_messages = Vec::new();
@ -307,6 +308,7 @@ where
layout,
cursor_position,
&mut original_messages,
renderer,
);
original_messages
@ -369,10 +371,15 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
renderer: &Renderer,
) {
self.element
.widget
.on_event(event, layout, cursor_position, messages)
self.element.widget.on_event(
event,
layout,
cursor_position,
messages,
renderer,
)
}
fn draw(

View file

@ -3,4 +3,4 @@ mod button;
mod event;
pub use button::Button;
pub use event::Event;
pub use event::{Event, ScrollDelta};

View file

@ -34,11 +34,22 @@ pub enum Event {
},
/// The mouse wheel was scrolled.
WheelScrolled {
WheelScrolled { delta: ScrollDelta },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScrollDelta {
Lines {
/// The number of horizontal lines scrolled
delta_x: f32,
x: f32,
/// The number of vertical lines scrolled
delta_y: f32,
y: f32,
},
Pixels {
/// The number of horizontal pixels scrolled
x: f32,
/// The number of vertical pixels scrolled
y: f32,
},
}

View file

@ -186,7 +186,7 @@ where
/// );
///
/// // Update the user interface
/// let messages = user_interface.update(events.drain(..));
/// let messages = user_interface.update(&renderer, events.drain(..));
///
/// cache = user_interface.into_cache();
///
@ -198,6 +198,7 @@ where
/// ```
pub fn update(
&mut self,
renderer: &Renderer,
events: impl Iterator<Item = Event>,
) -> Vec<Message> {
let mut messages = Vec::new();
@ -212,6 +213,7 @@ where
Layout::new(&self.layout),
self.cursor_position,
&mut messages,
renderer,
);
}
@ -281,7 +283,7 @@ where
/// &mut renderer,
/// );
///
/// let messages = user_interface.update(events.drain(..));
/// let messages = user_interface.update(&renderer, events.drain(..));
///
/// // Draw the user interface
/// let mouse_cursor = user_interface.draw(&mut renderer);
@ -347,7 +349,7 @@ impl Cache {
.0
.compute_layout(geometry::Size::undefined())
.unwrap(),
cursor_position: Point::new(0.0, 0.0),
cursor_position: Point::new(-1.0, -1.0),
}
}
}

View file

@ -26,6 +26,7 @@ pub mod column;
pub mod image;
pub mod radio;
pub mod row;
pub mod scrollable;
pub mod slider;
pub mod text;
@ -42,6 +43,8 @@ pub use radio::Radio;
#[doc(no_inline)]
pub use row::Row;
#[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
@ -114,6 +117,7 @@ where
_layout: Layout<'_>,
_cursor_position: Point,
_messages: &mut Vec<Message>,
_renderer: &Renderer,
) {
}
}

View file

@ -31,6 +31,7 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
_renderer: &Renderer,
) {
match event {
Event::Mouse(mouse::Event::Input {

View file

@ -20,6 +20,7 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
_renderer: &Renderer,
) {
match event {
Event::Mouse(mouse::Event::Input {

View file

@ -55,12 +55,17 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
renderer: &Renderer,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
child.widget.on_event(
event,
layout,
cursor_position,
messages,
renderer,
)
},
);
}

View file

@ -21,6 +21,7 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
_renderer: &Renderer,
) {
match event {
Event::Mouse(mouse::Event::Input {

View file

@ -55,12 +55,17 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
renderer: &Renderer,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
child.widget.on_event(
event,
layout,
cursor_position,
messages,
renderer,
)
},
);
}

View file

@ -0,0 +1,204 @@
use crate::{
column,
input::{mouse, ButtonState},
Element, Event, Hasher, Layout, Node, Point, Rectangle, Style, Widget,
};
pub use iced_core::scrollable::State;
use std::hash::Hash;
/// A scrollable [`Column`].
///
/// [`Column`]: ../column/struct.Column.html
pub type Scrollable<'a, Message, Renderer> =
iced_core::Scrollable<'a, Element<'a, Message, Renderer>>;
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
Renderer: self::Renderer + column::Renderer,
{
fn node(&self, renderer: &Renderer) -> Node {
let mut content = self.content.node(renderer);
{
let mut style = content.0.style();
style.flex_shrink = 0.0;
content.0.set_style(style);
}
let mut style = Style::default()
.width(self.content.width)
.max_width(self.content.max_width)
.height(self.height)
.max_height(self.max_height)
.align_self(self.align_self);
style.0.flex_direction = stretch::style::FlexDirection::Column;
Node::with_children(style, vec![content])
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
renderer: &Renderer,
) {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
let is_mouse_over_scrollbar = renderer.is_mouse_over_scrollbar(
bounds,
content_bounds,
cursor_position,
);
// TODO: Event capture. Nested scrollables should capture scroll events.
if is_mouse_over {
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
match delta {
mouse::ScrollDelta::Lines { y, .. } => {
// TODO: Configurable speed (?)
self.state.scroll(y * 15.0, bounds, content_bounds);
}
mouse::ScrollDelta::Pixels { y, .. } => {
self.state.scroll(y, bounds, content_bounds);
}
}
}
_ => {}
}
}
if self.state.is_scrollbar_grabbed() || is_mouse_over_scrollbar {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state,
}) => match state {
ButtonState::Pressed => {
self.state.scroll_to(
cursor_position.y / (bounds.y + bounds.height),
bounds,
content_bounds,
);
self.state.scrollbar_grabbed_at = Some(cursor_position);
}
ButtonState::Released => {
self.state.scrollbar_grabbed_at = None;
}
},
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(scrollbar_grabbed_at) =
self.state.scrollbar_grabbed_at
{
let ratio = content_bounds.height / bounds.height;
let delta = scrollbar_grabbed_at.y - cursor_position.y;
self.state.scroll(
delta * ratio,
bounds,
content_bounds,
);
self.state.scrollbar_grabbed_at = Some(cursor_position);
}
}
_ => {}
}
}
let cursor_position = if is_mouse_over
&& !(is_mouse_over_scrollbar
|| self.state.scrollbar_grabbed_at.is_some())
{
Point::new(
cursor_position.x,
cursor_position.y
+ self.state.offset(bounds, content_bounds) as f32,
)
} else {
// TODO: Make `cursor_position` an `Option<Point>` so we can encode
// cursor availability.
// This will probably happen naturally once we add multi-window
// support.
Point::new(cursor_position.x, -1.0)
};
self.content.on_event(
event,
content,
cursor_position,
messages,
renderer,
)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> Renderer::Output {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
self::Renderer::draw(
renderer,
&self,
bounds,
content_layout,
cursor_position,
)
}
fn hash_layout(&self, state: &mut Hasher) {
std::any::TypeId::of::<Scrollable<'static, (), ()>>().hash(state);
self.height.hash(state);
self.max_height.hash(state);
self.align_self.hash(state);
self.content.hash_layout(state)
}
}
pub trait Renderer: crate::Renderer + Sized {
fn is_mouse_over_scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
cursor_position: Point,
) -> bool;
fn draw<Message>(
&mut self,
scrollable: &Scrollable<'_, Message, Self>,
bounds: Rectangle,
content_layout: Layout<'_>,
cursor_position: Point,
) -> Self::Output;
}
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer + column::Renderer,
Message: 'static,
{
fn from(
scrollable: Scrollable<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(scrollable)
}
}

View file

@ -25,6 +25,7 @@ where
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
_renderer: &Renderer,
) {
let mut change = || {
let bounds = layout.bounds();

View file

@ -1,8 +1,8 @@
pub use iced_wgpu::{Primitive, Renderer};
pub use iced_winit::{
button, slider, text, winit, Align, Background, Checkbox, Color, Image,
Justify, Length, Radio, Slider, Text,
button, scrollable, slider, text, winit, Align, Background, Checkbox,
Color, Image, Justify, Length, Radio, Scrollable, Slider, Text,
};
pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>;

View file

@ -9,8 +9,9 @@ repository = "https://github.com/hecrj/iced"
[dependencies]
iced_native = { version = "0.1.0-alpha", path = "../native" }
wgpu = { version = "0.3", git = "https://github.com/gfx-rs/wgpu-rs", rev = "cb25914b95b58fee0dc139b400867e7a731d98f4" }
wgpu_glyph = { version = "0.4", git = "https://github.com/hecrj/wgpu_glyph", rev = "48daa98f5f785963838b4345e86ac40eac095ba9" }
raw-window-handle = "0.1"
wgpu = { version = "0.3", git = "https://github.com/gfx-rs/wgpu-rs", rev = "ed2c67f762970d0099c1e6c6e078fb645afbf964" }
wgpu_glyph = { version = "0.4", git = "https://github.com/hecrj/wgpu_glyph", rev = "954ac865ca1b7f6b97bf403f8c6174a7120e667c" }
raw-window-handle = "0.3"
image = "0.22"
glam = "0.8"
log = "0.4"

View file

@ -1,4 +1,5 @@
use crate::Transformation;
use iced_native::Rectangle;
use std::cell::RefCell;
use std::collections::HashMap;
@ -218,13 +219,12 @@ impl Pipeline {
encoder: &mut wgpu::CommandEncoder,
instances: &[Image],
transformation: Transformation,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
) {
let matrix: [f32; 16] = transformation.into();
let transform_buffer = device
.create_buffer_mapped(16, wgpu::BufferUsage::COPY_SRC)
.fill_from_slice(&matrix[..]);
.fill_from_slice(transformation.as_ref());
encoder.copy_buffer_to_buffer(
&transform_buffer,
@ -291,6 +291,12 @@ impl Pipeline {
0,
&[(&self.vertices, 0), (&self.instances, 0)],
);
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
render_pass.draw_indexed(
0..QUAD_INDICES.len() as u32,

View file

@ -23,4 +23,9 @@ pub enum Primitive {
path: String,
bounds: Rectangle,
},
Clip {
bounds: Rectangle,
offset: u32,
content: Box<Primitive>,
},
}

View file

@ -1,4 +1,5 @@
use crate::Transformation;
use iced_native::Rectangle;
use std::mem;
@ -22,14 +23,12 @@ impl Pipeline {
}],
});
let matrix: [f32; 16] = Transformation::identity().into();
let transform = device
.create_buffer_mapped(
16,
wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
)
.fill_from_slice(&matrix[..]);
.fill_from_slice(Transformation::identity().as_ref());
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &constant_layout,
@ -165,13 +164,12 @@ impl Pipeline {
encoder: &mut wgpu::CommandEncoder,
instances: &[Quad],
transformation: Transformation,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
) {
let matrix: [f32; 16] = transformation.into();
let transform_buffer = device
.create_buffer_mapped(16, wgpu::BufferUsage::COPY_SRC)
.fill_from_slice(&matrix[..]);
.fill_from_slice(transformation.as_ref());
encoder.copy_buffer_to_buffer(
&transform_buffer,
@ -227,6 +225,12 @@ impl Pipeline {
0,
&[(&self.vertices, 0), (&self.instances, 0)],
);
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
render_pass.draw_indexed(
0..QUAD_INDICES.len() as u32,

View file

@ -1,7 +1,7 @@
use crate::{quad, Image, Primitive, Quad, Transformation};
use iced_native::{
renderer::Debugger, renderer::Windowed, Background, Color, Layout,
MouseCursor, Point, Widget,
MouseCursor, Point, Rectangle, Widget,
};
use raw_window_handle::HasRawWindowHandle;
@ -20,19 +20,17 @@ mod column;
mod image;
mod radio;
mod row;
mod scrollable;
mod slider;
mod text;
pub struct Renderer {
surface: Surface,
adapter: Adapter,
device: Device,
queue: Queue,
quad_pipeline: quad::Pipeline,
image_pipeline: crate::image::Pipeline,
quads: Vec<Quad>,
images: Vec<Image>,
glyph_brush: Rc<RefCell<GlyphBrush<'static, ()>>>,
}
@ -43,6 +41,26 @@ pub struct Target {
swap_chain: SwapChain,
}
pub struct Layer<'a> {
bounds: Rectangle<u32>,
y_offset: u32,
quads: Vec<Quad>,
images: Vec<Image>,
text: Vec<wgpu_glyph::Section<'a>>,
}
impl<'a> Layer<'a> {
pub fn new(bounds: Rectangle<u32>, y_offset: u32) -> Self {
Self {
bounds,
y_offset,
quads: Vec::new(),
images: Vec::new(),
text: Vec::new(),
}
}
}
impl Renderer {
fn new<W: HasRawWindowHandle>(window: &W) -> Self {
let adapter = Adapter::request(&RequestAdapterOptions {
@ -55,7 +73,7 @@ impl Renderer {
extensions: Extensions {
anisotropic_filtering: false,
},
limits: Limits { max_bind_groups: 1 },
limits: Limits { max_bind_groups: 2 },
});
let surface = Surface::create(window);
@ -73,14 +91,11 @@ impl Renderer {
Self {
surface,
adapter,
device,
queue,
quad_pipeline,
image_pipeline,
quads: Vec::new(),
images: Vec::new(),
glyph_brush: Rc::new(RefCell::new(glyph_brush)),
}
}
@ -132,51 +147,46 @@ impl Renderer {
depth_stencil_attachment: None,
});
self.draw_primitive(primitive);
self.quad_pipeline.draw(
&mut self.device,
&mut encoder,
&self.quads,
target.transformation,
&frame.view,
let mut layers = Vec::new();
let mut current = Layer::new(
Rectangle {
x: 0,
y: 0,
width: u32::from(target.width),
height: u32::from(target.height),
},
0,
);
self.quads.clear();
self.draw_primitive(primitive, &mut current, &mut layers);
layers.push(current);
self.image_pipeline.draw(
&mut self.device,
&mut encoder,
&self.images,
target.transformation,
&frame.view,
);
self.images.clear();
self.glyph_brush
.borrow_mut()
.draw_queued(
&mut self.device,
for layer in layers {
self.flush(
target.transformation,
&layer,
&mut encoder,
&frame.view,
u32::from(target.width),
u32::from(target.height),
)
.expect("Draw text");
);
}
self.queue.submit(&[encoder.finish()]);
*mouse_cursor
}
fn draw_primitive(&mut self, primitive: &Primitive) {
fn draw_primitive<'a>(
&mut self,
primitive: &'a Primitive,
layer: &mut Layer<'a>,
layers: &mut Vec<Layer<'a>>,
) {
match primitive {
Primitive::None => {}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
self.draw_primitive(primitive)
self.draw_primitive(primitive, layer, layers)
}
}
Primitive::Text {
@ -207,7 +217,7 @@ impl Renderer {
}
};
self.glyph_brush.borrow_mut().queue(Section {
layer.text.push(Section {
text: &content,
screen_position: (x, y),
bounds: (bounds.width, bounds.height),
@ -244,8 +254,8 @@ impl Renderer {
background,
border_radius,
} => {
self.quads.push(Quad {
position: [bounds.x, bounds.y],
layer.quads.push(Quad {
position: [bounds.x, bounds.y - layer.y_offset as f32],
scale: [bounds.width, bounds.height],
color: match background {
Background::Color(color) => color.into_linear(),
@ -254,12 +264,88 @@ impl Renderer {
});
}
Primitive::Image { path, bounds } => {
self.images.push(Image {
layer.images.push(Image {
path: path.clone(),
position: [bounds.x, bounds.y],
scale: [bounds.width, bounds.height],
});
}
Primitive::Clip {
bounds,
offset,
content,
} => {
let mut new_layer = Layer::new(
Rectangle {
x: bounds.x as u32,
y: bounds.y as u32 - layer.y_offset,
width: bounds.width as u32,
height: bounds.height as u32,
},
layer.y_offset + offset,
);
// TODO: Primitive culling
self.draw_primitive(content, &mut new_layer, layers);
layers.push(new_layer);
}
}
}
fn flush(
&mut self,
transformation: Transformation,
layer: &Layer,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
) {
let translated = transformation
* Transformation::translate(0.0, -(layer.y_offset as f32));
if layer.quads.len() > 0 {
self.quad_pipeline.draw(
&mut self.device,
encoder,
&layer.quads,
transformation,
layer.bounds,
target,
);
}
if layer.images.len() > 0 {
self.image_pipeline.draw(
&mut self.device,
encoder,
&layer.images,
translated,
layer.bounds,
target,
);
}
if layer.text.len() > 0 {
let mut glyph_brush = self.glyph_brush.borrow_mut();
for text in layer.text.iter() {
glyph_brush.queue(text);
}
glyph_brush
.draw_queued_with_transform_and_scissoring(
&mut self.device,
encoder,
target,
translated.into(),
wgpu_glyph::Region {
x: layer.bounds.x,
y: layer.bounds.y,
width: layer.bounds.width,
height: layer.bounds.height,
},
)
.expect("Draw text");
}
}
}

View file

@ -0,0 +1,133 @@
use crate::{Primitive, Renderer};
use iced_native::{
scrollable, Background, Color, Layout, MouseCursor, Point, Rectangle,
Scrollable, Widget,
};
const SCROLLBAR_WIDTH: u16 = 10;
const SCROLLBAR_MARGIN: u16 = 2;
fn scrollbar_bounds(bounds: Rectangle) -> Rectangle {
Rectangle {
x: bounds.x + bounds.width
- f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN),
y: bounds.y,
width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN),
height: bounds.height,
}
}
impl scrollable::Renderer for Renderer {
fn is_mouse_over_scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
cursor_position: Point,
) -> bool {
content_bounds.height > bounds.height
&& scrollbar_bounds(bounds).contains(cursor_position)
}
fn draw<Message>(
&mut self,
scrollable: &Scrollable<'_, Message, Self>,
bounds: Rectangle,
content: Layout<'_>,
cursor_position: Point,
) -> Self::Output {
let is_mouse_over = bounds.contains(cursor_position);
let content_bounds = content.bounds();
let offset = scrollable.state.offset(bounds, content_bounds);
let is_content_overflowing = content_bounds.height > bounds.height;
let scrollbar_bounds = scrollbar_bounds(bounds);
let is_mouse_over_scrollbar = self.is_mouse_over_scrollbar(
bounds,
content_bounds,
cursor_position,
);
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
Point::new(cursor_position.x, cursor_position.y + offset as f32)
} else {
Point::new(cursor_position.x, -1.0)
};
let (content, mouse_cursor) =
scrollable.content.draw(self, content, cursor_position);
let primitive = Primitive::Clip {
bounds,
offset,
content: Box::new(content),
};
(
if is_content_overflowing
&& (is_mouse_over || scrollable.state.is_scrollbar_grabbed())
{
let ratio = bounds.height / content_bounds.height;
let scrollbar_height = bounds.height * ratio;
let y_offset = offset as f32 * ratio;
let scrollbar = Primitive::Quad {
bounds: Rectangle {
x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN),
y: scrollbar_bounds.y + y_offset,
width: scrollbar_bounds.width
- f32::from(2 * SCROLLBAR_MARGIN),
height: scrollbar_height,
},
background: Background::Color(Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.7,
}),
border_radius: 5,
};
if is_mouse_over_scrollbar
|| scrollable.state.is_scrollbar_grabbed()
{
let scrollbar_background = Primitive::Quad {
bounds: Rectangle {
x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN),
width: scrollbar_bounds.width
- f32::from(2 * SCROLLBAR_MARGIN),
..scrollbar_bounds
},
background: Background::Color(Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.3,
}),
border_radius: 5,
};
Primitive::Group {
primitives: vec![
primitive,
scrollbar_background,
scrollbar,
],
}
} else {
Primitive::Group {
primitives: vec![primitive, scrollbar],
}
}
} else {
primitive
},
if is_mouse_over_scrollbar
|| scrollable.state.is_scrollbar_grabbed()
{
MouseCursor::Idle
} else {
mouse_cursor
},
)
}
}

View file

@ -1,30 +1,49 @@
#[derive(Debug, Clone, Copy)]
pub struct Transformation([f32; 16]);
use glam::{Mat4, Vec3, Vec4};
use std::ops::Mul;
/// A 2D transformation matrix.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transformation(Mat4);
impl Transformation {
#[rustfmt::skip]
pub fn identity() -> Self {
Transformation([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])
/// Get the identity transformation.
pub fn identity() -> Transformation {
Transformation(Mat4::identity())
}
/// Creates an orthographic projection.
#[rustfmt::skip]
pub fn orthographic(width: u16, height: u16) -> Self {
Transformation([
2.0 / width as f32, 0.0, 0.0, 0.0,
0.0, 2.0 / height as f32, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-1.0, -1.0, 0.0, 1.0,
])
pub fn orthographic(width: u16, height: u16) -> Transformation {
Transformation(Mat4::from_cols(
Vec4::new(2.0 / f32::from(width), 0.0, 0.0, 0.0),
Vec4::new(0.0, 2.0 / f32::from(height), 0.0, 0.0),
Vec4::new(0.0, 0.0, -1.0, 0.0),
Vec4::new(-1.0, -1.0, 0.0, 1.0)
))
}
/// Creates a translate transformation.
pub fn translate(x: f32, y: f32) -> Transformation {
Transformation(Mat4::from_translation(Vec3::new(x, y, 0.0)))
}
}
impl Mul for Transformation {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Transformation(self.0 * rhs.0)
}
}
impl AsRef<[f32; 16]> for Transformation {
fn as_ref(&self) -> &[f32; 16] {
self.0.as_ref()
}
}
impl From<Transformation> for [f32; 16] {
fn from(transformation: Transformation) -> [f32; 16] {
transformation.0
fn from(t: Transformation) -> [f32; 16] {
t.as_ref().clone()
}
}

View file

@ -9,5 +9,5 @@ repository = "https://github.com/hecrj/iced"
[dependencies]
iced_native = { version = "0.1.0-alpha", path = "../native" }
winit = { version = "0.20.0-alpha3", git = "https://github.com/hecrj/winit", branch = "redraw-requested-2.0" }
winit = { version = "0.20.0-alpha3", git = "https://github.com/rust-windowing/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"}
log = "0.4"

View file

@ -68,7 +68,8 @@ pub trait Application {
&renderer,
);
let messages = user_interface.update(events.drain(..));
let messages =
user_interface.update(&renderer, events.drain(..));
if messages.is_empty() {
primitive = user_interface.draw(&mut renderer);
@ -122,6 +123,7 @@ pub trait Application {
..
} => match window_event {
WindowEvent::CursorMoved { position, .. } => {
// TODO: Remove when renderer supports HiDPI
let physical_position =
position.to_physical(window.hidpi_factor());
@ -136,6 +138,35 @@ pub trait Application {
state: conversion::button_state(state),
}));
}
WindowEvent::MouseWheel { delta, .. } => match delta {
winit::event::MouseScrollDelta::LineDelta(
delta_x,
delta_y,
) => {
events.push(Event::Mouse(
mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines {
x: delta_x,
y: delta_y,
},
},
));
}
winit::event::MouseScrollDelta::PixelDelta(position) => {
// TODO: Remove when renderer supports HiDPI
let physical_position =
position.to_physical(window.hidpi_factor());
events.push(Event::Mouse(
mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels {
x: physical_position.x as f32,
y: physical_position.y as f32,
},
},
));
}
},
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}