Reworked Scrollable to account for lack of widget order guarantees.

Fixed thumb "snapping" bug on scrollable when cursor is out of bounds.
This commit is contained in:
bungoboingo 2022-12-24 21:27:44 -08:00
parent d91f4f6aa7
commit 9f85e0c721
9 changed files with 624 additions and 697 deletions

View file

@ -75,3 +75,11 @@ impl std::ops::Sub<Point> for Point {
Vector::new(self.x - point.x, self.y - point.y) Vector::new(self.x - point.x, self.y - point.y)
} }
} }
impl std::ops::Add<Point> for Point {
type Output = Point;
fn add(self, point: Point) -> Point {
Point::new(self.x + point.x, self.y + point.y)
}
}

View file

@ -111,6 +111,12 @@ impl Rectangle<f32> {
} }
} }
impl std::cmp::PartialOrd for Rectangle<f32> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
(self.width * self.height).partial_cmp(&(other.width * other.height))
}
}
impl std::ops::Mul<f32> for Rectangle<f32> { impl std::ops::Mul<f32> for Rectangle<f32> {
type Output = Self; type Output = Self;

View file

@ -7,4 +7,4 @@ publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["debug"] } iced = { path = "../..", features = ["debug"] }
lazy_static = "1.4" once_cell = "1.16.0"

View file

@ -1,15 +1,13 @@
use iced::widget::scrollable::{Scrollbar, Scroller}; use iced::widget::scrollable::{Properties, Scrollbar, Scroller};
use iced::widget::{ use iced::widget::{
button, column, container, horizontal_space, progress_bar, radio, row, button, column, container, horizontal_space, progress_bar, radio, row,
scrollable, slider, text, vertical_space, scrollable, slider, text, vertical_space,
}; };
use iced::{executor, theme, Alignment, Color, Vector}; use iced::{executor, theme, Alignment, Color, Point};
use iced::{Application, Command, Element, Length, Settings, Theme}; use iced::{Application, Command, Element, Length, Settings, Theme};
use lazy_static::lazy_static; use once_cell::sync::Lazy;
lazy_static! { static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);
static ref SCROLLABLE_ID: scrollable::Id = scrollable::Id::unique();
}
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
ScrollableDemo::run(Settings::default()) ScrollableDemo::run(Settings::default())
@ -20,7 +18,7 @@ struct ScrollableDemo {
scrollbar_width: u16, scrollbar_width: u16,
scrollbar_margin: u16, scrollbar_margin: u16,
scroller_width: u16, scroller_width: u16,
current_scroll_offset: Vector<f32>, current_scroll_offset: Point,
} }
#[derive(Debug, Clone, Eq, PartialEq, Copy)] #[derive(Debug, Clone, Eq, PartialEq, Copy)]
@ -36,9 +34,9 @@ enum Message {
ScrollbarWidthChanged(u16), ScrollbarWidthChanged(u16),
ScrollbarMarginChanged(u16), ScrollbarMarginChanged(u16),
ScrollerWidthChanged(u16), ScrollerWidthChanged(u16),
ScrollToBeginning(scrollable::Direction), ScrollToBeginning,
ScrollToEnd(scrollable::Direction), ScrollToEnd,
Scrolled(Vector<f32>), Scrolled(Point),
} }
impl Application for ScrollableDemo { impl Application for ScrollableDemo {
@ -54,7 +52,7 @@ impl Application for ScrollableDemo {
scrollbar_width: 10, scrollbar_width: 10,
scrollbar_margin: 0, scrollbar_margin: 0,
scroller_width: 10, scroller_width: 10,
current_scroll_offset: Vector::new(0.0, 0.0), current_scroll_offset: Point::ORIGIN,
}, },
Command::none(), Command::none(),
) )
@ -67,10 +65,13 @@ impl Application for ScrollableDemo {
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::SwitchDirection(direction) => { Message::SwitchDirection(direction) => {
self.current_scroll_offset = Vector::new(0.0, 0.0); self.current_scroll_offset = Point::ORIGIN;
self.scrollable_direction = direction; self.scrollable_direction = direction;
Command::none() scrollable::snap_to(
SCROLLABLE_ID.clone(),
self.current_scroll_offset,
)
} }
Message::ScrollbarWidthChanged(width) => { Message::ScrollbarWidthChanged(width) => {
self.scrollbar_width = width; self.scrollbar_width = width;
@ -87,40 +88,20 @@ impl Application for ScrollableDemo {
Command::none() Command::none()
} }
Message::ScrollToBeginning(direction) => { Message::ScrollToBeginning => {
match direction { self.current_scroll_offset = Point::ORIGIN;
scrollable::Direction::Horizontal => {
self.current_scroll_offset.x = 0.0;
}
scrollable::Direction::Vertical => {
self.current_scroll_offset.y = 0.0;
}
}
scrollable::snap_to( scrollable::snap_to(
SCROLLABLE_ID.clone(), SCROLLABLE_ID.clone(),
Vector::new( self.current_scroll_offset,
self.current_scroll_offset.x,
self.current_scroll_offset.y,
),
) )
} }
Message::ScrollToEnd(direction) => { Message::ScrollToEnd => {
match direction { self.current_scroll_offset = Point::new(1.0, 1.0);
scrollable::Direction::Horizontal => {
self.current_scroll_offset.x = 1.0;
}
scrollable::Direction::Vertical => {
self.current_scroll_offset.y = 1.0;
}
}
scrollable::snap_to( scrollable::snap_to(
SCROLLABLE_ID.clone(), SCROLLABLE_ID.clone(),
Vector::new( self.current_scroll_offset,
self.current_scroll_offset.x,
self.current_scroll_offset.y,
),
) )
} }
Message::Scrolled(offset) => { Message::Scrolled(offset) => {
@ -186,33 +167,29 @@ impl Application for ScrollableDemo {
.spacing(20) .spacing(20)
.width(Length::Fill); .width(Length::Fill);
let scroll_to_end_button = |direction: scrollable::Direction| { let scroll_to_end_button = || {
button("Scroll to end") button("Scroll to end")
.padding(10) .padding(10)
.width(Length::Units(120)) .on_press(Message::ScrollToEnd)
.on_press(Message::ScrollToEnd(direction))
}; };
let scroll_to_beginning_button = |direction: scrollable::Direction| { let scroll_to_beginning_button = || {
button("Scroll to beginning") button("Scroll to beginning")
.padding(10) .padding(10)
.width(Length::Units(120)) .on_press(Message::ScrollToBeginning)
.on_press(Message::ScrollToBeginning(direction))
}; };
let scrollable_content: Element<Message> = let scrollable_content: Element<Message> =
Element::from(match self.scrollable_direction { Element::from(match self.scrollable_direction {
Direction::Vertical => scrollable( Direction::Vertical => scrollable(
column![ column![
scroll_to_end_button(scrollable::Direction::Vertical), scroll_to_end_button(),
text("Beginning!"), text("Beginning!"),
vertical_space(Length::Units(1200)), vertical_space(Length::Units(1200)),
text("Middle!"), text("Middle!"),
vertical_space(Length::Units(1200)), vertical_space(Length::Units(1200)),
text("End!"), text("End!"),
scroll_to_beginning_button( scroll_to_beginning_button(),
scrollable::Direction::Vertical
),
] ]
.width(Length::Fill) .width(Length::Fill)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -220,22 +197,23 @@ impl Application for ScrollableDemo {
.spacing(40), .spacing(40),
) )
.height(Length::Fill) .height(Length::Fill)
.scrollbar_width(self.scrollbar_width) .vertical_scroll(
.scrollbar_margin(self.scrollbar_margin) Properties::new()
.scroller_width(self.scroller_width) .width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width),
)
.id(SCROLLABLE_ID.clone()) .id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled), .on_scroll(Message::Scrolled),
Direction::Horizontal => scrollable( Direction::Horizontal => scrollable(
row![ row![
scroll_to_end_button(scrollable::Direction::Horizontal), scroll_to_end_button(),
text("Beginning!"), text("Beginning!"),
horizontal_space(Length::Units(1200)), horizontal_space(Length::Units(1200)),
text("Middle!"), text("Middle!"),
horizontal_space(Length::Units(1200)), horizontal_space(Length::Units(1200)),
text("End!"), text("End!"),
scroll_to_beginning_button( scroll_to_beginning_button(),
scrollable::Direction::Horizontal
),
] ]
.height(Length::Units(450)) .height(Length::Units(450))
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -244,14 +222,12 @@ impl Application for ScrollableDemo {
) )
.height(Length::Fill) .height(Length::Fill)
.horizontal_scroll( .horizontal_scroll(
scrollable::Horizontal::new() Properties::new()
.scrollbar_height(self.scrollbar_width) .width(self.scrollbar_width)
.scrollbar_margin(self.scrollbar_margin) .margin(self.scrollbar_margin)
.scroller_height(self.scroller_width), .scroller_width(self.scroller_width),
) )
.style(theme::Scrollable::Custom(Box::new( .style(theme::Scrollable::custom(ScrollbarCustomStyle))
ScrollbarCustomStyle,
)))
.id(SCROLLABLE_ID.clone()) .id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled), .on_scroll(Message::Scrolled),
Direction::Multi => scrollable( Direction::Multi => scrollable(
@ -261,45 +237,43 @@ impl Application for ScrollableDemo {
text("Let's do some scrolling!"), text("Let's do some scrolling!"),
vertical_space(Length::Units(2400)) vertical_space(Length::Units(2400))
], ],
scroll_to_end_button(scrollable::Direction::Horizontal), scroll_to_end_button(),
text("Horizontal - Beginning!"), text("Horizontal - Beginning!"),
horizontal_space(Length::Units(1200)), horizontal_space(Length::Units(1200)),
//vertical content //vertical content
column![ column![
text("Horizontal - Middle!"), text("Horizontal - Middle!"),
scroll_to_end_button( scroll_to_end_button(),
scrollable::Direction::Vertical
),
text("Vertical - Beginning!"), text("Vertical - Beginning!"),
vertical_space(Length::Units(1200)), vertical_space(Length::Units(1200)),
text("Vertical - Middle!"), text("Vertical - Middle!"),
vertical_space(Length::Units(1200)), vertical_space(Length::Units(1200)),
text("Vertical - End!"), text("Vertical - End!"),
scroll_to_beginning_button( scroll_to_beginning_button(),
scrollable::Direction::Vertical vertical_space(Length::Units(40)),
)
] ]
.align_items(Alignment::Fill) .align_items(Alignment::Fill)
.spacing(40), .spacing(40),
horizontal_space(Length::Units(1200)), horizontal_space(Length::Units(1200)),
text("Horizontal - End!"), text("Horizontal - End!"),
scroll_to_beginning_button( scroll_to_beginning_button(),
scrollable::Direction::Horizontal
),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.padding([0, 40, 0, 40]) .padding([0, 40, 0, 40])
.spacing(40), .spacing(40),
) )
.height(Length::Fill) .height(Length::Fill)
.scrollbar_width(self.scrollbar_width) .vertical_scroll(
.scrollbar_margin(self.scrollbar_margin) Properties::new()
.scroller_width(self.scroller_width) .width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width),
)
.horizontal_scroll( .horizontal_scroll(
scrollable::Horizontal::new() Properties::new()
.scrollbar_height(self.scrollbar_width) .width(self.scrollbar_width)
.scrollbar_margin(self.scrollbar_margin) .margin(self.scrollbar_margin)
.scroller_height(self.scroller_width), .scroller_width(self.scroller_width),
) )
.style(theme::Scrollable::Custom(Box::new( .style(theme::Scrollable::Custom(Box::new(
ScrollbarCustomStyle, ScrollbarCustomStyle,

View file

@ -4,7 +4,7 @@ use iced::alignment::{self, Alignment};
use iced::widget::{ use iced::widget::{
button, column, container, row, scrollable, text, text_input, Column, button, column, container, row, scrollable, text, text_input, Column,
}; };
use iced::{executor, Vector}; use iced::{executor, Point};
use iced::{ use iced::{
Application, Color, Command, Element, Length, Settings, Subscription, Theme, Application, Color, Command, Element, Length, Settings, Subscription, Theme,
}; };
@ -83,7 +83,7 @@ impl Application for WebSocket {
scrollable::snap_to( scrollable::snap_to(
MESSAGE_LOG.clone(), MESSAGE_LOG.clone(),
Vector::new(0.0, 1.0), Point::new(0.0, 1.0),
) )
} }
}, },

View file

@ -1,19 +1,19 @@
//! Operate on widgets that can be scrolled. //! Operate on widgets that can be scrolled.
use crate::widget::{Id, Operation}; use crate::widget::{Id, Operation};
use iced_core::Vector; use iced_core::Point;
/// The internal state of a widget that can be scrolled. /// The internal state of a widget that can be scrolled.
pub trait Scrollable { pub trait Scrollable {
/// Snaps the scroll of the widget to the given `percentage`. /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
fn snap_to(&mut self, percentage: Vector<f32>); fn snap_to(&mut self, percentage: Point);
} }
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
/// the provided `percentage`. /// the provided `percentage`.
pub fn snap_to<T>(target: Id, percentage: Vector<f32>) -> impl Operation<T> { pub fn snap_to<T>(target: Id, percentage: Point) -> impl Operation<T> {
struct SnapTo { struct SnapTo {
target: Id, target: Id,
percentage: Vector<f32>, percentage: Point,
} }
impl<T> Operation<T> for SnapTo { impl<T> Operation<T> for SnapTo {

File diff suppressed because it is too large Load diff

View file

@ -99,8 +99,7 @@ pub mod radio {
pub mod scrollable { pub mod scrollable {
//! Navigate an endless amount of content with a scrollbar. //! Navigate an endless amount of content with a scrollbar.
pub use iced_native::widget::scrollable::{ pub use iced_native::widget::scrollable::{
snap_to, style::Scrollbar, style::Scroller, Direction, Horizontal, Id, snap_to, style::Scrollbar, style::Scroller, Id, Properties, StyleSheet,
StyleSheet,
}; };
/// A widget that can vertically display an infinite amount of content /// A widget that can vertically display an infinite amount of content

View file

@ -872,6 +872,15 @@ pub enum Scrollable {
Custom(Box<dyn scrollable::StyleSheet<Style = Theme>>), Custom(Box<dyn scrollable::StyleSheet<Style = Theme>>),
} }
impl Scrollable {
/// Creates a custom [`Scrollable`] theme.
pub fn custom<T: scrollable::StyleSheet<Style = Theme> + 'static>(
style: T,
) -> Self {
Self::Custom(Box::new(style))
}
}
impl scrollable::StyleSheet for Theme { impl scrollable::StyleSheet for Theme {
type Style = Scrollable; type Style = Scrollable;