diff options
Diffstat (limited to '')
-rw-r--r-- | core/src/widget.rs | 4 | ||||
-rw-r--r-- | core/src/widget/scrollable.rs | 151 | ||||
-rw-r--r-- | examples/scroll.rs | 75 | ||||
-rw-r--r-- | examples/tour.rs | 77 | ||||
-rw-r--r-- | native/Cargo.toml | 2 | ||||
-rw-r--r-- | native/src/element.rs | 13 | ||||
-rw-r--r-- | native/src/input/mouse.rs | 2 | ||||
-rw-r--r-- | native/src/input/mouse/event.rs | 17 | ||||
-rw-r--r-- | native/src/user_interface.rs | 8 | ||||
-rw-r--r-- | native/src/widget.rs | 4 | ||||
-rw-r--r-- | native/src/widget/button.rs | 1 | ||||
-rw-r--r-- | native/src/widget/checkbox.rs | 1 | ||||
-rw-r--r-- | native/src/widget/column.rs | 11 | ||||
-rw-r--r-- | native/src/widget/radio.rs | 1 | ||||
-rw-r--r-- | native/src/widget/row.rs | 11 | ||||
-rw-r--r-- | native/src/widget/scrollable.rs | 204 | ||||
-rw-r--r-- | native/src/widget/slider.rs | 1 | ||||
-rw-r--r-- | src/winit.rs | 4 | ||||
-rw-r--r-- | wgpu/Cargo.toml | 7 | ||||
-rw-r--r-- | wgpu/src/image.rs | 12 | ||||
-rw-r--r-- | wgpu/src/primitive.rs | 5 | ||||
-rw-r--r-- | wgpu/src/quad.rs | 16 | ||||
-rw-r--r-- | wgpu/src/renderer.rs | 168 | ||||
-rw-r--r-- | wgpu/src/renderer/scrollable.rs | 133 | ||||
-rw-r--r-- | wgpu/src/transformation.rs | 57 | ||||
-rw-r--r-- | winit/Cargo.toml | 2 | ||||
-rw-r--r-- | winit/src/application.rs | 33 |
27 files changed, 907 insertions, 113 deletions
diff --git a/core/src/widget.rs b/core/src/widget.rs index f9d4bf2a..3ee8e347 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -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; diff --git a/core/src/widget/scrollable.rs b/core/src/widget/scrollable.rs new file mode 100644 index 00000000..31a5abed --- /dev/null +++ b/core/src/widget/scrollable.rs @@ -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() + } +} diff --git a/examples/scroll.rs b/examples/scroll.rs new file mode 100644 index 00000000..608923fe --- /dev/null +++ b/examples/scroll.rs @@ -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.") +} diff --git a/examples/tour.rs b/examples/tour.rs index 59a8c525..f63b4cfe 100644 --- a/examples/tour.rs +++ b/examples/tour.rs @@ -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, diff --git a/native/Cargo.toml b/native/Cargo.toml index 8cabe94c..bb6139d6 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -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" diff --git a/native/src/element.rs b/native/src/element.rs index bbedd942..c638c713 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -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( diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index d37f5b96..69dc6b4c 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -3,4 +3,4 @@ mod button; mod event; pub use button::Button; -pub use event::Event; +pub use event::{Event, ScrollDelta}; diff --git a/native/src/input/mouse/event.rs b/native/src/input/mouse/event.rs index 7b68208f..478f9b4d 100644 --- a/native/src/input/mouse/event.rs +++ b/native/src/input/mouse/event.rs @@ -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, }, } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 5675076d..0760dd7e 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -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), } } } diff --git a/native/src/widget.rs b/native/src/widget.rs index bcef2665..c04f3377 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -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, ) { } } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 4ab59f7f..cf5dba93 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -31,6 +31,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, + _renderer: &Renderer, ) { match event { Event::Mouse(mouse::Event::Input { diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 5393417e..b8053238 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -20,6 +20,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, + _renderer: &Renderer, ) { match event { Event::Mouse(mouse::Event::Input { diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 7995cf5d..086d05ef 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -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, + ) }, ); } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 27b8f8a8..93ec4a36 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -21,6 +21,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, + _renderer: &Renderer, ) { match event { Event::Mouse(mouse::Event::Input { diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 5ec27159..7dbfb92a 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -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, + ) }, ); } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs new file mode 100644 index 00000000..de4c749c --- /dev/null +++ b/native/src/widget/scrollable.rs @@ -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) + } +} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index d643d902..be2b9b22 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -25,6 +25,7 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, + _renderer: &Renderer, ) { let mut change = || { let bounds = layout.bounds(); diff --git a/src/winit.rs b/src/winit.rs index 64e301f4..b2200551 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -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>; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index cac5e113..04fae248 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -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" diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index c883eaa8..75cfa166 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -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, diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 0b9e2c41..354b0851 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -23,4 +23,9 @@ pub enum Primitive { path: String, bounds: Rectangle, }, + Clip { + bounds: Rectangle, + offset: u32, + content: Box<Primitive>, + }, } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index adb294f0..bfbd7e2d 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -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, diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs index ab6f744f..a70693af 100644 --- a/wgpu/src/renderer.rs +++ b/wgpu/src/renderer.rs @@ -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, - ); - - self.quads.clear(); - - self.image_pipeline.draw( - &mut self.device, - &mut encoder, - &self.images, - 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.images.clear(); + self.draw_primitive(primitive, &mut current, &mut layers); + layers.push(current); - 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"); } } } diff --git a/wgpu/src/renderer/scrollable.rs b/wgpu/src/renderer/scrollable.rs new file mode 100644 index 00000000..e812a7e1 --- /dev/null +++ b/wgpu/src/renderer/scrollable.rs @@ -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 + }, + ) + } +} diff --git a/wgpu/src/transformation.rs b/wgpu/src/transformation.rs index 1101e135..b0d14cc8 100644 --- a/wgpu/src/transformation.rs +++ b/wgpu/src/transformation.rs @@ -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() } } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index fa5d6adf..c8227ac4 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -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" diff --git a/winit/src/application.rs b/winit/src/application.rs index 418ee3c4..c8748199 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -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; } |