summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-03-08 13:34:36 +0100
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-03-08 13:34:36 +0100
commit288025f5143f4e3f8bc5af36e86f7afa7f07a4c7 (patch)
treee187f94cc74915f686a4b7cefd0984f4de6eae83
parent7161cb40c7a9fbda84ee61060a109b93416b2ea0 (diff)
downloadiced-288025f5143f4e3f8bc5af36e86f7afa7f07a4c7.tar.gz
iced-288025f5143f4e3f8bc5af36e86f7afa7f07a4c7.tar.bz2
iced-288025f5143f4e3f8bc5af36e86f7afa7f07a4c7.zip
Inline helper functions in `widget` modules
-rw-r--r--widget/src/combo_box.rs8
-rw-r--r--widget/src/lib.rs2
-rw-r--r--widget/src/pick_list.rs405
-rw-r--r--widget/src/scrollable.rs807
4 files changed, 553 insertions, 669 deletions
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 933a0fac..bddf2789 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -717,8 +717,7 @@ where
}
}
-/// Search list of options for a given query.
-pub fn search<'a, T, A>(
+fn search<'a, T, A>(
options: impl IntoIterator<Item = T> + 'a,
option_matchers: impl IntoIterator<Item = &'a A> + 'a,
query: &'a str,
@@ -745,8 +744,7 @@ where
})
}
-/// Build matchers from given list of options.
-pub fn build_matchers<'a, T>(
+fn build_matchers<'a, T>(
options: impl IntoIterator<Item = T> + 'a,
) -> Vec<String>
where
@@ -769,6 +767,8 @@ pub struct Style<Theme> {
pub text_input: fn(&Theme, text_input::Status) -> text_input::Appearance,
/// The style of the [`Menu`] of the [`ComboBox`].
+ ///
+ /// [`Menu`]: menu::Menu
pub menu: menu::Style<Theme>,
}
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 72596efc..209dfad9 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -18,6 +18,7 @@ pub use iced_runtime::core;
mod column;
mod mouse_area;
mod row;
+mod space;
mod themer;
pub mod button;
@@ -33,7 +34,6 @@ pub mod radio;
pub mod rule;
pub mod scrollable;
pub mod slider;
-pub mod space;
pub mod text;
pub mod text_editor;
pub mod text_input;
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index b8fc6079..beb4e0c1 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -16,6 +16,7 @@ use crate::core::{
use crate::overlay::menu::{self, Menu};
use std::borrow::Borrow;
+use std::f32;
/// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)]
@@ -186,19 +187,77 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(
- tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
- renderer,
- limits,
- self.width,
- self.padding,
- self.text_size,
- self.text_line_height,
- self.text_shaping,
- self.font,
- self.placeholder.as_deref(),
- self.options.borrow(),
- )
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
+
+ let font = self.font.unwrap_or_else(|| renderer.default_font());
+ let text_size =
+ self.text_size.unwrap_or_else(|| renderer.default_size());
+ let options = self.options.borrow();
+
+ state.options.resize_with(options.len(), Default::default);
+
+ let option_text = Text {
+ content: "",
+ bounds: Size::new(
+ f32::INFINITY,
+ self.text_line_height.to_absolute(text_size).into(),
+ ),
+ size: text_size,
+ line_height: self.text_line_height,
+ font,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: self.text_shaping,
+ };
+
+ for (option, paragraph) in options.iter().zip(state.options.iter_mut())
+ {
+ let label = option.to_string();
+
+ paragraph.update(Text {
+ content: &label,
+ ..option_text
+ });
+ }
+
+ if let Some(placeholder) = &self.placeholder {
+ state.placeholder.update(Text {
+ content: placeholder,
+ ..option_text
+ });
+ }
+
+ let max_width = match self.width {
+ Length::Shrink => {
+ let labels_width =
+ state.options.iter().fold(0.0, |width, paragraph| {
+ f32::max(width, paragraph.min_width())
+ });
+
+ labels_width.max(
+ self.placeholder
+ .as_ref()
+ .map(|_| state.placeholder.min_width())
+ .unwrap_or(0.0),
+ )
+ }
+ _ => 0.0,
+ };
+
+ let size = {
+ let intrinsic = Size::new(
+ max_width + text_size.0 + self.padding.left,
+ f32::from(self.text_line_height.to_absolute(text_size)),
+ );
+
+ limits
+ .width(self.width)
+ .shrink(self.padding)
+ .resolve(self.width, Length::Shrink, intrinsic)
+ .expand(self.padding)
+ };
+
+ layout::Node::new(size)
}
fn on_event(
@@ -212,18 +271,98 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- update(
- event,
- layout,
- cursor,
- shell,
- self.on_select.as_ref(),
- self.on_open.as_ref(),
- self.on_close.as_ref(),
- self.selected.as_ref().map(Borrow::borrow),
- self.options.borrow(),
- || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
- )
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let state =
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>();
+
+ if state.is_open {
+ // Event wasn't processed by overlay, so cursor was clicked either outside its
+ // bounds or on the drop-down, either way we close the overlay.
+ state.is_open = false;
+
+ if let Some(on_close) = &self.on_close {
+ shell.publish(on_close.clone());
+ }
+
+ event::Status::Captured
+ } else if cursor.is_over(layout.bounds()) {
+ let selected = self.selected.as_ref().map(Borrow::borrow);
+
+ state.is_open = true;
+ state.hovered_option = self
+ .options
+ .borrow()
+ .iter()
+ .position(|option| Some(option) == selected);
+
+ if let Some(on_open) = &self.on_open {
+ shell.publish(on_open.clone());
+ }
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ }
+ }
+ Event::Mouse(mouse::Event::WheelScrolled {
+ delta: mouse::ScrollDelta::Lines { y, .. },
+ }) => {
+ let state =
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>();
+
+ if state.keyboard_modifiers.command()
+ && cursor.is_over(layout.bounds())
+ && !state.is_open
+ {
+ fn find_next<'a, T: PartialEq>(
+ selected: &'a T,
+ mut options: impl Iterator<Item = &'a T>,
+ ) -> Option<&'a T> {
+ let _ = options.find(|&option| option == selected);
+
+ options.next()
+ }
+
+ let options = self.options.borrow();
+ let selected = self.selected.as_ref().map(Borrow::borrow);
+
+ let next_option = if y < 0.0 {
+ if let Some(selected) = selected {
+ find_next(selected, options.iter())
+ } else {
+ options.first()
+ }
+ } else if y > 0.0 {
+ if let Some(selected) = selected {
+ find_next(selected, options.iter().rev())
+ } else {
+ options.last()
+ }
+ } else {
+ None
+ };
+
+ if let Some(next_option) = next_option {
+ shell.publish((self.on_select)(next_option.clone()));
+ }
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ }
+ }
+ Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
+ let state =
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>();
+
+ state.keyboard_modifiers = modifiers;
+
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
+ }
}
fn mouse_interaction(
@@ -234,7 +373,14 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(layout, cursor)
+ let bounds = layout.bounds();
+ let is_mouse_over = cursor.is_over(bounds);
+
+ if is_mouse_over {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::default()
+ }
}
fn draw(
@@ -429,9 +575,8 @@ where
}
}
-/// The state of a [`PickList`].
#[derive(Debug)]
-pub struct State<P: text::Paragraph> {
+struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
@@ -504,210 +649,6 @@ pub struct Icon<Font> {
pub shaping: text::Shaping,
}
-/// Computes the layout of a [`PickList`].
-pub fn layout<Renderer, T>(
- state: &mut State<Renderer::Paragraph>,
- renderer: &Renderer,
- limits: &layout::Limits,
- width: Length,
- padding: Padding,
- text_size: Option<Pixels>,
- text_line_height: text::LineHeight,
- text_shaping: text::Shaping,
- font: Option<Renderer::Font>,
- placeholder: Option<&str>,
- options: &[T],
-) -> layout::Node
-where
- Renderer: text::Renderer,
- T: ToString,
-{
- use std::f32;
-
- let font = font.unwrap_or_else(|| renderer.default_font());
- let text_size = text_size.unwrap_or_else(|| renderer.default_size());
-
- state.options.resize_with(options.len(), Default::default);
-
- let option_text = Text {
- content: "",
- bounds: Size::new(
- f32::INFINITY,
- text_line_height.to_absolute(text_size).into(),
- ),
- size: text_size,
- line_height: text_line_height,
- font,
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text_shaping,
- };
-
- for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
- let label = option.to_string();
-
- paragraph.update(Text {
- content: &label,
- ..option_text
- });
- }
-
- if let Some(placeholder) = placeholder {
- state.placeholder.update(Text {
- content: placeholder,
- ..option_text
- });
- }
-
- let max_width = match width {
- Length::Shrink => {
- let labels_width =
- state.options.iter().fold(0.0, |width, paragraph| {
- f32::max(width, paragraph.min_width())
- });
-
- labels_width.max(
- placeholder
- .map(|_| state.placeholder.min_width())
- .unwrap_or(0.0),
- )
- }
- _ => 0.0,
- };
-
- let size = {
- let intrinsic = Size::new(
- max_width + text_size.0 + padding.left,
- f32::from(text_line_height.to_absolute(text_size)),
- );
-
- limits
- .width(width)
- .shrink(padding)
- .resolve(width, Length::Shrink, intrinsic)
- .expand(padding)
- };
-
- layout::Node::new(size)
-}
-
-/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
-/// accordingly.
-pub fn update<'a, T, P, Message>(
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- shell: &mut Shell<'_, Message>,
- on_select: &dyn Fn(T) -> Message,
- on_open: Option<&Message>,
- on_close: Option<&Message>,
- selected: Option<&T>,
- options: &[T],
- state: impl FnOnce() -> &'a mut State<P>,
-) -> event::Status
-where
- T: PartialEq + Clone + 'a,
- P: text::Paragraph + 'a,
- Message: Clone,
-{
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let state = state();
-
- if state.is_open {
- // Event wasn't processed by overlay, so cursor was clicked either outside it's
- // bounds or on the drop-down, either way we close the overlay.
- state.is_open = false;
-
- if let Some(on_close) = on_close {
- shell.publish(on_close.clone());
- }
-
- event::Status::Captured
- } else if cursor.is_over(layout.bounds()) {
- state.is_open = true;
- state.hovered_option =
- options.iter().position(|option| Some(option) == selected);
-
- if let Some(on_open) = on_open {
- shell.publish(on_open.clone());
- }
-
- event::Status::Captured
- } else {
- event::Status::Ignored
- }
- }
- Event::Mouse(mouse::Event::WheelScrolled {
- delta: mouse::ScrollDelta::Lines { y, .. },
- }) => {
- let state = state();
-
- if state.keyboard_modifiers.command()
- && cursor.is_over(layout.bounds())
- && !state.is_open
- {
- fn find_next<'a, T: PartialEq>(
- selected: &'a T,
- mut options: impl Iterator<Item = &'a T>,
- ) -> Option<&'a T> {
- let _ = options.find(|&option| option == selected);
-
- options.next()
- }
-
- let next_option = if y < 0.0 {
- if let Some(selected) = selected {
- find_next(selected, options.iter())
- } else {
- options.first()
- }
- } else if y > 0.0 {
- if let Some(selected) = selected {
- find_next(selected, options.iter().rev())
- } else {
- options.last()
- }
- } else {
- None
- };
-
- if let Some(next_option) = next_option {
- shell.publish((on_select)(next_option.clone()));
- }
-
- event::Status::Captured
- } else {
- event::Status::Ignored
- }
- }
- Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- let state = state();
-
- state.keyboard_modifiers = modifiers;
-
- event::Status::Ignored
- }
- _ => event::Status::Ignored,
- }
-}
-
-/// Returns the current [`mouse::Interaction`] of a [`PickList`].
-pub fn mouse_interaction(
- layout: Layout<'_>,
- cursor: mouse::Cursor,
-) -> mouse::Interaction {
- let bounds = layout.bounds();
- let is_mouse_over = cursor.is_over(bounds);
-
- if is_mouse_over {
- mouse::Interaction::Pointer
- } else {
- mouse::Interaction::default()
- }
-}
-
/// The possible status of a [`PickList`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 6cd2048f..9770ce57 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -269,20 +269,29 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(
- renderer,
- limits,
- self.width,
- self.height,
- &self.direction,
- |renderer, limits| {
- self.content.as_widget().layout(
- &mut tree.children[0],
- renderer,
- limits,
- )
- },
- )
+ layout::contained(limits, self.width, self.height, |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if self.direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if self.direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
+
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &child_limits,
+ )
+ })
}
fn operate(
@@ -332,28 +341,316 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- update(
- tree.state.downcast_mut::<State>(),
- event,
- layout,
- cursor,
- clipboard,
- shell,
- self.direction,
- &self.on_scroll,
- |event, layout, cursor, clipboard, shell, viewport| {
- self.content.as_widget_mut().on_event(
- &mut tree.children[0],
- event,
- layout,
- cursor,
- renderer,
- clipboard,
+ let state = tree.state.downcast_mut::<State>();
+ let bounds = layout.bounds();
+ let cursor_over_scrollable = cursor.position_over(bounds);
+
+ let content = layout.children().next().unwrap();
+ let content_bounds = content.bounds();
+
+ let scrollbars =
+ Scrollbars::new(state, self.direction, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor);
+
+ let mut event_status = {
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(
+ cursor_position
+ + state.translation(
+ self.direction,
+ bounds,
+ content_bounds,
+ ),
+ )
+ }
+ _ => mouse::Cursor::Unavailable,
+ };
+
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ self.content.as_widget_mut().on_event(
+ &mut tree.children[0],
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ )
+ };
+
+ if let event::Status::Captured = event_status {
+ return event::Status::Captured;
+ }
+
+ if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) =
+ event
+ {
+ state.keyboard_modifiers = modifiers;
+
+ return event::Status::Ignored;
+ }
+
+ match event {
+ Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+ if cursor_over_scrollable.is_none() {
+ return event::Status::Ignored;
+ }
+
+ 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 { x, y } => Vector::new(x, y),
+ };
+
+ state.scroll(delta, self.direction, bounds, content_bounds);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
shell,
- viewport,
- )
- },
- )
+ );
+
+ event_status = event::Status::Captured;
+ }
+ Event::Touch(event)
+ if state.scroll_area_touched_at.is_some()
+ || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
+ {
+ match event {
+ touch::Event::FingerPressed { .. } => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ state.scroll_area_touched_at = Some(cursor_position);
+ }
+ touch::Event::FingerMoved { .. } => {
+ if let Some(scroll_box_touched_at) =
+ state.scroll_area_touched_at
+ {
+ let Some(cursor_position) = cursor.position()
+ else {
+ return event::Status::Ignored;
+ };
+
+ let delta = Vector::new(
+ cursor_position.x - scroll_box_touched_at.x,
+ cursor_position.y - scroll_box_touched_at.y,
+ );
+
+ state.scroll(
+ delta,
+ self.direction,
+ bounds,
+ content_bounds,
+ );
+
+ state.scroll_area_touched_at =
+ Some(cursor_position);
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+ }
+ touch::Event::FingerLifted { .. }
+ | touch::Event::FingerLost { .. } => {
+ state.scroll_area_touched_at = None;
+ }
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+
+ 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.y_scroller_grabbed_at = None;
+
+ event_status = event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let Some(scrollbar) = scrollbars.y {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ state.scroll_y_to(
+ scrollbar.scroll_percentage_y(
+ scroller_grabbed_at,
+ cursor_position,
+ ),
+ bounds,
+ content_bounds,
+ );
+
+ notify_on_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ event_status = event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ } else if mouse_over_y_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ 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,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ event_status = 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;
+
+ event_status = event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ 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,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+ } else if mouse_over_x_scrollbar {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
+ 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,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
+
+ event_status = event::Status::Captured;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ event_status
}
fn draw(
@@ -551,21 +848,48 @@ where
_viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(
- tree.state.downcast_ref::<State>(),
- layout,
- cursor,
- self.direction,
- |layout, cursor, viewport| {
- self.content.as_widget().mouse_interaction(
- &tree.children[0],
- layout,
- cursor,
- viewport,
- renderer,
- )
- },
- )
+ let state = tree.state.downcast_ref::<State>();
+ let bounds = layout.bounds();
+ let cursor_over_scrollable = cursor.position_over(bounds);
+
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+
+ let scrollbars =
+ Scrollbars::new(state, self.direction, bounds, content_bounds);
+
+ let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
+ scrollbars.is_mouse_over(cursor);
+
+ if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
+ || state.scrollers_grabbed()
+ {
+ mouse::Interaction::Idle
+ } else {
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(cursor_position + translation)
+ }
+ _ => mouse::Cursor::Unavailable,
+ };
+
+ self.content.as_widget().mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ renderer,
+ )
+ }
}
fn overlay<'b>(
@@ -651,386 +975,6 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset))
}
-/// Computes the layout of a [`Scrollable`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
- limits: &layout::Limits,
- width: Length,
- height: Length,
- direction: &Direction,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
-) -> layout::Node {
- layout::contained(limits, width, height, |limits| {
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
-
- layout_content(renderer, &child_limits)
- })
-}
-
-/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
-/// accordingly.
-pub fn update<Message>(
- state: &mut State,
- event: Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Message>,
- direction: Direction,
- on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
- update_content: impl FnOnce(
- Event,
- Layout<'_>,
- mouse::Cursor,
- &mut dyn Clipboard,
- &mut Shell<'_, Message>,
- &Rectangle,
- ) -> event::Status,
-) -> event::Status {
- let bounds = layout.bounds();
- let cursor_over_scrollable = cursor.position_over(bounds);
-
- let content = layout.children().next().unwrap();
- let content_bounds = content.bounds();
-
- let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
-
- let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor);
-
- let mut event_status = {
- let cursor = match cursor_over_scrollable {
- Some(cursor_position)
- if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
- {
- mouse::Cursor::Available(
- cursor_position
- + state.translation(direction, bounds, content_bounds),
- )
- }
- _ => mouse::Cursor::Unavailable,
- };
-
- let translation = state.translation(direction, bounds, content_bounds);
-
- update_content(
- event.clone(),
- content,
- cursor,
- clipboard,
- shell,
- &Rectangle {
- y: bounds.y + translation.y,
- x: bounds.x + translation.x,
- ..bounds
- },
- )
- };
-
- if let event::Status::Captured = event_status {
- return event::Status::Captured;
- }
-
- if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event
- {
- state.keyboard_modifiers = modifiers;
-
- return event::Status::Ignored;
- }
-
- match event {
- Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- if cursor_over_scrollable.is_none() {
- return event::Status::Ignored;
- }
-
- 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 { x, y } => Vector::new(x, y),
- };
-
- state.scroll(delta, direction, bounds, content_bounds);
-
- notify_on_scroll(state, on_scroll, bounds, content_bounds, shell);
-
- event_status = event::Status::Captured;
- }
- Event::Touch(event)
- if state.scroll_area_touched_at.is_some()
- || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
- {
- match event {
- touch::Event::FingerPressed { .. } => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_area_touched_at = Some(cursor_position);
- }
- touch::Event::FingerMoved { .. } => {
- if let Some(scroll_box_touched_at) =
- state.scroll_area_touched_at
- {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
- );
-
- state.scroll(delta, direction, bounds, content_bounds);
-
- state.scroll_area_touched_at = Some(cursor_position);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
- }
- touch::Event::FingerLifted { .. }
- | touch::Event::FingerLost { .. } => {
- state.scroll_area_touched_at = None;
- }
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
-
- 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.y_scroller_grabbed_at = None;
-
- event_status = event::Status::Captured;
- }
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- } else if mouse_over_y_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- 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,
- );
- }
-
- event_status = 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;
-
- event_status = event::Status::Captured;
- }
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- 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,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- } else if mouse_over_x_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- 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,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- }
-
- event_status
-}
-
-/// Computes the current [`mouse::Interaction`] of a [`Scrollable`].
-pub fn mouse_interaction(
- state: &State,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- direction: Direction,
- content_interaction: impl FnOnce(
- Layout<'_>,
- mouse::Cursor,
- &Rectangle,
- ) -> mouse::Interaction,
-) -> mouse::Interaction {
- let bounds = layout.bounds();
- let cursor_over_scrollable = cursor.position_over(bounds);
-
- let content_layout = layout.children().next().unwrap();
- let content_bounds = content_layout.bounds();
-
- let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
-
- let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor);
-
- if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
- || state.scrollers_grabbed()
- {
- mouse::Interaction::Idle
- } else {
- let translation = state.translation(direction, bounds, content_bounds);
-
- let cursor = match cursor_over_scrollable {
- Some(cursor_position)
- if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
- {
- mouse::Cursor::Available(cursor_position + translation)
- }
- _ => mouse::Cursor::Unavailable,
- };
-
- content_interaction(
- content_layout,
- cursor,
- &Rectangle {
- y: bounds.y + translation.y,
- x: bounds.x + translation.x,
- ..bounds
- },
- )
- }
-}
-
fn notify_on_scroll<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
@@ -1078,9 +1022,8 @@ fn notify_on_scroll<Message>(
}
}
-/// The local state of a [`Scrollable`].
#[derive(Debug, Clone, Copy)]
-pub struct State {
+struct State {
scroll_area_touched_at: Option<Point>,
offset_y: Offset,
y_scroller_grabbed_at: Option<f32>,