summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--core/src/size.rs1
-rw-r--r--examples/README.md2
-rw-r--r--examples/scrollable/Cargo.toml1
-rw-r--r--examples/scrollable/screenshot.pngbin104995 -> 521151 bytes
-rw-r--r--examples/scrollable/src/main.rs498
-rw-r--r--examples/websocket/src/main.rs5
-rw-r--r--native/src/widget/column.rs2
-rw-r--r--native/src/widget/operation/scrollable.rs41
-rw-r--r--native/src/widget/scrollable.rs961
-rw-r--r--src/widget.rs3
-rw-r--r--style/src/scrollable.rs17
-rw-r--r--style/src/theme.rs33
12 files changed, 1043 insertions, 521 deletions
diff --git a/core/src/size.rs b/core/src/size.rs
index 31f3171b..a2c72926 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,5 +1,4 @@
use crate::{Padding, Vector};
-use std::f32;
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
diff --git a/examples/README.md b/examples/README.md
index bb15dc2e..74cf145b 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -99,7 +99,7 @@ A bunch of simpler examples exist:
- [`pick_list`](pick_list), a dropdown list of selectable options.
- [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].
- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider.
-- [`scrollable`](scrollable), a showcase of the various scrollbar width options.
+- [`scrollable`](scrollable), a showcase of various scrollable content configurations.
- [`sierpinski_triangle`](sierpinski_triangle), a [sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use `Canvas` and `Slider`.
- [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.
- [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time.
diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml
index 610c13b4..e6411e26 100644
--- a/examples/scrollable/Cargo.toml
+++ b/examples/scrollable/Cargo.toml
@@ -7,3 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
+once_cell = "1.16.0"
diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png
index e91fd565..ee044447 100644
--- a/examples/scrollable/screenshot.png
+++ b/examples/scrollable/screenshot.png
Binary files differ
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index 6eba34e2..128d98b2 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -1,44 +1,58 @@
-use iced::executor;
+use iced::widget::scrollable::{Properties, Scrollbar, Scroller};
use iced::widget::{
- button, column, container, horizontal_rule, progress_bar, radio,
- scrollable, text, vertical_space, Row,
+ button, column, container, horizontal_space, progress_bar, radio, row,
+ scrollable, slider, text, vertical_space,
};
+use iced::{executor, theme, Alignment, Color};
use iced::{Application, Command, Element, Length, Settings, Theme};
+use once_cell::sync::Lazy;
+
+static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);
pub fn main() -> iced::Result {
ScrollableDemo::run(Settings::default())
}
struct ScrollableDemo {
- theme: Theme,
- variants: Vec<Variant>,
+ scrollable_direction: Direction,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
+ current_scroll_offset: scrollable::RelativeOffset,
}
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-enum ThemeType {
- Light,
- Dark,
+#[derive(Debug, Clone, Eq, PartialEq, Copy)]
+enum Direction {
+ Vertical,
+ Horizontal,
+ Multi,
}
#[derive(Debug, Clone)]
enum Message {
- ThemeChanged(ThemeType),
- ScrollToTop(usize),
- ScrollToBottom(usize),
- Scrolled(usize, f32),
+ SwitchDirection(Direction),
+ ScrollbarWidthChanged(u16),
+ ScrollbarMarginChanged(u16),
+ ScrollerWidthChanged(u16),
+ ScrollToBeginning,
+ ScrollToEnd,
+ Scrolled(scrollable::RelativeOffset),
}
impl Application for ScrollableDemo {
+ type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
- type Executor = executor::Default;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
(
ScrollableDemo {
- theme: Default::default(),
- variants: Variant::all(),
+ scrollable_direction: Direction::Vertical,
+ scrollbar_width: 10,
+ scrollbar_margin: 0,
+ scroller_width: 10,
+ current_scroll_offset: scrollable::RelativeOffset::START,
},
Command::none(),
)
@@ -50,36 +64,48 @@ impl Application for ScrollableDemo {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
- Message::ThemeChanged(theme) => {
- self.theme = match theme {
- ThemeType::Light => Theme::Light,
- ThemeType::Dark => Theme::Dark,
- };
+ Message::SwitchDirection(direction) => {
+ self.current_scroll_offset = scrollable::RelativeOffset::START;
+ self.scrollable_direction = direction;
+
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ self.current_scroll_offset,
+ )
+ }
+ Message::ScrollbarWidthChanged(width) => {
+ self.scrollbar_width = width;
+
+ Command::none()
+ }
+ Message::ScrollbarMarginChanged(margin) => {
+ self.scrollbar_margin = margin;
Command::none()
}
- Message::ScrollToTop(i) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = 0.0;
-
- scrollable::snap_to(Variant::id(i), 0.0)
- } else {
- Command::none()
- }
+ Message::ScrollerWidthChanged(width) => {
+ self.scroller_width = width;
+
+ Command::none()
}
- Message::ScrollToBottom(i) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = 1.0;
-
- scrollable::snap_to(Variant::id(i), 1.0)
- } else {
- Command::none()
- }
+ Message::ScrollToBeginning => {
+ self.current_scroll_offset = scrollable::RelativeOffset::START;
+
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ self.current_scroll_offset,
+ )
}
- Message::Scrolled(i, offset) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = offset;
- }
+ Message::ScrollToEnd => {
+ self.current_scroll_offset = scrollable::RelativeOffset::END;
+
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ self.current_scroll_offset,
+ )
+ }
+ Message::Scrolled(offset) => {
+ self.current_scroll_offset = offset;
Command::none()
}
@@ -87,172 +113,262 @@ impl Application for ScrollableDemo {
}
fn view(&self) -> Element<Message> {
- let ScrollableDemo { variants, .. } = self;
-
- let choose_theme = [ThemeType::Light, ThemeType::Dark].iter().fold(
- column!["Choose a theme:"].spacing(10),
- |column, option| {
- column.push(radio(
- format!("{:?}", option),
- *option,
- Some(*option),
- Message::ThemeChanged,
- ))
- },
+ let scrollbar_width_slider = slider(
+ 0..=15,
+ self.scrollbar_width,
+ Message::ScrollbarWidthChanged,
+ );
+ let scrollbar_margin_slider = slider(
+ 0..=15,
+ self.scrollbar_margin,
+ Message::ScrollbarMarginChanged,
);
+ let scroller_width_slider =
+ slider(0..=15, self.scroller_width, Message::ScrollerWidthChanged);
- let scrollable_row = Row::with_children(
- variants
- .iter()
- .enumerate()
- .map(|(i, variant)| {
- let mut contents = column![
- variant.title,
- button("Scroll to bottom",)
- .width(Length::Fill)
- .padding(10)
- .on_press(Message::ScrollToBottom(i)),
- ]
- .padding(10)
- .spacing(10)
- .width(Length::Fill);
-
- if let Some(scrollbar_width) = variant.scrollbar_width {
- contents = contents.push(text(format!(
- "scrollbar_width: {:?}",
- scrollbar_width
- )));
- }
-
- if let Some(scrollbar_margin) = variant.scrollbar_margin {
- contents = contents.push(text(format!(
- "scrollbar_margin: {:?}",
- scrollbar_margin
- )));
- }
-
- if let Some(scroller_width) = variant.scroller_width {
- contents = contents.push(text(format!(
- "scroller_width: {:?}",
- scroller_width
- )));
- }
-
- contents = contents
- .push(vertical_space(Length::Units(100)))
- .push(
- "Some content that should wrap within the \
- scrollable. Let's output a lot of short words, so \
- that we'll make sure to see how wrapping works \
- with these scrollbars.",
- )
- .push(vertical_space(Length::Units(1200)))
- .push("Middle")
- .push(vertical_space(Length::Units(1200)))
- .push("The End.")
- .push(
- button("Scroll to top")
- .width(Length::Fill)
- .padding(10)
- .on_press(Message::ScrollToTop(i)),
- );
-
- let mut scrollable = scrollable(contents)
- .id(Variant::id(i))
- .height(Length::Fill)
- .on_scroll(move |offset| Message::Scrolled(i, offset));
-
- if let Some(scrollbar_width) = variant.scrollbar_width {
- scrollable =
- scrollable.scrollbar_width(scrollbar_width);
- }
-
- if let Some(scrollbar_margin) = variant.scrollbar_margin {
- scrollable =
- scrollable.scrollbar_margin(scrollbar_margin);
- }
-
- if let Some(scroller_width) = variant.scroller_width {
- scrollable = scrollable.scroller_width(scroller_width);
- }
+ let scroll_slider_controls = column![
+ text("Scrollbar width:"),
+ scrollbar_width_slider,
+ text("Scrollbar margin:"),
+ scrollbar_margin_slider,
+ text("Scroller width:"),
+ scroller_width_slider,
+ ]
+ .spacing(10)
+ .width(Length::Fill);
+
+ let scroll_orientation_controls = column(vec![
+ text("Scrollbar direction:").into(),
+ radio(
+ "Vertical",
+ Direction::Vertical,
+ Some(self.scrollable_direction),
+ Message::SwitchDirection,
+ )
+ .into(),
+ radio(
+ "Horizontal",
+ Direction::Horizontal,
+ Some(self.scrollable_direction),
+ Message::SwitchDirection,
+ )
+ .into(),
+ radio(
+ "Both!",
+ Direction::Multi,
+ Some(self.scrollable_direction),
+ Message::SwitchDirection,
+ )
+ .into(),
+ ])
+ .spacing(10)
+ .width(Length::Fill);
+ let scroll_controls =
+ row![scroll_slider_controls, scroll_orientation_controls]
+ .spacing(20)
+ .width(Length::Fill);
+
+ let scroll_to_end_button = || {
+ button("Scroll to end")
+ .padding(10)
+ .on_press(Message::ScrollToEnd)
+ };
+
+ let scroll_to_beginning_button = || {
+ button("Scroll to beginning")
+ .padding(10)
+ .on_press(Message::ScrollToBeginning)
+ };
+
+ let scrollable_content: Element<Message> =
+ Element::from(match self.scrollable_direction {
+ Direction::Vertical => scrollable(
column![
- scrollable,
- progress_bar(0.0..=1.0, variant.latest_offset,)
+ scroll_to_end_button(),
+ text("Beginning!"),
+ vertical_space(Length::Units(1200)),
+ text("Middle!"),
+ vertical_space(Length::Units(1200)),
+ text("End!"),
+ scroll_to_beginning_button(),
]
.width(Length::Fill)
- .height(Length::Fill)
- .spacing(10)
+ .align_items(Alignment::Center)
+ .padding([40, 0, 40, 0])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .vertical_scroll(
+ Properties::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width),
+ )
+ .id(SCROLLABLE_ID.clone())
+ .on_scroll(Message::Scrolled),
+ Direction::Horizontal => scrollable(
+ row![
+ scroll_to_end_button(),
+ text("Beginning!"),
+ horizontal_space(Length::Units(1200)),
+ text("Middle!"),
+ horizontal_space(Length::Units(1200)),
+ text("End!"),
+ scroll_to_beginning_button(),
+ ]
+ .height(Length::Units(450))
+ .align_items(Alignment::Center)
+ .padding([0, 40, 0, 40])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .horizontal_scroll(
+ Properties::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width),
+ )
+ .style(theme::Scrollable::custom(ScrollbarCustomStyle))
+ .id(SCROLLABLE_ID.clone())
+ .on_scroll(Message::Scrolled),
+ Direction::Multi => scrollable(
+ //horizontal content
+ row![
+ column![
+ text("Let's do some scrolling!"),
+ vertical_space(Length::Units(2400))
+ ],
+ scroll_to_end_button(),
+ text("Horizontal - Beginning!"),
+ horizontal_space(Length::Units(1200)),
+ //vertical content
+ column![
+ text("Horizontal - Middle!"),
+ scroll_to_end_button(),
+ text("Vertical - Beginning!"),
+ vertical_space(Length::Units(1200)),
+ text("Vertical - Middle!"),
+ vertical_space(Length::Units(1200)),
+ text("Vertical - End!"),
+ scroll_to_beginning_button(),
+ vertical_space(Length::Units(40)),
+ ]
+ .align_items(Alignment::Fill)
+ .spacing(40),
+ horizontal_space(Length::Units(1200)),
+ text("Horizontal - End!"),
+ scroll_to_beginning_button(),
+ ]
+ .align_items(Alignment::Center)
+ .padding([0, 40, 0, 40])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .vertical_scroll(
+ Properties::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width),
+ )
+ .horizontal_scroll(
+ Properties::new()
+ .width(self.scrollbar_width)
+ .margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width),
+ )
+ .style(theme::Scrollable::Custom(Box::new(
+ ScrollbarCustomStyle,
+ )))
+ .id(SCROLLABLE_ID.clone())
+ .on_scroll(Message::Scrolled),
+ });
+
+ let progress_bars: Element<Message> = match self.scrollable_direction {
+ Direction::Vertical => {
+ progress_bar(0.0..=1.0, self.current_scroll_offset.y).into()
+ }
+ Direction::Horizontal => {
+ progress_bar(0.0..=1.0, self.current_scroll_offset.x)
+ .style(theme::ProgressBar::Custom(Box::new(
+ ProgressBarCustomStyle,
+ )))
.into()
- })
- .collect(),
- )
- .spacing(20)
- .width(Length::Fill)
- .height(Length::Fill);
+ }
+ Direction::Multi => column![
+ progress_bar(0.0..=1.0, self.current_scroll_offset.y),
+ progress_bar(0.0..=1.0, self.current_scroll_offset.x).style(
+ theme::ProgressBar::Custom(Box::new(
+ ProgressBarCustomStyle,
+ ))
+ )
+ ]
+ .spacing(10)
+ .into(),
+ };
- let content =
- column![choose_theme, horizontal_rule(20), scrollable_row]
- .spacing(20)
- .padding(20);
-
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ let content: Element<Message> =
+ column![scroll_controls, scrollable_content, progress_bars]
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .align_items(Alignment::Center)
+ .spacing(10)
+ .into();
+
+ Element::from(
+ container(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(40)
+ .center_x()
+ .center_y(),
+ )
}
- fn theme(&self) -> Theme {
- self.theme.clone()
+ fn theme(&self) -> Self::Theme {
+ Theme::Dark
}
}
-/// A version of a scrollable
-struct Variant {
- title: &'static str,
- scrollbar_width: Option<u16>,
- scrollbar_margin: Option<u16>,
- scroller_width: Option<u16>,
- latest_offset: f32,
-}
+struct ScrollbarCustomStyle;
-impl Variant {
- pub fn all() -> Vec<Self> {
- vec![
- Self {
- title: "Default Scrollbar",
- scrollbar_width: None,
- scrollbar_margin: None,
- scroller_width: None,
- latest_offset: 0.0,
- },
- Self {
- title: "Slimmed & Margin",
- scrollbar_width: Some(4),
- scrollbar_margin: Some(3),
- scroller_width: Some(4),
- latest_offset: 0.0,
- },
- Self {
- title: "Wide Scroller",
- scrollbar_width: Some(4),
- scrollbar_margin: None,
- scroller_width: Some(10),
- latest_offset: 0.0,
- },
- Self {
- title: "Narrow Scroller",
- scrollbar_width: Some(10),
- scrollbar_margin: None,
- scroller_width: Some(4),
- latest_offset: 0.0,
+impl scrollable::StyleSheet for ScrollbarCustomStyle {
+ type Style = Theme;
+
+ fn active(&self, style: &Self::Style) -> Scrollbar {
+ style.active(&theme::Scrollable::Default)
+ }
+
+ fn hovered(&self, style: &Self::Style) -> Scrollbar {
+ style.hovered(&theme::Scrollable::Default)
+ }
+
+ fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar {
+ Scrollbar {
+ background: style.active(&theme::Scrollable::default()).background,
+ border_radius: 0.0,
+ border_width: 0.0,
+ border_color: Default::default(),
+ scroller: Scroller {
+ color: Color::from_rgb8(250, 85, 134),
+ border_radius: 0.0,
+ border_width: 0.0,
+ border_color: Default::default(),
},
- ]
+ }
}
+}
+
+struct ProgressBarCustomStyle;
- pub fn id(i: usize) -> scrollable::Id {
- scrollable::Id::new(format!("scrollable-{}", i))
+impl progress_bar::StyleSheet for ProgressBarCustomStyle {
+ type Style = Theme;
+
+ fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance {
+ progress_bar::Appearance {
+ background: style.extended_palette().background.strong.color.into(),
+ bar: Color::from_rgb8(250, 85, 134).into(),
+ border_radius: 0.0,
+ }
}
}
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index ff2929da..ccd9c815 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -81,7 +81,10 @@ impl Application for WebSocket {
echo::Event::MessageReceived(message) => {
self.messages.push(message);
- scrollable::snap_to(MESSAGE_LOG.clone(), 1.0)
+ scrollable::snap_to(
+ MESSAGE_LOG.clone(),
+ scrollable::RelativeOffset::END,
+ )
}
},
Message::Server => Command::none(),
diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs
index f2ef132a..5ad4d858 100644
--- a/native/src/widget/column.rs
+++ b/native/src/widget/column.rs
@@ -10,8 +10,6 @@ use crate::{
Shell, Widget,
};
-use std::u32;
-
/// A container that distributes its contents vertically.
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Renderer> {
diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs
index 2210137d..3b20631f 100644
--- a/native/src/widget/operation/scrollable.rs
+++ b/native/src/widget/operation/scrollable.rs
@@ -3,25 +3,19 @@ use crate::widget::{Id, Operation};
/// The internal state of a widget that can be scrolled.
pub trait Scrollable {
- /// Snaps the scroll of the widget to the given `percentage`.
- fn snap_to(&mut self, percentage: f32);
+ /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
+ fn snap_to(&mut self, offset: RelativeOffset);
}
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
/// the provided `percentage`.
-pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> {
+pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
struct SnapTo {
target: Id,
- percentage: f32,
+ offset: RelativeOffset,
}
impl<T> Operation<T> for SnapTo {
- fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
- if Some(&self.target) == id {
- state.snap_to(self.percentage);
- }
- }
-
fn container(
&mut self,
_id: Option<&Id>,
@@ -29,7 +23,32 @@ pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> {
) {
operate_on_children(self)
}
+
+ fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+ if Some(&self.target) == id {
+ state.snap_to(self.offset);
+ }
+ }
}
- SnapTo { target, percentage }
+ SnapTo { target, offset }
+}
+
+/// The amount of offset in each direction of a [`Scrollable`].
+///
+/// A value of `0.0` means start, while `1.0` means end.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct RelativeOffset {
+ /// The amount of horizontal offset
+ pub x: f32,
+ /// The amount of vertical offset
+ pub y: f32,
+}
+
+impl RelativeOffset {
+ /// A relative offset that points to the top-left of a [`Scrollable`].
+ pub const START: Self = Self { x: 0.0, y: 0.0 };
+
+ /// A relative offset that points to the bottom-right of a [`Scrollable`].
+ pub const END: Self = Self { x: 1.0, y: 1.0 };
}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index 20780f89..82286036 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -1,5 +1,6 @@
//! Navigate an endless amount of content with a scrollbar.
use crate::event::{self, Event};
+use crate::keyboard;
use crate::layout;
use crate::mouse;
use crate::overlay;
@@ -13,9 +14,8 @@ use crate::{
Rectangle, Shell, Size, Vector, Widget,
};
-use std::{f32, u32};
-
pub use iced_style::scrollable::StyleSheet;
+pub use operation::scrollable::RelativeOffset;
pub mod style {
//! The styles of a [`Scrollable`].
@@ -34,11 +34,10 @@ where
{
id: Option<Id>,
height: Length,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
+ vertical: Properties,
+ horizontal: Option<Properties>,
content: Element<'a, Message, Renderer>,
- on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>,
+ on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -52,9 +51,8 @@ where
Scrollable {
id: None,
height: Length::Shrink,
- scrollbar_width: 10,
- scrollbar_margin: 0,
- scroller_width: 10,
+ vertical: Properties::default(),
+ horizontal: None,
content: content.into(),
on_scroll: None,
style: Default::default(),
@@ -73,32 +71,26 @@ where
self
}
- /// Sets the scrollbar width of the [`Scrollable`] .
- /// Silently enforces a minimum value of 1.
- pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self {
- self.scrollbar_width = scrollbar_width.max(1);
+ /// Configures the vertical scrollbar of the [`Scrollable`] .
+ pub fn vertical_scroll(mut self, properties: Properties) -> Self {
+ self.vertical = properties;
self
}
- /// Sets the scrollbar margin of the [`Scrollable`] .
- pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self {
- self.scrollbar_margin = scrollbar_margin;
- self
- }
-
- /// Sets the scroller width of the [`Scrollable`] .
- ///
- /// It silently enforces a minimum value of 1.
- pub fn scroller_width(mut self, scroller_width: u16) -> Self {
- self.scroller_width = scroller_width.max(1);
+ /// Configures the horizontal scrollbar of the [`Scrollable`] .
+ pub fn horizontal_scroll(mut self, properties: Properties) -> Self {
+ self.horizontal = Some(properties);
self
}
/// Sets a function to call when the [`Scrollable`] is scrolled.
///
- /// The function takes the new relative offset of the [`Scrollable`]
- /// (e.g. `0` means top, while `1` means bottom).
- pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self {
+ /// The function takes the new relative x & y offset of the [`Scrollable`]
+ /// (e.g. `0` means beginning, while `1` means end).
+ pub fn on_scroll(
+ mut self,
+ f: impl Fn(RelativeOffset) -> Message + 'a,
+ ) -> Self {
self.on_scroll = Some(Box::new(f));
self
}
@@ -113,6 +105,51 @@ where
}
}
+/// Properties of a scrollbar within a [`Scrollable`].
+#[derive(Debug)]
+pub struct Properties {
+ width: u16,
+ margin: u16,
+ scroller_width: u16,
+}
+
+impl Default for Properties {
+ fn default() -> Self {
+ Self {
+ width: 10,
+ margin: 0,
+ scroller_width: 10,
+ }
+ }
+}
+
+impl Properties {
+ /// Creates new [`Properties`] for use in a [`Scrollable`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Silently enforces a minimum width of 1.
+ pub fn width(mut self, width: u16) -> Self {
+ self.width = width.max(1);
+ self
+ }
+
+ /// Sets the scrollbar margin of the [`Scrollable`] .
+ pub fn margin(mut self, margin: u16) -> Self {
+ self.margin = margin;
+ self
+ }
+
+ /// Sets the scroller width of the [`Scrollable`] .
+ /// Silently enforces a minimum width of 1.
+ pub fn scroller_width(mut self, scroller_width: u16) -> Self {
+ self.scroller_width = scroller_width.max(1);
+ self
+ }
+}
+
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
@@ -153,7 +190,7 @@ where
limits,
Widget::<Message, Renderer>::width(self),
self.height,
- u32::MAX,
+ self.horizontal.is_some(),
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
@@ -198,9 +235,8 @@ where
cursor_position,
clipboard,
shell,
- self.scrollbar_width,
- self.scrollbar_margin,
- self.scroller_width,
+ &self.vertical,
+ self.horizontal.as_ref(),
&self.on_scroll,
|event, layout, cursor_position, clipboard, shell| {
self.content.as_widget_mut().on_event(
@@ -232,9 +268,8 @@ where
theme,
layout,
cursor_position,
- self.scrollbar_width,
- self.scrollbar_margin,
- self.scroller_width,
+ &self.vertical,
+ self.horizontal.as_ref(),
&self.style,
|renderer, layout, cursor_position, viewport| {
self.content.as_widget().draw(
@@ -262,9 +297,8 @@ where
tree.state.downcast_ref::<State>(),
layout,
cursor_position,
- self.scrollbar_width,
- self.scrollbar_margin,
- self.scroller_width,
+ &self.vertical,
+ self.horizontal.as_ref(),
|layout, cursor_position, viewport| {
self.content.as_widget().mouse_interaction(
&tree.children[0],
@@ -299,7 +333,7 @@ where
.downcast_ref::<State>()
.offset(bounds, content_bounds);
- overlay.translate(Vector::new(0.0, -(offset as f32)))
+ overlay.translate(Vector::new(-offset.x, -offset.y))
})
}
}
@@ -343,9 +377,12 @@ impl From<Id> for widget::Id {
}
/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`]
-/// to the provided `percentage`.
-pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> {
- Command::widget(operation::scrollable::snap_to(id.0, percentage))
+/// to the provided `percentage` along the x & y axis.
+pub fn snap_to<Message: 'static>(
+ id: Id,
+ offset: RelativeOffset,
+) -> Command<Message> {
+ Command::widget(operation::scrollable::snap_to(id.0, offset))
}
/// Computes the layout of a [`Scrollable`].
@@ -354,14 +391,29 @@ pub fn layout<Renderer>(
limits: &layout::Limits,
width: Length,
height: Length,
- max_height: u32,
+ horizontal_enabled: bool,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits.max_height(max_height).width(width).height(height);
+ let limits = limits
+ .max_height(u32::MAX)
+ .max_width(if horizontal_enabled {
+ u32::MAX
+ } else {
+ limits.max().width as u32
+ })
+ .width(width)
+ .height(height);
let child_limits = layout::Limits::new(
Size::new(limits.min().width, 0.0),
- Size::new(limits.max().width, f32::INFINITY),
+ Size::new(
+ if horizontal_enabled {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ f32::MAX,
+ ),
);
let content = layout_content(renderer, &child_limits);
@@ -379,10 +431,9 @@ pub fn update<Message>(
cursor_position: Point,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
- on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>,
+ vertical: &Properties,
+ horizontal: Option<&Properties>,
+ on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
update_content: impl FnOnce(
Event,
Layout<'_>,
@@ -392,36 +443,28 @@ pub fn update<Message>(
) -> event::Status,
) -> event::Status {
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let mouse_over_scrollable = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
- let scrollbar = scrollbar(
- state,
- scrollbar_width,
- scrollbar_margin,
- scroller_width,
- bounds,
- content_bounds,
- );
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
+ let scrollbars =
+ Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor_position);
let event_status = {
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(
- cursor_position.x,
- cursor_position.y + state.offset(bounds, content_bounds) as f32,
- )
+ let cursor_position = if mouse_over_scrollable
+ && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
+ {
+ cursor_position + state.offset(bounds, content_bounds)
} 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)
+ Point::new(-1.0, -1.0)
};
update_content(
@@ -437,18 +480,31 @@ pub fn update<Message>(
return event::Status::Captured;
}
- if is_mouse_over {
+ if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event
+ {
+ state.keyboard_modifiers = modifiers;
+
+ return event::Status::Ignored;
+ }
+
+ if mouse_over_scrollable {
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- match delta {
- mouse::ScrollDelta::Lines { y, .. } => {
- // TODO: Configurable speed (?)
- state.scroll(y * 60.0, bounds, content_bounds);
+ let delta = match delta {
+ mouse::ScrollDelta::Lines { x, y } => {
+ // TODO: Configurable speed/friction (?)
+ let movement = if state.keyboard_modifiers.shift() {
+ Vector::new(y, x)
+ } else {
+ Vector::new(x, y)
+ };
+
+ movement * 60.0
}
- mouse::ScrollDelta::Pixels { y, .. } => {
- state.scroll(y, bounds, content_bounds);
- }
- }
+ mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
+ };
+
+ state.scroll(delta, bounds, content_bounds);
notify_on_scroll(
state,
@@ -460,21 +516,27 @@ pub fn update<Message>(
return event::Status::Captured;
}
- Event::Touch(event) => {
+ Event::Touch(event)
+ if state.scroll_area_touched_at.is_some()
+ || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
+ {
match event {
touch::Event::FingerPressed { .. } => {
- state.scroll_box_touched_at = Some(cursor_position);
+ state.scroll_area_touched_at = Some(cursor_position);
}
touch::Event::FingerMoved { .. } => {
if let Some(scroll_box_touched_at) =
- state.scroll_box_touched_at
+ state.scroll_area_touched_at
{
- let delta =
- cursor_position.y - scroll_box_touched_at.y;
+ let delta = Vector::new(
+ cursor_position.x - scroll_box_touched_at.x,
+ cursor_position.y - scroll_box_touched_at.y,
+ );
state.scroll(delta, bounds, content_bounds);
- state.scroll_box_touched_at = Some(cursor_position);
+ state.scroll_area_touched_at =
+ Some(cursor_position);
notify_on_scroll(
state,
@@ -487,7 +549,7 @@ pub fn update<Message>(
}
touch::Event::FingerLifted { .. }
| touch::Event::FingerLost { .. } => {
- state.scroll_box_touched_at = None;
+ state.scroll_area_touched_at = None;
}
}
@@ -497,22 +559,20 @@ pub fn update<Message>(
}
}
- if state.is_scroller_grabbed() {
+ if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
- state.scroller_grabbed_at = None;
+ state.y_scroller_grabbed_at = None;
return event::Status::Captured;
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let (Some(scrollbar), Some(scroller_grabbed_at)) =
- (scrollbar, state.scroller_grabbed_at)
- {
- state.scroll_to(
- scrollbar.scroll_percentage(
+ if let Some(scrollbar) = scrollbars.y {
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
scroller_grabbed_at,
cursor_position,
),
@@ -533,35 +593,100 @@ pub fn update<Message>(
}
_ => {}
}
- } else if is_mouse_over_scrollbar {
+ } else if mouse_over_y_scrollbar {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if let Some(scrollbar) = scrollbar {
- if let Some(scroller_grabbed_at) =
- scrollbar.grab_scroller(cursor_position)
- {
- state.scroll_to(
- scrollbar.scroll_percentage(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- return event::Status::Captured;
- }
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) =
+ (scrollbars.grab_y_scroller(cursor_position), scrollbars.y)
+ {
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ state.x_scroller_grabbed_at = None;
+
+ return event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let Some(scrollbar) = scrollbars.x {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ } else if mouse_over_x_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ if let (Some(scroller_grabbed_at), Some(scrollbar)) =
+ (scrollbars.grab_x_scroller(cursor_position), scrollbars.x)
+ {
+ state.scroll_x_to(
+ scrollbar.scroll_percentage_x(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ return event::Status::Captured;
}
}
_ => {}
@@ -576,9 +701,8 @@ pub fn mouse_interaction(
state: &State,
layout: Layout<'_>,
cursor_position: Point,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
+ vertical: &Properties,
+ horizontal: Option<&Properties>,
content_interaction: impl FnOnce(
Layout<'_>,
Point,
@@ -586,39 +710,38 @@ pub fn mouse_interaction(
) -> mouse::Interaction,
) -> mouse::Interaction {
let bounds = layout.bounds();
+ let mouse_over_scrollable = bounds.contains(cursor_position);
+
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let scrollbar = scrollbar(
- state,
- scrollbar_width,
- scrollbar_margin,
- scroller_width,
- bounds,
- content_bounds,
- );
- let is_mouse_over = bounds.contains(cursor_position);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
+ let scrollbars =
+ Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor_position);
- if is_mouse_over_scrollbar || state.is_scroller_grabbed() {
+ if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
+ || state.scrollers_grabbed()
+ {
mouse::Interaction::Idle
} else {
let offset = state.offset(bounds, content_bounds);
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(cursor_position.x, cursor_position.y + offset as f32)
+ let cursor_position = if mouse_over_scrollable
+ && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
+ {
+ cursor_position + offset
} else {
- Point::new(cursor_position.x, -1.0)
+ Point::new(-1.0, -1.0)
};
content_interaction(
content_layout,
cursor_position,
&Rectangle {
- y: bounds.y + offset as f32,
+ y: bounds.y + offset.y,
+ x: bounds.x + offset.x,
..bounds
},
)
@@ -632,9 +755,8 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: Point,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
+ vertical: &Properties,
+ horizontal: Option<&Properties>,
style: &<Renderer::Theme as StyleSheet>::Style,
draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
) where
@@ -644,39 +766,37 @@ pub fn draw<Renderer>(
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let offset = state.offset(bounds, content_bounds);
- let scrollbar = scrollbar(
- state,
- scrollbar_width,
- scrollbar_margin,
- scroller_width,
- bounds,
- content_bounds,
- );
- let is_mouse_over = bounds.contains(cursor_position);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
+ let scrollbars =
+ Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+
+ let mouse_over_scrollable = bounds.contains(cursor_position);
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor_position);
+
+ let offset = state.offset(bounds, content_bounds);
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(cursor_position.x, cursor_position.y + offset as f32)
+ let cursor_position = if mouse_over_scrollable
+ && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar)
+ {
+ cursor_position + offset
} else {
- Point::new(cursor_position.x, -1.0)
+ Point::new(-1.0, -1.0)
};
- if let Some(scrollbar) = scrollbar {
+ // Draw inner content
+ if scrollbars.active() {
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(
- Vector::new(0.0, -(offset as f32)),
+ Vector::new(-offset.x, -offset.y),
|renderer| {
draw_content(
renderer,
content_layout,
cursor_position,
&Rectangle {
- y: bounds.y + offset as f32,
+ y: bounds.y + offset.y,
+ x: bounds.x + offset.x,
..bounds
},
);
@@ -684,25 +804,15 @@ pub fn draw<Renderer>(
);
});
- let style = if state.is_scroller_grabbed() {
- theme.dragging(style)
- } else if is_mouse_over_scrollbar {
- theme.hovered(style)
- } else {
- theme.active(style)
- };
-
- let is_scrollbar_visible =
- style.background.is_some() || style.border_width > 0.0;
-
- renderer.with_layer(
- Rectangle {
- width: bounds.width + 2.0,
- height: bounds.height + 2.0,
- ..bounds
- },
- |renderer| {
- if is_scrollbar_visible {
+ let draw_scrollbar =
+ |renderer: &mut Renderer,
+ style: style::Scrollbar,
+ scrollbar: &Scrollbar| {
+ //track
+ if style.background.is_some()
+ || (style.border_color != Color::TRANSPARENT
+ && style.border_width > 0.0)
+ {
renderer.fill_quad(
renderer::Quad {
bounds: scrollbar.bounds,
@@ -716,8 +826,10 @@ pub fn draw<Renderer>(
);
}
- if (is_mouse_over || state.is_scroller_grabbed())
- && is_scrollbar_visible
+ //thumb
+ if style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border_color != Color::TRANSPARENT
+ && style.scroller.border_width > 0.0)
{
renderer.fill_quad(
renderer::Quad {
@@ -729,6 +841,40 @@ pub fn draw<Renderer>(
style.scroller.color,
);
}
+ };
+
+ renderer.with_layer(
+ Rectangle {
+ width: bounds.width + 2.0,
+ height: bounds.height + 2.0,
+ ..bounds
+ },
+ |renderer| {
+ //draw y scrollbar
+ if let Some(scrollbar) = scrollbars.y {
+ let style = if state.y_scroller_grabbed_at.is_some() {
+ theme.dragging(style)
+ } else if mouse_over_y_scrollbar {
+ theme.hovered(style)
+ } else {
+ theme.active(style)
+ };
+
+ draw_scrollbar(renderer, style, &scrollbar);
+ }
+
+ //draw x scrollbar
+ if let Some(scrollbar) = scrollbars.x {
+ let style = if state.x_scroller_grabbed_at.is_some() {
+ theme.dragging_horizontal(style)
+ } else if mouse_over_x_scrollbar {
+ theme.hovered_horizontal(style)
+ } else {
+ theme.active_horizontal(style)
+ };
+
+ draw_scrollbar(renderer, style, &scrollbar);
+ }
},
);
} else {
@@ -737,110 +883,70 @@ pub fn draw<Renderer>(
content_layout,
cursor_position,
&Rectangle {
- y: bounds.y + offset as f32,
+ x: bounds.x + offset.x,
+ y: bounds.y + offset.y,
..bounds
},
);
}
}
-fn scrollbar(
- state: &State,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
- bounds: Rectangle,
- content_bounds: Rectangle,
-) -> Option<Scrollbar> {
- let offset = state.offset(bounds, content_bounds);
-
- if content_bounds.height > bounds.height {
- let outer_width =
- scrollbar_width.max(scroller_width) + 2 * scrollbar_margin;
-
- let outer_bounds = Rectangle {
- x: bounds.x + bounds.width - outer_width as f32,
- y: bounds.y,
- width: outer_width as f32,
- height: bounds.height,
- };
-
- let scrollbar_bounds = Rectangle {
- x: bounds.x + bounds.width
- - f32::from(outer_width / 2 + scrollbar_width / 2),
- y: bounds.y,
- width: scrollbar_width as f32,
- height: bounds.height,
- };
-
- let ratio = bounds.height / content_bounds.height;
- let scroller_height = bounds.height * ratio;
- let y_offset = offset as f32 * ratio;
-
- let scroller_bounds = Rectangle {
- x: bounds.x + bounds.width
- - f32::from(outer_width / 2 + scroller_width / 2),
- y: scrollbar_bounds.y + y_offset,
- width: scroller_width as f32,
- height: scroller_height,
- };
-
- Some(Scrollbar {
- outer_bounds,
- bounds: scrollbar_bounds,
- scroller: Scroller {
- bounds: scroller_bounds,
- },
- })
- } else {
- None
- }
-}
-
fn notify_on_scroll<Message>(
state: &State,
- on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>,
+ on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
bounds: Rectangle,
content_bounds: Rectangle,
shell: &mut Shell<'_, Message>,
) {
- if content_bounds.height <= bounds.height {
- return;
- }
-
if let Some(on_scroll) = on_scroll {
- shell.publish(on_scroll(
- state.offset.absolute(bounds, content_bounds)
- / (content_bounds.height - bounds.height),
- ));
+ if content_bounds.width <= bounds.width
+ && content_bounds.height <= bounds.height
+ {
+ return;
+ }
+
+ let x = state.offset_x.absolute(bounds.width, content_bounds.width)
+ / (content_bounds.width - bounds.width);
+
+ let y = state
+ .offset_y
+ .absolute(bounds.height, content_bounds.height)
+ / (content_bounds.height - bounds.height);
+
+ shell.publish(on_scroll(RelativeOffset { x, y }))
}
}
/// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy)]
pub struct State {
- scroller_grabbed_at: Option<f32>,
- scroll_box_touched_at: Option<Point>,
- offset: Offset,
+ scroll_area_touched_at: Option<Point>,
+ offset_y: Offset,
+ y_scroller_grabbed_at: Option<f32>,
+ offset_x: Offset,
+ x_scroller_grabbed_at: Option<f32>,
+ keyboard_modifiers: keyboard::Modifiers,
}
impl Default for State {
fn default() -> Self {
Self {
- scroller_grabbed_at: None,
- scroll_box_touched_at: None,
- offset: Offset::Absolute(0.0),
+ scroll_area_touched_at: None,
+ offset_y: Offset::Absolute(0.0),
+ y_scroller_grabbed_at: None,
+ offset_x: Offset::Absolute(0.0),
+ x_scroller_grabbed_at: None,
+ keyboard_modifiers: keyboard::Modifiers::default(),
}
}
}
impl operation::Scrollable for State {
- fn snap_to(&mut self, percentage: f32) {
- State::snap_to(self, percentage);
+ fn snap_to(&mut self, offset: RelativeOffset) {
+ State::snap_to(self, offset);
}
}
-/// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy)]
enum Offset {
Absolute(f32),
@@ -848,23 +954,20 @@ enum Offset {
}
impl Offset {
- fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 {
+ fn absolute(self, window: f32, content: f32) -> f32 {
match self {
- Self::Absolute(absolute) => {
- let hidden_content =
- (content_bounds.height - bounds.height).max(0.0);
-
- absolute.min(hidden_content)
+ Offset::Absolute(absolute) => {
+ absolute.min((content - window).max(0.0))
}
- Self::Relative(percentage) => {
- ((content_bounds.height - bounds.height) * percentage).max(0.0)
+ Offset::Relative(percentage) => {
+ ((content - window) * percentage).max(0.0)
}
}
}
}
impl State {
- /// Creates a new [`State`] with the scrollbar located at the top.
+ /// Creates a new [`State`] with the scrollbar(s) at the beginning.
pub fn new() -> Self {
State::default()
}
@@ -873,107 +976,341 @@ impl State {
/// the [`Scrollable`] and its contents.
pub fn scroll(
&mut self,
- delta_y: f32,
+ delta: Vector<f32>,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- if bounds.height >= content_bounds.height {
- return;
+ if bounds.height < content_bounds.height {
+ self.offset_y = Offset::Absolute(
+ (self.offset_y.absolute(bounds.height, content_bounds.height)
+ - delta.y)
+ .clamp(0.0, content_bounds.height - bounds.height),
+ )
}
- self.offset = Offset::Absolute(
- (self.offset.absolute(bounds, content_bounds) - delta_y)
- .clamp(0.0, content_bounds.height - bounds.height),
- );
+ if bounds.width < content_bounds.width {
+ self.offset_x = Offset::Absolute(
+ (self.offset_x.absolute(bounds.width, content_bounds.width)
+ - delta.x)
+ .clamp(0.0, content_bounds.width - bounds.width),
+ );
+ }
}
- /// Scrolls the [`Scrollable`] to a relative amount.
+ /// Scrolls the [`Scrollable`] to a relative amount along the y axis.
///
- /// `0` represents scrollbar at the top, while `1` represents scrollbar at
- /// the bottom.
- pub fn scroll_to(
+ /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
+ /// the end.
+ pub fn scroll_y_to(
&mut self,
percentage: f32,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- self.snap_to(percentage);
+ self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0));
self.unsnap(bounds, content_bounds);
}
- /// Snaps the scroll position to a relative amount.
+ /// Scrolls the [`Scrollable`] to a relative amount along the x axis.
///
- /// `0` represents scrollbar at the top, while `1` represents scrollbar at
- /// the bottom.
- pub fn snap_to(&mut self, percentage: f32) {
- self.offset = Offset::Relative(percentage.clamp(0.0, 1.0));
+ /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
+ /// the end.
+ pub fn scroll_x_to(
+ &mut self,
+ percentage: f32,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0));
+ self.unsnap(bounds, content_bounds);
+ }
+
+ /// Snaps the scroll position to a [`RelativeOffset`].
+ pub fn snap_to(&mut self, offset: RelativeOffset) {
+ self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0));
+ self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
}
/// Unsnaps the current scroll position, if snapped, given the bounds of the
/// [`Scrollable`] and its contents.
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
- self.offset =
- Offset::Absolute(self.offset.absolute(bounds, content_bounds));
+ self.offset_x = Offset::Absolute(
+ self.offset_x.absolute(bounds.width, content_bounds.width),
+ );
+ self.offset_y = Offset::Absolute(
+ self.offset_y.absolute(bounds.height, content_bounds.height),
+ );
}
- /// Returns the current scrolling offset of the [`State`], given the bounds
- /// of the [`Scrollable`] and its contents.
- pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
- self.offset.absolute(bounds, content_bounds) as u32
+ /// Returns the scrolling offset of the [`State`], given the bounds of the
+ /// [`Scrollable`] and its contents.
+ pub fn offset(
+ &self,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) -> Vector {
+ Vector::new(
+ self.offset_x.absolute(bounds.width, content_bounds.width),
+ self.offset_y.absolute(bounds.height, content_bounds.height),
+ )
}
- /// Returns whether the scroller is currently grabbed or not.
- pub fn is_scroller_grabbed(&self) -> bool {
- self.scroller_grabbed_at.is_some()
+ /// Returns whether any scroller is currently grabbed or not.
+ pub fn scrollers_grabbed(&self) -> bool {
+ self.x_scroller_grabbed_at.is_some()
+ || self.y_scroller_grabbed_at.is_some()
}
+}
+
+#[derive(Debug)]
+/// State of both [`Scrollbar`]s.
+struct Scrollbars {
+ y: Option<Scrollbar>,
+ x: Option<Scrollbar>,
+}
- /// Returns whether the scroll box is currently touched or not.
- pub fn is_scroll_box_touched(&self) -> bool {
- self.scroll_box_touched_at.is_some()
+impl Scrollbars {
+ /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
+ fn new(
+ state: &State,
+ vertical: &Properties,
+ horizontal: Option<&Properties>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) -> Self {
+ let offset = state.offset(bounds, content_bounds);
+
+ let show_scrollbar_x = horizontal.and_then(|h| {
+ if content_bounds.width > bounds.width {
+ Some(h)
+ } else {
+ None
+ }
+ });
+
+ let y_scrollbar = if content_bounds.height > bounds.height {
+ let Properties {
+ width,
+ margin,
+ scroller_width,
+ } = *vertical;
+
+ // Adjust the height of the vertical scrollbar if the horizontal scrollbar
+ // is present
+ let x_scrollbar_height = show_scrollbar_x.map_or(0.0, |h| {
+ (h.width.max(h.scroller_width) + h.margin) as f32
+ });
+
+ let total_scrollbar_width = width.max(scroller_width) + 2 * margin;
+
+ // Total bounds of the scrollbar + margin + scroller width
+ let total_scrollbar_bounds = Rectangle {
+ x: bounds.x + bounds.width - total_scrollbar_width as f32,
+ y: bounds.y,
+ width: total_scrollbar_width as f32,
+ height: (bounds.height - x_scrollbar_height).max(0.0),
+ };
+
+ // Bounds of just the scrollbar
+ let scrollbar_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - f32::from(total_scrollbar_width / 2 + width / 2),
+ y: bounds.y,
+ width: width as f32,
+ height: (bounds.height - x_scrollbar_height).max(0.0),
+ };
+
+ let ratio = bounds.height / content_bounds.height;
+ // min height for easier grabbing with super tall content
+ let scroller_height = (bounds.height * ratio).max(2.0);
+ let scroller_offset = offset.y * ratio;
+
+ let scroller_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - f32::from(total_scrollbar_width / 2 + scroller_width / 2),
+ y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height)
+ .max(0.0),
+ width: scroller_width as f32,
+ height: scroller_height,
+ };
+
+ Some(Scrollbar {
+ total_bounds: total_scrollbar_bounds,
+ bounds: scrollbar_bounds,
+ scroller: Scroller {
+ bounds: scroller_bounds,
+ },
+ })
+ } else {
+ None
+ };
+
+ let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
+ let Properties {
+ width,
+ margin,
+ scroller_width,
+ } = *horizontal;
+
+ // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
+ // is present
+ let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| {
+ (vertical.width.max(vertical.scroller_width) + vertical.margin)
+ as f32
+ });
+
+ let total_scrollbar_height = width.max(scroller_width) + 2 * margin;
+
+ // Total bounds of the scrollbar + margin + scroller width
+ let total_scrollbar_bounds = Rectangle {
+ x: bounds.x,
+ y: bounds.y + bounds.height - total_scrollbar_height as f32,
+ width: (bounds.width - scrollbar_y_width).max(0.0),
+ height: total_scrollbar_height as f32,
+ };
+
+ // Bounds of just the scrollbar
+ let scrollbar_bounds = Rectangle {
+ x: bounds.x,
+ y: bounds.y + bounds.height
+ - f32::from(total_scrollbar_height / 2 + width / 2),
+ width: (bounds.width - scrollbar_y_width).max(0.0),
+ height: width as f32,
+ };
+
+ let ratio = bounds.width / content_bounds.width;
+ // min width for easier grabbing with extra wide content
+ let scroller_length = (bounds.width * ratio).max(2.0);
+ let scroller_offset = offset.x * ratio;
+
+ let scroller_bounds = Rectangle {
+ x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width)
+ .max(0.0),
+ y: bounds.y + bounds.height
+ - f32::from(
+ total_scrollbar_height / 2 + scroller_width / 2,
+ ),
+ width: scroller_length,
+ height: scroller_width as f32,
+ };
+
+ Some(Scrollbar {
+ total_bounds: total_scrollbar_bounds,
+ bounds: scrollbar_bounds,
+ scroller: Scroller {
+ bounds: scroller_bounds,
+ },
+ })
+ } else {
+ None
+ };
+
+ Self {
+ y: y_scrollbar,
+ x: x_scrollbar,
+ }
+ }
+
+ fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) {
+ (
+ self.y
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false),
+ self.x
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false),
+ )
+ }
+
+ fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
+ self.y.and_then(|scrollbar| {
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scrollbar.scroller.bounds.contains(cursor_position) {
+ (cursor_position.y - scrollbar.scroller.bounds.y)
+ / scrollbar.scroller.bounds.height
+ } else {
+ 0.5
+ })
+ } else {
+ None
+ }
+ })
+ }
+
+ fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
+ self.x.and_then(|scrollbar| {
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scrollbar.scroller.bounds.contains(cursor_position) {
+ (cursor_position.x - scrollbar.scroller.bounds.x)
+ / scrollbar.scroller.bounds.width
+ } else {
+ 0.5
+ })
+ } else {
+ None
+ }
+ })
+ }
+
+ fn active(&self) -> bool {
+ self.y.is_some() || self.x.is_some()
}
}
/// The scrollbar of a [`Scrollable`].
-#[derive(Debug)]
+#[derive(Debug, Copy, Clone)]
struct Scrollbar {
- /// The outer bounds of the scrollable, including the [`Scrollbar`] and
- /// [`Scroller`].
- outer_bounds: Rectangle,
+ /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller,
+ /// and the scrollbar margin.
+ total_bounds: Rectangle,
- /// The bounds of the [`Scrollbar`].
+ /// The bounds of just the [`Scrollbar`].
bounds: Rectangle,
- /// The bounds of the [`Scroller`].
+ /// The state of this scrollbar's [`Scroller`].
scroller: Scroller,
}
impl Scrollbar {
+ /// Returns whether the mouse is over the scrollbar or not.
fn is_mouse_over(&self, cursor_position: Point) -> bool {
- self.outer_bounds.contains(cursor_position)
+ self.total_bounds.contains(cursor_position)
}
- fn grab_scroller(&self, cursor_position: Point) -> Option<f32> {
- if self.outer_bounds.contains(cursor_position) {
- Some(if self.scroller.bounds.contains(cursor_position) {
- (cursor_position.y - self.scroller.bounds.y)
- / self.scroller.bounds.height
- } else {
- 0.5
- })
+ /// Returns the y-axis scrolled percentage from the cursor position.
+ fn scroll_percentage_y(
+ &self,
+ grabbed_at: f32,
+ cursor_position: Point,
+ ) -> f32 {
+ if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
+ // cursor position is unavailable! Set to either end or beginning of scrollbar depending
+ // on where the thumb currently is in the track
+ (self.scroller.bounds.y / self.total_bounds.height).round()
} else {
- None
+ (cursor_position.y
+ - self.bounds.y
+ - self.scroller.bounds.height * grabbed_at)
+ / (self.bounds.height - self.scroller.bounds.height)
}
}
- fn scroll_percentage(
+ /// Returns the x-axis scrolled percentage from the cursor position.
+ fn scroll_percentage_x(
&self,
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- (cursor_position.y
- - self.bounds.y
- - self.scroller.bounds.height * grabbed_at)
- / (self.bounds.height - self.scroller.bounds.height)
+ if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
+ (self.scroller.bounds.x / self.total_bounds.width).round()
+ } else {
+ (cursor_position.x
+ - self.bounds.x
+ - self.scroller.bounds.width * grabbed_at)
+ / (self.bounds.width - self.scroller.bounds.width)
+ }
}
}
diff --git a/src/widget.rs b/src/widget.rs
index d2d4a1b8..f71bf7ff 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -99,7 +99,8 @@ pub mod radio {
pub mod scrollable {
//! Navigate an endless amount of content with a scrollbar.
pub use iced_native::widget::scrollable::{
- snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet,
+ snap_to, style::Scrollbar, style::Scroller, Id, Properties,
+ RelativeOffset, StyleSheet,
};
/// A widget that can vertically display an infinite amount of content
diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs
index c6d7d537..64ed8462 100644
--- a/style/src/scrollable.rs
+++ b/style/src/scrollable.rs
@@ -37,11 +37,26 @@ pub trait StyleSheet {
/// Produces the style of an active scrollbar.
fn active(&self, style: &Self::Style) -> Scrollbar;
- /// Produces the style of an hovered scrollbar.
+ /// Produces the style of a hovered scrollbar.
fn hovered(&self, style: &Self::Style) -> Scrollbar;
/// Produces the style of a scrollbar that is being dragged.
fn dragging(&self, style: &Self::Style) -> Scrollbar {
self.hovered(style)
}
+
+ /// Produces the style of an active horizontal scrollbar.
+ fn active_horizontal(&self, style: &Self::Style) -> Scrollbar {
+ self.active(style)
+ }
+
+ /// Produces the style of a hovered horizontal scrollbar.
+ fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar {
+ self.hovered(style)
+ }
+
+ /// Produces the style of a horizontal scrollbar that is being dragged.
+ fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar {
+ self.hovered_horizontal(style)
+ }
}
diff --git a/style/src/theme.rs b/style/src/theme.rs
index a766b279..55bfa4ca 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -872,6 +872,15 @@ pub enum Scrollable {
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 {
type Style = Scrollable;
@@ -925,6 +934,30 @@ impl scrollable::StyleSheet for Theme {
Scrollable::Custom(custom) => custom.dragging(self),
}
}
+
+ fn active_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar {
+ match style {
+ Scrollable::Default => self.active(style),
+ Scrollable::Custom(custom) => custom.active_horizontal(self),
+ }
+ }
+
+ fn hovered_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar {
+ match style {
+ Scrollable::Default => self.hovered(style),
+ Scrollable::Custom(custom) => custom.hovered_horizontal(self),
+ }
+ }
+
+ fn dragging_horizontal(
+ &self,
+ style: &Self::Style,
+ ) -> scrollable::Scrollbar {
+ match style {
+ Scrollable::Default => self.hovered_horizontal(style),
+ Scrollable::Custom(custom) => custom.dragging_horizontal(self),
+ }
+ }
}
/// The style of text.