Refactored Scroll to be UPx

Also addressed comments in #185
This commit is contained in:
Jonathan Johnson 2024-10-17 09:46:32 -07:00
parent e3898bda50
commit 51649a3c05
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 118 additions and 99 deletions

View file

@ -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. default is `true`, which was the behavior before this flag was added.
- `Image` now supports `ImageCornerRadius`. Thanks to @danbulant for helping - `Image` now supports `ImageCornerRadius`. Thanks to @danbulant for helping
with this change! 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 [139]: https://github.com/khonsulabs/cushy/issues/139

View file

@ -2,24 +2,20 @@ use cushy::styles::{Dimension, DimensionRange, Edges};
use cushy::value::{Destination, Dynamic, Source}; use cushy::value::{Destination, Dynamic, Source};
use cushy::widget::MakeWidget; use cushy::widget::MakeWidget;
use cushy::Run; use cushy::Run;
use figures::units::{Lp, Px}; use figures::units::{Lp, UPx};
use figures::{Point, Size}; use figures::{Point, Size};
fn list() -> impl MakeWidget { fn list() -> impl MakeWidget {
let height = Lp::inches(10); let height = Lp::inches(10);
let content_size: Dynamic<Size<Px>> = Dynamic::default(); let content_size: Dynamic<Size<UPx>> = Dynamic::default();
let control_size = Dynamic::default(); let control_size = Dynamic::default();
let current_scroll: Dynamic<Point<Px>> = Dynamic::default(); let current_scroll: Dynamic<Point<UPx>> = Dynamic::default();
let max_scroll = Dynamic::default(); let max_scroll = Dynamic::default();
let content = content_size let content = content_size.map_each(|s| format!("Content size: {:?};", s));
.map_each(|s| format!("Content size: {:?};", s)); let control = control_size.map_each(|s| format!("Control size: {:?};", s));
let control = control_size let scroll = current_scroll.map_each(|s| format!("Current scroll: {:?};", s));
.map_each(|s| format!("Control size: {:?};", s)); let max = max_scroll.map_each(|s| format!("Max scroll: {:?};", 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 let content = content
.and(control) .and(control)
@ -29,23 +25,30 @@ fn list() -> impl MakeWidget {
.and("Hello world!") .and("Hello world!")
.into_rows() .into_rows()
.pad_by(current_scroll.map_each(|scroll| Edges { .pad_by(current_scroll.map_each(|scroll| Edges {
top: Dimension::from(-scroll.y), top: Dimension::from(scroll.y),
..Default::default() ..Default::default()
})) }))
.size(Size::new(DimensionRange::default(), DimensionRange::from(height))); .size(Size::new(
DimensionRange::default(),
DimensionRange::from(height),
));
let scroll = content.scroll(); let scroll = content.scroll();
scroll.get_content_size() scroll
.content_size()
.for_each_cloned(move |s| content_size.set(s)) .for_each_cloned(move |s| content_size.set(s))
.persist(); .persist();
scroll.get_control_size() scroll
.control_size()
.for_each_cloned(move |s| control_size.set(s)) .for_each_cloned(move |s| control_size.set(s))
.persist(); .persist();
scroll.get_scroll() scroll
.scroll
.for_each_cloned(move |s| current_scroll.set(s)) .for_each_cloned(move |s| current_scroll.set(s))
.persist(); .persist();
scroll.get_max_scroll() scroll
.max_scroll()
.for_each_cloned(move |s| max_scroll.set(s)) .for_each_cloned(move |s| max_scroll.set(s))
.persist(); .persist();

View file

@ -757,6 +757,12 @@ impl Default for Dimension {
} }
} }
impl From<UPx> for Dimension {
fn from(value: UPx) -> Self {
Self::Px(value.into_signed())
}
}
impl From<Px> for Dimension { impl From<Px> for Dimension {
fn from(value: Px) -> Self { fn from(value: Px) -> Self {
Self::Px(value) Self::Px(value)

View file

@ -22,18 +22,23 @@ use crate::ConstraintLimit;
#[derive(Debug)] #[derive(Debug)]
pub struct Scroll { pub struct Scroll {
contents: WidgetRef, contents: WidgetRef,
content_size: Dynamic<Size<Px>>, content_size: Dynamic<Size<UPx>>,
control_size: Dynamic<Size<Px>>, control_size: Dynamic<Size<UPx>>,
pub scroll: Dynamic<Point<Px>>, /// 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<Point<UPx>>,
enabled: Point<bool>, enabled: Point<bool>,
preserve_max_scroll: Value<bool>, preserve_max_scroll: Value<bool>,
max_scroll: Dynamic<Point<Px>>, max_scroll: Dynamic<Point<UPx>>,
scrollbar_opacity: Dynamic<ZeroToOne>, scrollbar_opacity: Dynamic<ZeroToOne>,
scrollbar_opacity_animation: OpacityAnimationState, scrollbar_opacity_animation: OpacityAnimationState,
horizontal_bar: ScrollbarInfo, horizontal_bar: ScrollbarInfo,
vertical_bar: ScrollbarInfo, vertical_bar: ScrollbarInfo,
bar_width: Px, bar_width: UPx,
line_height: Px, line_height: UPx,
drag: DragInfo, drag: DragInfo,
} }
@ -62,8 +67,8 @@ impl Scroll {
}, },
horizontal_bar: ScrollbarInfo::default(), horizontal_bar: ScrollbarInfo::default(),
vertical_bar: ScrollbarInfo::default(), vertical_bar: ScrollbarInfo::default(),
bar_width: Px::default(), bar_width: UPx::default(),
line_height: Px::default(), line_height: UPx::default(),
drag: DragInfo::default(), drag: DragInfo::default(),
preserve_max_scroll: Value::Constant(true), preserve_max_scroll: Value::Constant(true),
} }
@ -98,35 +103,31 @@ impl Scroll {
self self
} }
/// Creates a reader for the scroll value. /// Returns a reader for the maximum scroll value.
pub fn get_scroll(&self) -> DynamicReader<Point<Px>> { ///
self.scroll.create_reader()
}
/// Creates a reader for the maximum scroll value.
/// This represents the maximum amount that the scroll can be moved by. /// 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. #[must_use]
pub fn get_max_scroll(&self) -> DynamicReader<Point<Px>> { pub fn max_scroll(&self) -> DynamicReader<Point<UPx>> {
self.max_scroll.create_reader() self.max_scroll.create_reader()
} }
/// Creates a reader for the content size. /// Returns a reader for the size of the scrollable area.
/// This is the size of the content that is being scrolled. #[must_use]
pub fn get_content_size(&self) -> DynamicReader<Size<Px>> { pub fn content_size(&self) -> DynamicReader<Size<UPx>> {
self.content_size.create_reader() self.content_size.create_reader()
} }
/// Creates a reader for the control size. /// Returns a reader for the size of this Scroll widget.
/// This is the size of the visible area of the scroll widget. #[must_use]
pub fn get_control_size(&self) -> DynamicReader<Size<Px>> { pub fn control_size(&self) -> DynamicReader<Size<UPx>> {
self.control_size.create_reader() self.control_size.create_reader()
} }
fn constrained_scroll(scroll: Point<Px>, max_scroll: Point<Px>) -> Point<Px> { fn constrained_scroll(scroll: Point<UPx>, max_scroll: Point<UPx>) -> Point<UPx> {
scroll.max(max_scroll).min(Point::default()) scroll.min(max_scroll)
} }
fn constrain_scroll(&mut self) -> (Point<Px>, Point<Px>) { fn constrain_scroll(&mut self) -> (Point<UPx>, Point<UPx>) {
let scroll = self.scroll.get(); let scroll = self.scroll.get();
let max_scroll = self.max_scroll.get(); let max_scroll = self.max_scroll.get();
let clamped = Self::constrained_scroll(scroll, max_scroll); 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()); let managed = self.contents.mounted(&mut context.as_event_context());
context.for_other(&managed).redraw(); 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 { if self.horizontal_bar.amount_hidden > 0 {
context.gfx.draw_shape(&Shape::filled_rect( context.gfx.draw_shape(&Shape::filled_rect(
Rect::new( Rect::new(
Point::new(self.horizontal_bar.offset, size.height - self.bar_width), Point::new(self.horizontal_bar.offset, size.height - self.bar_width),
Size::new(self.horizontal_bar.size, 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()), Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
)); ));
} }
@ -228,7 +230,8 @@ impl Widget for Scroll {
Rect::new( Rect::new(
Point::new(size.width - self.bar_width, self.vertical_bar.offset), Point::new(size.width - self.bar_width, self.vertical_bar.offset),
Size::new(self.bar_width, self.vertical_bar.size), 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()), Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
)); ));
} }
@ -241,8 +244,8 @@ impl Widget for Scroll {
) -> Size<UPx> { ) -> Size<UPx> {
self.bar_width = context self.bar_width = context
.get(&ScrollBarThickness) .get(&ScrollBarThickness)
.into_px(context.gfx.scale()); .into_upx(context.gfx.scale());
self.line_height = context.get(&LineHeight).into_px(context.gfx.scale()); self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale());
let (mut scroll, current_max_scroll) = self.constrain_scroll(); 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 managed = self.contents.mounted(&mut context.as_event_context());
let new_content_size = context let new_content_size = context.for_other(&managed).layout(max_extents);
.for_other(&managed)
.layout(max_extents)
.into_signed();
let layout_size = Size::new( let new_control_size = Size::new(
if self.enabled.x { if self.enabled.x {
constrain_child(available_space.width, new_content_size.width) constrain_child(available_space.width, new_content_size.width)
} else { } else {
@ -276,22 +276,21 @@ impl Widget for Scroll {
new_content_size.height.into_unsigned() new_content_size.height.into_unsigned()
}, },
); );
let new_control_size = layout_size.into_signed();
self.horizontal_bar = self.horizontal_bar =
scrollbar_region(scroll.x, new_content_size.width, new_control_size.width); scrollbar_region(scroll.x, new_content_size.width, new_control_size.width);
let max_scroll_x = if self.enabled.x { let max_scroll_x = if self.enabled.x {
-self.horizontal_bar.amount_hidden self.horizontal_bar.amount_hidden
} else { } else {
Px::ZERO UPx::ZERO
}; };
self.vertical_bar = self.vertical_bar =
scrollbar_region(scroll.y, new_content_size.height, new_control_size.height); scrollbar_region(scroll.y, new_content_size.height, new_control_size.height);
let max_scroll_y = if self.enabled.y { let max_scroll_y = if self.enabled.y {
-self.vertical_bar.amount_hidden self.vertical_bar.amount_hidden
} else { } else {
Px::ZERO UPx::ZERO
}; };
let new_max_scroll = Point::new(max_scroll_x, max_scroll_y); let new_max_scroll = Point::new(max_scroll_x, max_scroll_y);
if current_max_scroll != new_max_scroll { if current_max_scroll != new_max_scroll {
@ -304,23 +303,24 @@ impl Widget for Scroll {
let control_size = self.control_size.get(); let control_size = self.control_size.get();
// Preserve the current scroll if the widget has resized // Preserve the current scroll if the widget has resized
if content_size.width != new_content_size.width if content_size != Size::ZERO && content_size != new_content_size {
|| control_size.width != new_control_size.width if (content_size.width != new_content_size.width
{ || control_size.width != new_control_size.width)
// content_size.width = new_content_size.width; && scroll.x == current_max_scroll.x
if self.preserve_max_scroll.get() && scroll.x == current_max_scroll.x { && self.preserve_max_scroll.get()
{
scroll.x = max_scroll_x; scroll.x = max_scroll_x;
} }
}
if content_size.height != new_content_size.height if (content_size.height != new_content_size.height
|| control_size.height != new_control_size.height || control_size.height != new_control_size.height)
{ && scroll.y == current_max_scroll.y
// content_size.height = new_content_size.height; && self.preserve_max_scroll.get()
if self.preserve_max_scroll.get() && scroll.y == current_max_scroll.y { {
scroll.y = max_scroll_y; scroll.y = max_scroll_y;
} }
} }
// Set the current scroll, but prevent immediately triggering // Set the current scroll, but prevent immediately triggering
// invalidate. // invalidate.
{ {
@ -333,13 +333,14 @@ impl Widget for Scroll {
self.content_size.set(new_content_size); self.content_size.set(new_content_size);
let region = Rect::new( let region = Rect::new(
scroll, -scroll.into_signed(),
new_content_size 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); context.set_child_layout(&managed, region);
layout_size new_control_size
} }
fn mouse_wheel( fn mouse_wheel(
@ -355,8 +356,10 @@ impl Widget for Scroll {
}; };
let mut scroll = self.scroll.lock(); let mut scroll = self.scroll.lock();
let old_scroll = *scroll; let old_scroll = *scroll;
let new_scroll = let new_scroll = Self::constrained_scroll(
Self::constrained_scroll(*scroll + amount.cast::<Px>(), self.max_scroll.get()); (scroll.into_signed() - amount.cast::<Px>()).into_unsigned(),
self.max_scroll.get(),
);
if old_scroll == new_scroll { if old_scroll == new_scroll {
IGNORED IGNORED
} else { } else {
@ -378,11 +381,11 @@ impl Widget for Scroll {
context: &mut EventContext<'_>, context: &mut EventContext<'_>,
) -> EventHandling { ) -> EventHandling {
let control_size = self.control_size.get(); 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 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; let in_horizontal_area = self.enabled.x && relative_y <= self.bar_width;
if matches!( if matches!(
@ -392,14 +395,14 @@ impl Widget for Scroll {
return IGNORED; return IGNORED;
} }
self.drag.start = location; self.drag.start = location.into_signed();
self.drag.start_scroll = self.scroll.get(); self.drag.start_scroll = self.scroll.get();
self.drag.horizontal = in_horizontal_area; self.drag.horizontal = in_horizontal_area;
self.drag.in_bar = if 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 relative >= 0 && relative < self.horizontal_bar.size
} else { } 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 relative >= 0 && relative < self.vertical_bar.size
}; };
@ -450,7 +453,9 @@ impl Widget for Scroll {
if self.drag.mouse_buttons_down == 0 { if self.drag.mouse_buttons_down == 0 {
if location.map_or(false, |location| { 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.scrollbar_opacity_animation.handle.clear();
self.show_scrollbars(context); self.show_scrollbars(context);
@ -472,7 +477,7 @@ impl Widget for Scroll {
struct DragInfo { struct DragInfo {
mouse_buttons_down: usize, mouse_buttons_down: usize,
start: Point<Px>, start: Point<Px>,
start_scroll: Point<Px>, start_scroll: Point<UPx>,
horizontal: bool, horizontal: bool,
in_bar: bool, in_bar: bool,
} }
@ -481,11 +486,11 @@ impl DragInfo {
fn update( fn update(
&self, &self,
location: Point<Px>, location: Point<Px>,
dynamic_scroll: &Dynamic<Point<Px>>, dynamic_scroll: &Dynamic<Point<UPx>>,
horizontal_bar: &ScrollbarInfo, horizontal_bar: &ScrollbarInfo,
vertical_bar: &ScrollbarInfo, vertical_bar: &ScrollbarInfo,
max_scroll: Point<Px>, max_scroll: Point<UPx>,
control_size: Size<Px>, control_size: Size<UPx>,
) { ) {
let mut scroll = dynamic_scroll.get(); let mut scroll = dynamic_scroll.get();
if self.horizontal { if self.horizontal {
@ -514,33 +519,35 @@ impl DragInfo {
&self, &self,
location: Px, location: Px,
start: Px, start: Px,
max_scroll: Px, max_scroll: UPx,
start_scroll: Px, start_scroll: UPx,
bar: &ScrollbarInfo, bar: &ScrollbarInfo,
control_size: Px, control_size: UPx,
) -> Px { ) -> UPx {
if self.in_bar { if self.in_bar {
let dy = location - start; let dy = location - start;
if dy == 0 { if dy == 0 {
start_scroll start_scroll
} else { } else {
(start_scroll (start_scroll.into_signed()
- Px::from( + Px::from(
dy.into_float() / (control_size - bar.size).into_float() dy.into_float() / (control_size - bar.size).into_float()
* bar.amount_hidden.into_float(), * bar.amount_hidden.into_float(),
)) ))
.clamp(max_scroll, Px::ZERO) .into_unsigned()
.min(max_scroll)
} }
} else { } else {
max_scroll 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()) / (control_size - bar.size).into_float())
} }
} }
} }
fn constrain_child(constraint: ConstraintLimit, measured: Px) -> UPx { fn constrain_child(constraint: ConstraintLimit, measured: UPx) -> UPx {
let measured = measured.into_unsigned();
match constraint { match constraint {
ConstraintLimit::Fill(size) => size.min(measured), ConstraintLimit::Fill(size) => size.min(measured),
ConstraintLimit::SizeToFit(_) => measured, ConstraintLimit::SizeToFit(_) => measured,
@ -549,18 +556,18 @@ fn constrain_child(constraint: ConstraintLimit, measured: Px) -> UPx {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct ScrollbarInfo { struct ScrollbarInfo {
offset: Px, offset: UPx,
amount_hidden: Px, amount_hidden: UPx,
size: Px, 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 { if content_size > control_size {
let amount_hidden = content_size - control_size; let amount_hidden = content_size - control_size;
let ratio_visible = control_size.into_float() / content_size.into_float(); let ratio_visible = control_size.into_float() / content_size.into_float();
let bar_size = control_size * ratio_visible; let bar_size = control_size * ratio_visible;
let remaining_area = control_size - bar_size; 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; let bar_offset = remaining_area * amount_scrolled;
ScrollbarInfo { ScrollbarInfo {
offset: bar_offset, offset: bar_offset,