diff options
-rw-r--r-- | core/src/widget/scrollable.rs | 20 | ||||
-rw-r--r-- | native/src/widget/scrollable.rs | 65 | ||||
-rw-r--r-- | wgpu/src/renderer/scrollable.rs | 87 |
3 files changed, 155 insertions, 17 deletions
diff --git a/core/src/widget/scrollable.rs b/core/src/widget/scrollable.rs index 1bad8555..31a5abed 100644 --- a/core/src/widget/scrollable.rs +++ b/core/src/widget/scrollable.rs @@ -1,4 +1,4 @@ -use crate::{Align, Column, Length, Rectangle}; +use crate::{Align, Column, Length, Point, Rectangle}; #[derive(Debug)] pub struct Scrollable<'a, Element> { @@ -103,6 +103,7 @@ impl<'a, Element> Scrollable<'a, Element> { #[derive(Debug, Clone, Copy, Default)] pub struct State { + pub scrollbar_grabbed_at: Option<Point>, offset: u32, } @@ -121,17 +122,30 @@ impl State { return; } - // TODO: Configurable speed (?) - self.offset = (self.offset as i32 - delta_y.round() as i32 * 15) + 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/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index e52f3c3f..76d12124 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,6 +1,7 @@ use crate::{ - column, input::mouse, Element, Event, Hasher, Layout, Node, Point, - Rectangle, Style, Widget, + column, + input::{mouse, ButtonState}, + Element, Event, Hasher, Layout, Node, Point, Rectangle, Style, Widget, }; pub use iced_core::scrollable::State; @@ -54,18 +55,65 @@ where 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_y, .. }) => { - self.state.scroll(delta_y, bounds, content_bounds); + // TODO: Configurable speed (?) + self.state.scroll(delta_y * 15.0, 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 + { + self.state.scroll( + scrollbar_grabbed_at.y - cursor_position.y, + bounds, + content_bounds, + ); + + self.state.scrollbar_grabbed_at = Some(cursor_position); + } } _ => {} } } - let cursor_position = if is_mouse_over { + 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 @@ -73,7 +121,7 @@ where ) } else { // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor unavailability. + // cursor availability. // This will probably happen naturally once we add multi-window // support. Point::new(cursor_position.x, -1.0) @@ -118,6 +166,13 @@ where } 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>, diff --git a/wgpu/src/renderer/scrollable.rs b/wgpu/src/renderer/scrollable.rs index 1327e577..7f8a7db6 100644 --- a/wgpu/src/renderer/scrollable.rs +++ b/wgpu/src/renderer/scrollable.rs @@ -1,9 +1,33 @@ use crate::{Primitive, Renderer}; use iced_native::{ - scrollable, Background, Color, Layout, Point, Rectangle, Scrollable, Widget, + scrollable, Background, Color, Layout, MouseCursor, Point, Rectangle, + Scrollable, Widget, }; +const SCROLLBAR_WIDTH: u16 = 10; +const SCROLLBAR_MARGIN: u16 = 10; + +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>, @@ -15,8 +39,15 @@ impl scrollable::Renderer for Renderer { 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 bounds.contains(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) @@ -32,16 +63,19 @@ impl scrollable::Renderer for Renderer { }; ( - if is_mouse_over && content_bounds.height > bounds.height { + 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: bounds.x + bounds.width - 12.0, - y: bounds.y + y_offset, - width: 10.0, + 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 { @@ -52,13 +86,48 @@ impl scrollable::Renderer for Renderer { }), border_radius: 5, }; - Primitive::Group { - primitives: vec![primitive, scrollbar], + + 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 }, - mouse_cursor, + if is_mouse_over_scrollbar + || scrollable.state.is_scrollbar_grabbed() + { + MouseCursor::Idle + } else { + mouse_cursor + }, ) } } |