summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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.rs506
-rw-r--r--examples/websocket/src/main.rs7
-rw-r--r--native/src/widget/column.rs2
-rw-r--r--native/src/widget/operation/scrollable.rs19
-rw-r--r--native/src/widget/scrollable.rs1116
-rw-r--r--src/widget.rs3
-rw-r--r--style/src/scrollable.rs17
-rw-r--r--style/src/theme.rs24
12 files changed, 1135 insertions, 563 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..4bef4281 100644
--- a/examples/scrollable/Cargo.toml
+++ b/examples/scrollable/Cargo.toml
@@ -7,3 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
+lazy_static = "1.4"
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..1481afcc 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -1,44 +1,60 @@
-use iced::executor;
+use iced::widget::scrollable::{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, Vector};
use iced::{Application, Command, Element, Length, Settings, Theme};
+use lazy_static::lazy_static;
+
+lazy_static! {
+ static ref SCROLLABLE_ID: scrollable::Id = 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: Vector<f32>,
}
-#[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(scrollable::Direction),
+ ScrollToEnd(scrollable::Direction),
+ Scrolled(Vector<f32>),
}
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: Vector::new(0.0, 0.0),
},
Command::none(),
)
@@ -50,209 +66,333 @@ 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 = Vector::new(0.0, 0.0);
+ self.scrollable_direction = direction;
Command::none()
}
- Message::ScrollToTop(i) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = 0.0;
+ Message::ScrollbarWidthChanged(width) => {
+ self.scrollbar_width = width;
- scrollable::snap_to(Variant::id(i), 0.0)
- } else {
- Command::none()
- }
+ Command::none()
+ }
+ Message::ScrollbarMarginChanged(margin) => {
+ self.scrollbar_margin = margin;
+
+ Command::none()
}
- Message::ScrollToBottom(i) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = 1.0;
+ Message::ScrollerWidthChanged(width) => {
+ self.scroller_width = width;
- scrollable::snap_to(Variant::id(i), 1.0)
- } else {
- Command::none()
+ Command::none()
+ }
+ Message::ScrollToBeginning(direction) => {
+ match direction {
+ scrollable::Direction::Horizontal => {
+ self.current_scroll_offset.x = 0.0;
+ }
+ scrollable::Direction::Vertical => {
+ self.current_scroll_offset.y = 0.0;
+ }
}
+
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ Vector::new(
+ self.current_scroll_offset.x,
+ self.current_scroll_offset.y,
+ ),
+ )
}
- Message::Scrolled(i, offset) => {
- if let Some(variant) = self.variants.get_mut(i) {
- variant.latest_offset = offset;
+ Message::ScrollToEnd(direction) => {
+ match direction {
+ scrollable::Direction::Horizontal => {
+ self.current_scroll_offset.x = 1.0;
+ }
+ scrollable::Direction::Vertical => {
+ self.current_scroll_offset.y = 1.0;
+ }
}
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ Vector::new(
+ self.current_scroll_offset.x,
+ self.current_scroll_offset.y,
+ ),
+ )
+ }
+ Message::Scrolled(offset) => {
+ self.current_scroll_offset = offset;
+
Command::none()
}
}
}
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
- )));
- }
+ let scroll_slider_controls = column![
+ text("Scrollbar width:"),
+ scrollbar_width_slider,
+ text("Scrollbar margin:"),
+ scrollbar_margin_slider,
+ text("Scroller width:"),
+ scroller_width_slider,
+ ]
+ .width(Length::Fill);
- if let Some(scroller_width) = variant.scroller_width {
- contents = contents.push(text(format!(
- "scroller_width: {:?}",
- scroller_width
- )));
- }
+ 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(),
+ ])
+ .width(Length::Fill);
- 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);
- }
+ let scroll_controls =
+ row![scroll_slider_controls, scroll_orientation_controls]
+ .spacing(20)
+ .width(Length::Fill);
- if let Some(scrollbar_margin) = variant.scrollbar_margin {
- scrollable =
- scrollable.scrollbar_margin(scrollbar_margin);
- }
+ let scroll_to_end_button = |direction: scrollable::Direction| {
+ button("Scroll to end")
+ .padding(10)
+ .width(Length::Units(120))
+ .on_press(Message::ScrollToEnd(direction))
+ };
- if let Some(scroller_width) = variant.scroller_width {
- scrollable = scrollable.scroller_width(scroller_width);
- }
+ let scroll_to_beginning_button = |direction: scrollable::Direction| {
+ button("Scroll to beginning")
+ .padding(10)
+ .width(Length::Units(120))
+ .on_press(Message::ScrollToBeginning(direction))
+ };
+ 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(scrollable::Direction::Vertical),
+ text("Beginning!"),
+ vertical_space(Length::Units(1200)),
+ text("Middle!"),
+ vertical_space(Length::Units(1200)),
+ text("End!"),
+ scroll_to_beginning_button(
+ scrollable::Direction::Vertical
+ ),
]
.width(Length::Fill)
- .height(Length::Fill)
- .spacing(10)
+ .align_items(Alignment::Center)
+ .padding([40, 0, 40, 0])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .scrollbar_width(self.scrollbar_width)
+ .scrollbar_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(scrollable::Direction::Horizontal),
+ text("Beginning!"),
+ horizontal_space(Length::Units(1200)),
+ text("Middle!"),
+ horizontal_space(Length::Units(1200)),
+ text("End!"),
+ scroll_to_beginning_button(
+ scrollable::Direction::Horizontal
+ ),
+ ]
+ .height(Length::Units(450))
+ .align_items(Alignment::Center)
+ .padding([0, 40, 0, 40])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .horizontal_scroll(
+ scrollable::Horizontal::new()
+ .scrollbar_height(self.scrollbar_width)
+ .scrollbar_margin(self.scrollbar_margin)
+ .scroller_height(self.scroller_width),
+ )
+ .style(theme::Scrollable::Custom(Box::new(
+ 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(scrollable::Direction::Horizontal),
+ text("Horizontal - Beginning!"),
+ horizontal_space(Length::Units(1200)),
+ //vertical content
+ column![
+ text("Horizontal - Middle!"),
+ scroll_to_end_button(
+ scrollable::Direction::Vertical
+ ),
+ text("Vertical - Beginning!"),
+ vertical_space(Length::Units(1200)),
+ text("Vertical - Middle!"),
+ vertical_space(Length::Units(1200)),
+ text("Vertical - End!"),
+ scroll_to_beginning_button(
+ scrollable::Direction::Vertical
+ )
+ ]
+ .align_items(Alignment::Fill)
+ .spacing(40),
+ horizontal_space(Length::Units(1200)),
+ text("Horizontal - End!"),
+ scroll_to_beginning_button(
+ scrollable::Direction::Horizontal
+ ),
+ ]
+ .align_items(Alignment::Center)
+ .padding([0, 40, 0, 40])
+ .spacing(40),
+ )
+ .height(Length::Fill)
+ .scrollbar_width(self.scrollbar_width)
+ .scrollbar_margin(self.scrollbar_margin)
+ .scroller_width(self.scroller_width)
+ .horizontal_scroll(
+ scrollable::Horizontal::new()
+ .scrollbar_height(self.scrollbar_width)
+ .scrollbar_margin(self.scrollbar_margin)
+ .scroller_height(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(),
},
- ]
+ }
}
+}
- pub fn id(i: usize) -> scrollable::Id {
- scrollable::Id::new(format!("scrollable-{}", i))
+struct ProgressBarCustomStyle;
+
+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..b10ef17e 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -1,10 +1,10 @@
mod echo;
use iced::alignment::{self, Alignment};
-use iced::executor;
use iced::widget::{
button, column, container, row, scrollable, text, text_input, Column,
};
+use iced::{executor, Vector};
use iced::{
Application, Color, Command, Element, Length, Settings, Subscription, Theme,
};
@@ -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(),
+ Vector::new(0.0, 1.0),
+ )
}
},
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..1e8b7543 100644
--- a/native/src/widget/operation/scrollable.rs
+++ b/native/src/widget/operation/scrollable.rs
@@ -1,27 +1,22 @@
//! Operate on widgets that can be scrolled.
use crate::widget::{Id, Operation};
+use iced_core::Vector;
/// 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);
+ fn snap_to(&mut self, percentage: Vector<f32>);
}
/// 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, percentage: Vector<f32>) -> impl Operation<T> {
struct SnapTo {
target: Id,
- percentage: f32,
+ percentage: Vector<f32>,
}
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,6 +24,12 @@ 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.percentage);
+ }
+ }
}
SnapTo { target, percentage }
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index 20780f89..ec081343 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -13,8 +13,6 @@ use crate::{
Rectangle, Shell, Size, Vector, Widget,
};
-use std::{f32, u32};
-
pub use iced_style::scrollable::StyleSheet;
pub mod style {
@@ -37,8 +35,9 @@ where
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
+ scroll_horizontal: Option<Horizontal>,
content: Element<'a, Message, Renderer>,
- on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>,
+ on_scroll: Option<Box<dyn Fn(Vector<f32>) -> Message + 'a>>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -55,6 +54,7 @@ where
scrollbar_width: 10,
scrollbar_margin: 0,
scroller_width: 10,
+ scroll_horizontal: None,
content: content.into(),
on_scroll: None,
style: Default::default(),
@@ -74,7 +74,7 @@ where
}
/// Sets the scrollbar width of the [`Scrollable`] .
- /// Silently enforces a minimum value of 1.
+ /// Silently enforces a minimum width of 1.
pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self {
self.scrollbar_width = scrollbar_width.max(1);
self
@@ -88,17 +88,26 @@ where
/// Sets the scroller width of the [`Scrollable`] .
///
- /// It silently enforces a minimum value of 1.
+ /// It 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
}
+ /// Allow scrolling in a horizontal direction within the [`Scrollable`] .
+ pub fn horizontal_scroll(mut self, horizontal: Horizontal) -> Self {
+ self.scroll_horizontal = Some(horizontal);
+ 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(Vector<f32>) -> Message + 'a,
+ ) -> Self {
self.on_scroll = Some(Box::new(f));
self
}
@@ -113,28 +122,57 @@ where
}
}
-impl<'a, Message, Renderer> Widget<Message, Renderer>
- for Scrollable<'a, Message, Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: StyleSheet,
-{
- fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State>()
+/// Properties of a horizontal scrollbar within a [`Scrollable`].
+#[derive(Debug)]
+pub struct Horizontal {
+ scrollbar_height: u16,
+ scrollbar_margin: u16,
+ scroller_height: u16,
+}
+
+impl Default for Horizontal {
+ fn default() -> Self {
+ Self {
+ scrollbar_height: 10,
+ scrollbar_margin: 0,
+ scroller_height: 10,
+ }
}
+}
- fn state(&self) -> tree::State {
- tree::State::new(State::new())
+impl Horizontal {
+ /// Creates a new [`Horizontal`] for use in a [`Scrollable`].
+ pub fn new() -> Self {
+ Self::default()
}
- fn children(&self) -> Vec<Tree> {
- vec![Tree::new(&self.content)]
+ /// Sets the [`Horizontal`] scrollbar height of the [`Scrollable`] .
+ /// Silently enforces a minimum height of 1.
+ pub fn scrollbar_height(mut self, scrollbar_height: u16) -> Self {
+ self.scrollbar_height = scrollbar_height.max(1);
+ self
}
- fn diff(&self, tree: &mut Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ /// Sets the [`Horizontal`] scrollbar margin of the [`Scrollable`] .
+ pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self {
+ self.scrollbar_margin = scrollbar_margin;
+ self
}
+ /// Sets the scroller height of the [`Horizontal`] scrollbar of the [`Scrollable`] .
+ /// Silently enforces a minimum height of 1.
+ pub fn scroller_height(mut self, scroller_height: u16) -> Self {
+ self.scroller_height = scroller_height.max(1);
+ self
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for Scrollable<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+ Renderer::Theme: StyleSheet,
+{
fn width(&self) -> Length {
self.content.as_widget().width()
}
@@ -153,18 +191,64 @@ where
limits,
Widget::<Message, Renderer>::width(self),
self.height,
- u32::MAX,
+ self.scroll_horizontal.is_some(),
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
)
}
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) {
+ draw(
+ tree.state.downcast_ref::<State>(),
+ renderer,
+ theme,
+ layout,
+ cursor_position,
+ &self.style,
+ |renderer, layout, cursor_position, viewport| {
+ self.content.as_widget().draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor_position,
+ viewport,
+ )
+ },
+ )
+ }
+
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::new())
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ vec![Tree::new(&self.content)]
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(std::slice::from_ref(&self.content))
+ }
+
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
- renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
let state = tree.state.downcast_mut::<State>();
@@ -175,7 +259,6 @@ where
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
- renderer,
operation,
);
});
@@ -201,6 +284,7 @@ where
self.scrollbar_width,
self.scrollbar_margin,
self.scroller_width,
+ self.scroll_horizontal.as_ref(),
&self.on_scroll,
|event, layout, cursor_position, clipboard, shell| {
self.content.as_widget_mut().on_event(
@@ -216,40 +300,6 @@ where
)
}
- fn draw(
- &self,
- tree: &Tree,
- renderer: &mut Renderer,
- theme: &Renderer::Theme,
- style: &renderer::Style,
- layout: Layout<'_>,
- cursor_position: Point,
- _viewport: &Rectangle,
- ) {
- draw(
- tree.state.downcast_ref::<State>(),
- renderer,
- theme,
- layout,
- cursor_position,
- self.scrollbar_width,
- self.scrollbar_margin,
- self.scroller_width,
- &self.style,
- |renderer, layout, cursor_position, viewport| {
- self.content.as_widget().draw(
- &tree.children[0],
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
- )
- },
- )
- }
-
fn mouse_interaction(
&self,
tree: &Tree,
@@ -262,9 +312,6 @@ where
tree.state.downcast_ref::<State>(),
layout,
cursor_position,
- self.scrollbar_width,
- self.scrollbar_margin,
- self.scroller_width,
|layout, cursor_position, viewport| {
self.content.as_widget().mouse_interaction(
&tree.children[0],
@@ -278,13 +325,13 @@ where
}
fn overlay<'b>(
- &'b mut self,
+ &'b self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content
- .as_widget_mut()
+ .as_widget()
.overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
@@ -294,12 +341,15 @@ where
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let offset = tree
+ let (offset_x, offset_y) = tree
.state
.downcast_ref::<State>()
.offset(bounds, content_bounds);
- overlay.translate(Vector::new(0.0, -(offset as f32)))
+ overlay.translate(Vector::new(
+ -(offset_x as f32),
+ -(offset_y as f32),
+ ))
})
}
}
@@ -344,7 +394,10 @@ 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> {
+pub fn snap_to<Message: 'static>(
+ id: Id,
+ percentage: Vector<f32>,
+) -> Command<Message> {
Command::widget(operation::scrollable::snap_to(id.0, percentage))
}
@@ -354,14 +407,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);
@@ -382,7 +450,8 @@ pub fn update<Message>(
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
- on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>,
+ horizontal: Option<&Horizontal>,
+ on_scroll: &Option<Box<dyn Fn(Vector<f32>) -> Message + '_>>,
update_content: impl FnOnce(
Event,
Layout<'_>,
@@ -392,36 +461,39 @@ 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,
+ state.create_scrollbars_maybe(
+ horizontal,
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 (mouse_over_x_scrollbar, mouse_over_y_scrollbar) =
+ state.mouse_over_scrollbars(cursor_position);
let event_status = {
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
+ let cursor_position = if mouse_over_scrollable
+ && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
+ {
+ let (offset_x, offset_y) = state.offset(bounds, content_bounds);
+
Point::new(
- cursor_position.x,
- cursor_position.y + state.offset(bounds, content_bounds) as f32,
+ cursor_position.x + offset_x as f32,
+ cursor_position.y + offset_y as f32,
)
} 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 +509,18 @@ pub fn update<Message>(
return event::Status::Captured;
}
- if is_mouse_over {
+ 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 (?)
+ Vector::new(x * 60.0, y * 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,
@@ -463,18 +535,21 @@ pub fn update<Message>(
Event::Touch(event) => {
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 +562,7 @@ pub fn update<Message>(
}
touch::Event::FingerLifted { .. }
| touch::Event::FingerLost { .. } => {
- state.scroll_box_touched_at = None;
+ state.scroll_area_touched_at = None;
}
}
@@ -497,21 +572,21 @@ pub fn update<Message>(
}
}
- if state.is_scroller_grabbed() {
- 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;
+ if let Some(scrollbar) = &mut state.scrollbar_y {
+ if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ scrollbar.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(
+ return event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ scrollbar.scroll_to(
scrollbar.scroll_percentage(
scroller_grabbed_at,
cursor_position,
@@ -530,18 +605,18 @@ pub fn update<Message>(
return event::Status::Captured;
}
+ _ => {}
}
- _ => {}
- }
- } else if is_mouse_over_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- if let Some(scrollbar) = scrollbar {
+ } else if scrollbar.is_mouse_over(cursor_position) {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(scroller_grabbed_at) =
scrollbar.grab_scroller(cursor_position)
{
- state.scroll_to(
+ scrollbar.scroll_to(
scrollbar.scroll_percentage(
scroller_grabbed_at,
cursor_position,
@@ -550,7 +625,8 @@ pub fn update<Message>(
content_bounds,
);
- state.scroller_grabbed_at = Some(scroller_grabbed_at);
+ scrollbar.scroller.grabbed_at =
+ Some(scroller_grabbed_at);
notify_on_scroll(
state,
@@ -559,12 +635,84 @@ pub fn update<Message>(
content_bounds,
shell,
);
+ }
- return event::Status::Captured;
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ }
+ }
+
+ if let Some(scrollbar) = &mut state.scrollbar_x {
+ if let Some(scroller_grabbed_at) = scrollbar.scroller.grabbed_at {
+ match event {
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ scrollbar.scroller.grabbed_at = None;
+
+ return event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ scrollbar.scroll_to(
+ scrollbar.scroll_percentage(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ return event::Status::Captured;
+ }
+ _ => {}
+ }
+ } else if scrollbar.is_mouse_over(cursor_position) {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ if let Some(scroller_grabbed_at) =
+ scrollbar.grab_scroller(cursor_position)
+ {
+ scrollbar.scroll_to(
+ scrollbar.scroll_percentage(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ scrollbar.scroller.grabbed_at =
+ Some(scroller_grabbed_at);
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
}
+
+ return event::Status::Captured;
}
+ _ => {}
}
- _ => {}
}
}
@@ -576,9 +724,6 @@ pub fn mouse_interaction(
state: &State,
layout: Layout<'_>,
cursor_position: Point,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
content_interaction: impl FnOnce(
Layout<'_>,
Point,
@@ -586,39 +731,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 (mouse_over_x_scrollbar, mouse_over_y_scrollbar) =
+ state.mouse_over_scrollbars(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 (offset_x, offset_y) = 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)
+ {
+ Point::new(
+ cursor_position.x + offset_x as f32,
+ cursor_position.y + offset_y as f32,
+ )
} 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 as f32,
+ x: bounds.x + offset_x as f32,
..bounds
},
)
@@ -632,9 +776,6 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: Point,
- scrollbar_width: u16,
- scrollbar_margin: u16,
- scroller_width: u16,
style: &<Renderer::Theme as StyleSheet>::Style,
draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
) where
@@ -644,39 +785,38 @@ 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 (offset_x, offset_y) = 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 mouse_over_scrollable = bounds.contains(cursor_position);
+
+ let (mouse_over_x_scrollbar, mouse_over_y_scrollbar) =
+ state.mouse_over_scrollbars(cursor_position);
+
+ let cursor_position = if mouse_over_scrollable
+ && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar)
+ {
+ Point::new(
+ cursor_position.x + offset_x as f32,
+ cursor_position.y + offset_y as f32,
+ )
} else {
- Point::new(cursor_position.x, -1.0)
+ Point::new(-1.0, -1.0)
};
- if let Some(scrollbar) = scrollbar {
+ // Draw inner content
+ if state.scrollbar_y.is_some() || state.scrollbar_x.is_some() {
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(
- Vector::new(0.0, -(offset as f32)),
+ Vector::new(-(offset_x as f32), -(offset_y as f32)),
|renderer| {
draw_content(
renderer,
content_layout,
cursor_position,
&Rectangle {
- y: bounds.y + offset as f32,
+ y: bounds.y + offset_y as f32,
+ x: bounds.x + offset_x as f32,
..bounds
},
);
@@ -684,16 +824,65 @@ 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 draw_scrollbar =
+ |renderer: &mut Renderer, scrollbar: Option<&Scrollbar>| {
+ if let Some(scrollbar) = scrollbar {
+ let style = match scrollbar.direction {
+ Direction::Vertical => {
+ if scrollbar.scroller.grabbed_at.is_some() {
+ theme.dragging(style)
+ } else if mouse_over_y_scrollbar {
+ theme.hovered(style)
+ } else {
+ theme.active(style)
+ }
+ }
+ Direction::Horizontal => {
+ if scrollbar.scroller.grabbed_at.is_some() {
+ theme.dragging_horizontal(style)
+ } else if mouse_over_x_scrollbar {
+ theme.hovered_horizontal(style)
+ } else {
+ theme.active_horizontal(style)
+ }
+ }
+ };
- let is_scrollbar_visible =
- style.background.is_some() || style.border_width > 0.0;
+ //track
+ if style.background.is_some()
+ || (style.border_color != Color::TRANSPARENT
+ && style.border_width > 0.0)
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.bounds,
+ border_radius: style.border_radius,
+ border_width: style.border_width,
+ border_color: style.border_color,
+ },
+ style.background.unwrap_or(Background::Color(
+ Color::TRANSPARENT,
+ )),
+ );
+ }
+
+ //thumb
+ if style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border_color != Color::TRANSPARENT
+ && style.scroller.border_width > 0.0)
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scrollbar.scroller.bounds,
+ border_radius: style.scroller.border_radius,
+ border_width: style.scroller.border_width,
+ border_color: style.scroller.border_color,
+ },
+ style.scroller.color,
+ );
+ }
+ }
+ };
renderer.with_layer(
Rectangle {
@@ -702,33 +891,8 @@ pub fn draw<Renderer>(
..bounds
},
|renderer| {
- if is_scrollbar_visible {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.bounds,
- border_radius: style.border_radius.into(),
- border_width: style.border_width,
- border_color: style.border_color,
- },
- style
- .background
- .unwrap_or(Background::Color(Color::TRANSPARENT)),
- );
- }
-
- if (is_mouse_over || state.is_scroller_grabbed())
- && is_scrollbar_visible
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.scroller.bounds,
- border_radius: style.scroller.border_radius.into(),
- border_width: style.scroller.border_width,
- border_color: style.scroller.border_color,
- },
- style.scroller.color,
- );
- }
+ draw_scrollbar(renderer, state.scrollbar_y.as_ref());
+ draw_scrollbar(renderer, state.scrollbar_x.as_ref());
},
);
} else {
@@ -737,226 +901,403 @@ pub fn draw<Renderer>(
content_layout,
cursor_position,
&Rectangle {
- y: bounds.y + offset as f32,
+ x: bounds.x + offset_x as f32,
+ y: bounds.y + offset_y as f32,
..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(Vector<f32>) -> 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),
- ));
- }
-}
+ let delta_x = if content_bounds.width <= bounds.width {
+ 0.0
+ } else {
+ state.scrollbar_x.map_or(0.0, |scrollbar| {
+ scrollbar.offset.absolute(
+ Direction::Horizontal,
+ bounds,
+ content_bounds,
+ ) / (content_bounds.width - bounds.width)
+ })
+ };
-/// 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,
-}
+ let delta_y = if content_bounds.height <= bounds.height {
+ 0.0
+ } else {
+ state.scrollbar_y.map_or(0.0, |scrollbar| {
+ scrollbar.offset.absolute(
+ Direction::Vertical,
+ bounds,
+ content_bounds,
+ ) / (content_bounds.height - bounds.height)
+ })
+ };
-impl Default for State {
- fn default() -> Self {
- Self {
- scroller_grabbed_at: None,
- scroll_box_touched_at: None,
- offset: Offset::Absolute(0.0),
- }
+ shell.publish(on_scroll(Vector::new(delta_x, delta_y)))
}
}
-impl operation::Scrollable for State {
- fn snap_to(&mut self, percentage: f32) {
- State::snap_to(self, percentage);
- }
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+/// The direction of the [`Scrollable`].
+pub enum Direction {
+ /// X or horizontal
+ Horizontal,
+ /// Y or vertical
+ Vertical,
}
/// The local state of a [`Scrollable`].
-#[derive(Debug, Clone, Copy)]
-enum Offset {
- Absolute(f32),
- Relative(f32),
+#[derive(Debug, Clone, Copy, Default)]
+pub struct State {
+ scroll_area_touched_at: Option<Point>,
+ scrollbar_x: Option<Scrollbar>,
+ scrollbar_y: Option<Scrollbar>,
}
-impl Offset {
- fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 {
- match self {
- Self::Absolute(absolute) => {
- let hidden_content =
- (content_bounds.height - bounds.height).max(0.0);
-
- absolute.min(hidden_content)
- }
- Self::Relative(percentage) => {
- ((content_bounds.height - bounds.height) * percentage).max(0.0)
- }
+impl operation::Scrollable for State {
+ fn snap_to(&mut self, percentage: Vector<f32>) {
+ if let Some(scrollbar) = &mut self.scrollbar_y {
+ scrollbar.snap_to(percentage.y)
+ }
+ if let Some(scrollbar) = &mut self.scrollbar_x {
+ scrollbar.snap_to(percentage.x)
}
}
}
impl State {
- /// Creates a new [`State`] with the scrollbar located at the top.
+ /// Creates a new [`State`].
pub fn new() -> Self {
State::default()
}
- /// Apply a scrolling offset to the current [`State`], given the bounds of
- /// the [`Scrollable`] and its contents.
- pub fn scroll(
+ /// Create y or x scrollbars if content is overflowing the [`Scrollable`] bounds.
+ pub fn create_scrollbars_maybe(
&mut self,
- delta_y: f32,
+ horizontal: Option<&Horizontal>,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- if bounds.height >= content_bounds.height {
- return;
- }
+ let show_scrollbar_x = horizontal.and_then(|h| {
+ if content_bounds.width > bounds.width {
+ Some(h)
+ } else {
+ None
+ }
+ });
- self.offset = Offset::Absolute(
- (self.offset.absolute(bounds, content_bounds) - delta_y)
- .clamp(0.0, content_bounds.height - bounds.height),
- );
- }
+ self.scrollbar_y = if content_bounds.height > bounds.height {
+ let (offset_y, scroller_grabbed) =
+ if let Some(scrollbar) = &self.scrollbar_y {
+ (
+ scrollbar.offset.absolute(
+ scrollbar.direction,
+ bounds,
+ content_bounds,
+ ),
+ scrollbar.scroller.grabbed_at,
+ )
+ } else {
+ (0.0, None)
+ };
+
+ // Need to adjust the height of the vertical scrollbar if the horizontal scrollbar
+ // is present
+ let scrollbar_x_height = show_scrollbar_x.map_or(0.0, |h| {
+ (h.scrollbar_height.max(h.scroller_height) + h.scrollbar_margin)
+ as f32
+ });
+
+ let total_scrollbar_width =
+ scrollbar_width.max(scroller_width) + 2 * scrollbar_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 - scrollbar_x_height).max(0.0),
+ };
+
+ // Bounds of just the scrollbar
+ let scrollbar_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - f32::from(
+ total_scrollbar_width / 2 + scrollbar_width / 2,
+ ),
+ y: bounds.y,
+ width: scrollbar_width as f32,
+ height: (bounds.height - scrollbar_x_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 as f32 * 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 - scrollbar_x_height)
+ .max(0.0),
+ width: scroller_width as f32,
+ height: scroller_height,
+ };
+
+ Some(Scrollbar {
+ total_bounds: total_scrollbar_bounds,
+ bounds: scrollbar_bounds,
+ direction: Direction::Vertical,
+ scroller: Scroller {
+ bounds: scroller_bounds,
+ grabbed_at: scroller_grabbed,
+ },
+ offset: Offset::Absolute(offset_y),
+ })
+ } else {
+ None
+ };
- /// Scrolls the [`Scrollable`] to a relative amount.
- ///
- /// `0` represents scrollbar at the top, while `1` represents scrollbar at
- /// the bottom.
- pub fn scroll_to(
- &mut self,
- percentage: f32,
- bounds: Rectangle,
- content_bounds: Rectangle,
- ) {
- self.snap_to(percentage);
- self.unsnap(bounds, content_bounds);
+ self.scrollbar_x = if let Some(horizontal) = show_scrollbar_x {
+ let (offset_x, scroller_grabbed) =
+ if let Some(scrollbar) = &self.scrollbar_x {
+ (
+ scrollbar.offset.absolute(
+ scrollbar.direction,
+ bounds,
+ content_bounds,
+ ),
+ scrollbar.scroller.grabbed_at,
+ )
+ } else {
+ (0.0, None)
+ };
+
+ // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
+ // is present
+ let scrollbar_y_width = self.scrollbar_y.map_or(0.0, |_| {
+ (scrollbar_width.max(scroller_width) + scrollbar_margin) as f32
+ });
+
+ let total_scrollbar_height =
+ horizontal.scrollbar_height.max(horizontal.scroller_height)
+ + 2 * horizontal.scrollbar_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
+ + horizontal.scrollbar_height / 2,
+ ),
+ width: (bounds.width - scrollbar_y_width).max(0.0),
+ height: horizontal.scrollbar_height as f32,
+ };
+
+ let ratio = bounds.width / content_bounds.width;
+ // min width for easier grabbing with extra wide content
+ let scroller_width = (bounds.width * ratio).max(2.0);
+ let scroller_offset = offset_x as f32 * 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
+ + horizontal.scroller_height / 2,
+ ),
+ width: scroller_width,
+ height: horizontal.scroller_height as f32,
+ };
+
+ Some(Scrollbar {
+ total_bounds: total_scrollbar_bounds,
+ bounds: scrollbar_bounds,
+ direction: Direction::Horizontal,
+ scroller: Scroller {
+ bounds: scroller_bounds,
+ grabbed_at: scroller_grabbed,
+ },
+ offset: Offset::Absolute(offset_x),
+ })
+ } else {
+ None
+ };
}
- /// Snaps the scroll position to a relative amount.
- ///
- /// `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));
+ /// Returns whether the mouse is within the bounds of each scrollbar.
+ fn mouse_over_scrollbars(&self, cursor_position: Point) -> (bool, bool) {
+ (
+ self.scrollbar_x.map_or(false, |scrollbar| {
+ scrollbar.is_mouse_over(cursor_position)
+ }),
+ self.scrollbar_y.map_or(false, |scrollbar| {
+ scrollbar.is_mouse_over(cursor_position)
+ }),
+ )
}
- /// 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));
+ /// Returns whether the scroller for either scrollbar is currently grabbed.
+ fn scrollers_grabbed(&self) -> bool {
+ self.scrollbar_x
+ .map_or(false, |scrollbar| scrollbar.scroller.grabbed_at.is_some())
+ || self.scrollbar_y.map_or(false, |scrollbar| {
+ scrollbar.scroller.grabbed_at.is_some()
+ })
}
- /// 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
- }
+ /// Apply a scrolling offset to the current [`State`], given the bounds of
+ /// the [`Scrollable`] and its contents.
+ pub fn scroll(
+ &mut self,
+ delta: Vector<f32>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ if delta.x != 0.0 && bounds.width < content_bounds.width {
+ if let Some(scrollbar) = &mut self.scrollbar_x {
+ scrollbar.offset = Offset::Absolute(
+ (scrollbar.offset.absolute(
+ Direction::Horizontal,
+ bounds,
+ content_bounds,
+ ) - delta.x)
+ .max(0.0)
+ .min((content_bounds.width - bounds.width) as f32),
+ );
+ }
+ }
- /// Returns whether the scroller is currently grabbed or not.
- pub fn is_scroller_grabbed(&self) -> bool {
- self.scroller_grabbed_at.is_some()
+ if delta.y != 0.0 && bounds.height < content_bounds.height {
+ if let Some(scrollbar) = &mut self.scrollbar_y {
+ scrollbar.offset = Offset::Absolute(
+ (scrollbar.offset.absolute(
+ Direction::Vertical,
+ bounds,
+ content_bounds,
+ ) - delta.y)
+ .max(0.0)
+ .min((content_bounds.height - bounds.height) as f32),
+ )
+ }
+ }
}
- /// 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()
+ /// Returns the current x & y scrolling offset of the [`State`], given the bounds
+ /// of the [`Scrollable`] and its contents.
+ pub fn offset(
+ &self,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) -> (f32, f32) {
+ (
+ self.scrollbar_x.map_or(0.0, |scrollbar| {
+ scrollbar.offset.absolute(
+ Direction::Horizontal,
+ bounds,
+ content_bounds,
+ )
+ }),
+ self.scrollbar_y.map_or(0.0, |scrollbar| {
+ scrollbar.offset.absolute(
+ Direction::Vertical,
+ bounds,
+ content_bounds,
+ )
+ }),
+ )
}
}
/// 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 direction of the [`Scrollbar`].
+ direction: Direction,
+
+ /// The state of this scrollbar's [`Scroller`].
scroller: Scroller,
+
+ /// The current offset of the [`Scrollbar`].
+ offset: Offset,
}
impl Scrollbar {
+ /// Snaps the scroll position to a relative amount.
+ ///
+ /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at
+ /// the end.
+ pub fn snap_to(&mut self, percentage: f32) {
+ self.offset = Offset::Relative(percentage.max(0.0).min(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(
+ self.direction,
+ bounds,
+ content_bounds,
+ ));
+ }
+
+ /// Scrolls the [`Scrollbar`] to a certain percentage.
+ fn scroll_to(
+ &mut self,
+ percentage: f32,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.snap_to(percentage);
+ self.unsnap(bounds, content_bounds);
+ }
+
+ /// 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) {
+ if self.total_bounds.contains(cursor_position) {
Some(if self.scroller.bounds.contains(cursor_position) {
- (cursor_position.y - self.scroller.bounds.y)
- / self.scroller.bounds.height
+ match self.direction {
+ Direction::Vertical => {
+ (cursor_position.y - self.scroller.bounds.y)
+ / self.scroller.bounds.height
+ }
+ Direction::Horizontal => {
+ (cursor_position.x - self.scroller.bounds.x)
+ / self.scroller.bounds.width
+ }
+ }
} else {
0.5
})
@@ -970,10 +1311,56 @@ impl Scrollbar {
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)
+ match self.direction {
+ Direction::Vertical => {
+ (cursor_position.y
+ - self.bounds.y
+ - self.scroller.bounds.height * grabbed_at)
+ / (self.bounds.height - self.scroller.bounds.height)
+ }
+ Direction::Horizontal => {
+ (cursor_position.x
+ - self.bounds.x
+ - self.scroller.bounds.width * grabbed_at)
+ / (self.bounds.width - self.scroller.bounds.width)
+ }
+ }
+ }
+}
+
+/// The directional offset of a [`Scrollable`].
+#[derive(Debug, Clone, Copy)]
+enum Offset {
+ Absolute(f32),
+ Relative(f32),
+}
+
+impl Offset {
+ fn absolute(
+ self,
+ direction: Direction,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) -> f32 {
+ match self {
+ Self::Absolute(absolute) => match direction {
+ Direction::Horizontal => {
+ absolute.min((content_bounds.width - bounds.width).max(0.0))
+ }
+ Direction::Vertical => absolute
+ .min((content_bounds.height - bounds.height).max(0.0)),
+ },
+ Self::Relative(percentage) => match direction {
+ Direction::Horizontal => {
+ ((content_bounds.width - bounds.width) * percentage)
+ .max(0.0)
+ }
+ Direction::Vertical => {
+ ((content_bounds.height - bounds.height) * percentage)
+ .max(0.0)
+ }
+ },
+ }
}
}
@@ -982,4 +1369,7 @@ impl Scrollbar {
struct Scroller {
/// The bounds of the [`Scroller`].
bounds: Rectangle,
+
+ /// Whether or not the scroller is currently grabbed.
+ grabbed_at: Option<f32>,
}
diff --git a/src/widget.rs b/src/widget.rs
index 76cea7be..ee30548c 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, Direction, Horizontal, Id,
+ 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 271d9a29..cef8f2be 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -925,6 +925,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.