diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a502e6..81bb08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,6 +235,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 default is `true`, which was the behavior before this flag was added. - `Image` now supports `ImageCornerRadius`. Thanks to @danbulant for helping with this change! +- `Scroll` now exposes its scroll amount, maximum scroll, and more information + that allows completely customizing a scroll view's behavior. Thanks to + @danbulant for helping with this change! [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/examples/virtual-scroll-list.rs b/examples/virtual-scroll-list.rs index 8149f01..ce2c3b0 100644 --- a/examples/virtual-scroll-list.rs +++ b/examples/virtual-scroll-list.rs @@ -2,24 +2,20 @@ use cushy::styles::{Dimension, DimensionRange, Edges}; use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::Run; -use figures::units::{Lp, Px}; +use figures::units::{Lp, UPx}; use figures::{Point, Size}; fn list() -> impl MakeWidget { let height = Lp::inches(10); - let content_size: Dynamic> = Dynamic::default(); + let content_size: Dynamic> = Dynamic::default(); let control_size = Dynamic::default(); - let current_scroll: Dynamic> = Dynamic::default(); + let current_scroll: Dynamic> = Dynamic::default(); let max_scroll = Dynamic::default(); - let content = content_size - .map_each(|s| format!("Content size: {:?};", s)); - let control = control_size - .map_each(|s| format!("Control size: {:?};", s)); - let scroll = current_scroll - .map_each(|s| format!("Current scroll: {:?};", s)); - let max = max_scroll - .map_each(|s| format!("Max scroll: {:?};", s)); + let content = content_size.map_each(|s| format!("Content size: {:?};", s)); + let control = control_size.map_each(|s| format!("Control size: {:?};", s)); + let scroll = current_scroll.map_each(|s| format!("Current scroll: {:?};", s)); + let max = max_scroll.map_each(|s| format!("Max scroll: {:?};", s)); let content = content .and(control) @@ -29,23 +25,30 @@ fn list() -> impl MakeWidget { .and("Hello world!") .into_rows() .pad_by(current_scroll.map_each(|scroll| Edges { - top: Dimension::from(-scroll.y), + top: Dimension::from(scroll.y), ..Default::default() })) - .size(Size::new(DimensionRange::default(), DimensionRange::from(height))); + .size(Size::new( + DimensionRange::default(), + DimensionRange::from(height), + )); let scroll = content.scroll(); - scroll.get_content_size() + scroll + .content_size() .for_each_cloned(move |s| content_size.set(s)) .persist(); - scroll.get_control_size() + scroll + .control_size() .for_each_cloned(move |s| control_size.set(s)) .persist(); - scroll.get_scroll() + scroll + .scroll .for_each_cloned(move |s| current_scroll.set(s)) .persist(); - scroll.get_max_scroll() + scroll + .max_scroll() .for_each_cloned(move |s| max_scroll.set(s)) .persist(); diff --git a/src/styles.rs b/src/styles.rs index 3413699..0606a41 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -757,6 +757,12 @@ impl Default for Dimension { } } +impl From for Dimension { + fn from(value: UPx) -> Self { + Self::Px(value.into_signed()) + } +} + impl From for Dimension { fn from(value: Px) -> Self { Self::Px(value) diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 1cdeb0a..a015853 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -22,18 +22,23 @@ use crate::ConstraintLimit; #[derive(Debug)] pub struct Scroll { contents: WidgetRef, - content_size: Dynamic>, - control_size: Dynamic>, - pub scroll: Dynamic>, + content_size: Dynamic>, + control_size: Dynamic>, + /// The current scroll position. + /// + /// When a new value is assigned to this, this widget will scroll its + /// contents. If a value is out of bounds of the maximum scroll, it will be + /// clamped and this dynamic will be updated with clamped scroll. + pub scroll: Dynamic>, enabled: Point, preserve_max_scroll: Value, - max_scroll: Dynamic>, + max_scroll: Dynamic>, scrollbar_opacity: Dynamic, scrollbar_opacity_animation: OpacityAnimationState, horizontal_bar: ScrollbarInfo, vertical_bar: ScrollbarInfo, - bar_width: Px, - line_height: Px, + bar_width: UPx, + line_height: UPx, drag: DragInfo, } @@ -62,8 +67,8 @@ impl Scroll { }, horizontal_bar: ScrollbarInfo::default(), vertical_bar: ScrollbarInfo::default(), - bar_width: Px::default(), - line_height: Px::default(), + bar_width: UPx::default(), + line_height: UPx::default(), drag: DragInfo::default(), preserve_max_scroll: Value::Constant(true), } @@ -98,35 +103,31 @@ impl Scroll { self } - /// Creates a reader for the scroll value. - pub fn get_scroll(&self) -> DynamicReader> { - self.scroll.create_reader() - } - - /// Creates a reader for the maximum scroll value. + /// Returns a reader for the maximum scroll value. + /// /// This represents the maximum amount that the scroll can be moved by. - /// This should usually mean that this value + the scroll visual size is the size of the content. - pub fn get_max_scroll(&self) -> DynamicReader> { + #[must_use] + pub fn max_scroll(&self) -> DynamicReader> { self.max_scroll.create_reader() } - /// Creates a reader for the content size. - /// This is the size of the content that is being scrolled. - pub fn get_content_size(&self) -> DynamicReader> { + /// Returns a reader for the size of the scrollable area. + #[must_use] + pub fn content_size(&self) -> DynamicReader> { self.content_size.create_reader() } - /// Creates a reader for the control size. - /// This is the size of the visible area of the scroll widget. - pub fn get_control_size(&self) -> DynamicReader> { + /// Returns a reader for the size of this Scroll widget. + #[must_use] + pub fn control_size(&self) -> DynamicReader> { self.control_size.create_reader() } - fn constrained_scroll(scroll: Point, max_scroll: Point) -> Point { - scroll.max(max_scroll).min(Point::default()) + fn constrained_scroll(scroll: Point, max_scroll: Point) -> Point { + scroll.min(max_scroll) } - fn constrain_scroll(&mut self) -> (Point, Point) { + fn constrain_scroll(&mut self) -> (Point, Point) { let scroll = self.scroll.get(); let max_scroll = self.max_scroll.get(); let clamped = Self::constrained_scroll(scroll, max_scroll); @@ -211,14 +212,15 @@ impl Widget for Scroll { let managed = self.contents.mounted(&mut context.as_event_context()); context.for_other(&managed).redraw(); - let size = context.gfx.region().size; + let size = context.gfx.region().size.into_unsigned(); if self.horizontal_bar.amount_hidden > 0 { context.gfx.draw_shape(&Shape::filled_rect( Rect::new( Point::new(self.horizontal_bar.offset, size.height - self.bar_width), Size::new(self.horizontal_bar.size, self.bar_width), - ), + ) + .into_signed(), // See https://github.com/khonsulabs/cushy/issues/186 Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), )); } @@ -228,7 +230,8 @@ impl Widget for Scroll { Rect::new( Point::new(size.width - self.bar_width, self.vertical_bar.offset), Size::new(self.bar_width, self.vertical_bar.size), - ), + ) + .into_signed(), // See https://github.com/khonsulabs/cushy/issues/186 Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), )); } @@ -241,8 +244,8 @@ impl Widget for Scroll { ) -> Size { self.bar_width = context .get(&ScrollBarThickness) - .into_px(context.gfx.scale()); - self.line_height = context.get(&LineHeight).into_px(context.gfx.scale()); + .into_upx(context.gfx.scale()); + self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale()); let (mut scroll, current_max_scroll) = self.constrain_scroll(); @@ -259,12 +262,9 @@ impl Widget for Scroll { }, ); let managed = self.contents.mounted(&mut context.as_event_context()); - let new_content_size = context - .for_other(&managed) - .layout(max_extents) - .into_signed(); + let new_content_size = context.for_other(&managed).layout(max_extents); - let layout_size = Size::new( + let new_control_size = Size::new( if self.enabled.x { constrain_child(available_space.width, new_content_size.width) } else { @@ -276,22 +276,21 @@ impl Widget for Scroll { new_content_size.height.into_unsigned() }, ); - let new_control_size = layout_size.into_signed(); self.horizontal_bar = scrollbar_region(scroll.x, new_content_size.width, new_control_size.width); let max_scroll_x = if self.enabled.x { - -self.horizontal_bar.amount_hidden + self.horizontal_bar.amount_hidden } else { - Px::ZERO + UPx::ZERO }; self.vertical_bar = scrollbar_region(scroll.y, new_content_size.height, new_control_size.height); let max_scroll_y = if self.enabled.y { - -self.vertical_bar.amount_hidden + self.vertical_bar.amount_hidden } else { - Px::ZERO + UPx::ZERO }; let new_max_scroll = Point::new(max_scroll_x, max_scroll_y); if current_max_scroll != new_max_scroll { @@ -304,23 +303,24 @@ impl Widget for Scroll { let control_size = self.control_size.get(); // Preserve the current scroll if the widget has resized - if content_size.width != new_content_size.width - || control_size.width != new_control_size.width - { - // content_size.width = new_content_size.width; - if self.preserve_max_scroll.get() && scroll.x == current_max_scroll.x { + if content_size != Size::ZERO && content_size != new_content_size { + if (content_size.width != new_content_size.width + || control_size.width != new_control_size.width) + && scroll.x == current_max_scroll.x + && self.preserve_max_scroll.get() + { scroll.x = max_scroll_x; } - } - if content_size.height != new_content_size.height - || control_size.height != new_control_size.height - { - // content_size.height = new_content_size.height; - if self.preserve_max_scroll.get() && scroll.y == current_max_scroll.y { + if (content_size.height != new_content_size.height + || control_size.height != new_control_size.height) + && scroll.y == current_max_scroll.y + && self.preserve_max_scroll.get() + { scroll.y = max_scroll_y; } } + // Set the current scroll, but prevent immediately triggering // invalidate. { @@ -333,13 +333,14 @@ impl Widget for Scroll { self.content_size.set(new_content_size); let region = Rect::new( - scroll, + -scroll.into_signed(), new_content_size - .min(Size::new(Px::MAX, Px::MAX) - scroll.max(Point::default())), + .min(Size::new(UPx::MAX, UPx::MAX) - scroll.max(Point::default())) + .into_signed(), ); context.set_child_layout(&managed, region); - layout_size + new_control_size } fn mouse_wheel( @@ -355,8 +356,10 @@ impl Widget for Scroll { }; let mut scroll = self.scroll.lock(); let old_scroll = *scroll; - let new_scroll = - Self::constrained_scroll(*scroll + amount.cast::(), self.max_scroll.get()); + let new_scroll = Self::constrained_scroll( + (scroll.into_signed() - amount.cast::()).into_unsigned(), + self.max_scroll.get(), + ); if old_scroll == new_scroll { IGNORED } else { @@ -378,11 +381,11 @@ impl Widget for Scroll { context: &mut EventContext<'_>, ) -> EventHandling { let control_size = self.control_size.get(); - - let relative_x = (control_size.width - location.x).max(Px::ZERO); + + let relative_x = (control_size.width.into_signed() - location.x).into_unsigned(); let in_vertical_area = self.enabled.y && relative_x <= self.bar_width; - let relative_y = (control_size.height - location.y).max(Px::ZERO); + let relative_y = (control_size.height.into_signed() - location.y).into_unsigned(); let in_horizontal_area = self.enabled.x && relative_y <= self.bar_width; if matches!( @@ -392,14 +395,14 @@ impl Widget for Scroll { return IGNORED; } - self.drag.start = location; + self.drag.start = location.into_signed(); self.drag.start_scroll = self.scroll.get(); self.drag.horizontal = in_horizontal_area; self.drag.in_bar = if in_horizontal_area { - let relative = location.x - self.horizontal_bar.offset; + let relative = location.x - self.horizontal_bar.offset.into_signed(); relative >= 0 && relative < self.horizontal_bar.size } else { - let relative = location.y - self.vertical_bar.offset; + let relative = location.y - self.vertical_bar.offset.into_signed(); relative >= 0 && relative < self.vertical_bar.size }; @@ -450,7 +453,9 @@ impl Widget for Scroll { if self.drag.mouse_buttons_down == 0 { if location.map_or(false, |location| { - Rect::from(self.control_size.get()).contains(location) + Rect::from(self.control_size.get()) + .into_signed() + .contains(location) }) { self.scrollbar_opacity_animation.handle.clear(); self.show_scrollbars(context); @@ -472,7 +477,7 @@ impl Widget for Scroll { struct DragInfo { mouse_buttons_down: usize, start: Point, - start_scroll: Point, + start_scroll: Point, horizontal: bool, in_bar: bool, } @@ -481,11 +486,11 @@ impl DragInfo { fn update( &self, location: Point, - dynamic_scroll: &Dynamic>, + dynamic_scroll: &Dynamic>, horizontal_bar: &ScrollbarInfo, vertical_bar: &ScrollbarInfo, - max_scroll: Point, - control_size: Size, + max_scroll: Point, + control_size: Size, ) { let mut scroll = dynamic_scroll.get(); if self.horizontal { @@ -514,33 +519,35 @@ impl DragInfo { &self, location: Px, start: Px, - max_scroll: Px, - start_scroll: Px, + max_scroll: UPx, + start_scroll: UPx, bar: &ScrollbarInfo, - control_size: Px, - ) -> Px { + control_size: UPx, + ) -> UPx { if self.in_bar { let dy = location - start; if dy == 0 { start_scroll } else { - (start_scroll - - Px::from( + (start_scroll.into_signed() + + Px::from( dy.into_float() / (control_size - bar.size).into_float() * bar.amount_hidden.into_float(), )) - .clamp(max_scroll, Px::ZERO) + .into_unsigned() + .min(max_scroll) } } else { max_scroll - * ((location - bar.size / 2).max(Px::ZERO).into_float() + * ((location - bar.size.into_signed() / 2) + .max(Px::ZERO) + .into_float() / (control_size - bar.size).into_float()) } } } -fn constrain_child(constraint: ConstraintLimit, measured: Px) -> UPx { - let measured = measured.into_unsigned(); +fn constrain_child(constraint: ConstraintLimit, measured: UPx) -> UPx { match constraint { ConstraintLimit::Fill(size) => size.min(measured), ConstraintLimit::SizeToFit(_) => measured, @@ -549,18 +556,18 @@ fn constrain_child(constraint: ConstraintLimit, measured: Px) -> UPx { #[derive(Debug, Default)] struct ScrollbarInfo { - offset: Px, - amount_hidden: Px, - size: Px, + offset: UPx, + amount_hidden: UPx, + size: UPx, } -fn scrollbar_region(scroll: Px, content_size: Px, control_size: Px) -> ScrollbarInfo { +fn scrollbar_region(scroll: UPx, content_size: UPx, control_size: UPx) -> ScrollbarInfo { if content_size > control_size { let amount_hidden = content_size - control_size; let ratio_visible = control_size.into_float() / content_size.into_float(); let bar_size = control_size * ratio_visible; let remaining_area = control_size - bar_size; - let amount_scrolled = -scroll.into_float() / amount_hidden.into_float(); + let amount_scrolled = scroll.into_float() / amount_hidden.into_float(); let bar_offset = remaining_area * amount_scrolled; ScrollbarInfo { offset: bar_offset,