Inline helper functions in widget modules

This commit is contained in:
Héctor Ramón Jiménez 2024-03-08 13:34:36 +01:00
parent 7161cb40c7
commit 288025f514
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
4 changed files with 553 additions and 669 deletions

View file

@ -717,8 +717,7 @@ where
} }
} }
/// Search list of options for a given query. fn search<'a, T, A>(
pub fn search<'a, T, A>(
options: impl IntoIterator<Item = T> + 'a, options: impl IntoIterator<Item = T> + 'a,
option_matchers: impl IntoIterator<Item = &'a A> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a,
query: &'a str, query: &'a str,
@ -745,8 +744,7 @@ where
}) })
} }
/// Build matchers from given list of options. fn build_matchers<'a, T>(
pub fn build_matchers<'a, T>(
options: impl IntoIterator<Item = T> + 'a, options: impl IntoIterator<Item = T> + 'a,
) -> Vec<String> ) -> Vec<String>
where where
@ -769,6 +767,8 @@ pub struct Style<Theme> {
pub text_input: fn(&Theme, text_input::Status) -> text_input::Appearance, pub text_input: fn(&Theme, text_input::Status) -> text_input::Appearance,
/// The style of the [`Menu`] of the [`ComboBox`]. /// The style of the [`Menu`] of the [`ComboBox`].
///
/// [`Menu`]: menu::Menu
pub menu: menu::Style<Theme>, pub menu: menu::Style<Theme>,
} }

View file

@ -18,6 +18,7 @@ pub use iced_runtime::core;
mod column; mod column;
mod mouse_area; mod mouse_area;
mod row; mod row;
mod space;
mod themer; mod themer;
pub mod button; pub mod button;
@ -33,7 +34,6 @@ pub mod radio;
pub mod rule; pub mod rule;
pub mod scrollable; pub mod scrollable;
pub mod slider; pub mod slider;
pub mod space;
pub mod text; pub mod text;
pub mod text_editor; pub mod text_editor;
pub mod text_input; pub mod text_input;

View file

@ -16,6 +16,7 @@ use crate::core::{
use crate::overlay::menu::{self, Menu}; use crate::overlay::menu::{self, Menu};
use std::borrow::Borrow; use std::borrow::Borrow;
use std::f32;
/// A widget for selecting a single value from a list of options. /// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
@ -186,19 +187,77 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
renderer, let font = self.font.unwrap_or_else(|| renderer.default_font());
limits, let text_size =
self.width, self.text_size.unwrap_or_else(|| renderer.default_size());
self.padding, let options = self.options.borrow();
self.text_size,
self.text_line_height, state.options.resize_with(options.len(), Default::default);
self.text_shaping,
self.font, let option_text = Text {
self.placeholder.as_deref(), content: "",
self.options.borrow(), 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( fn on_event(
@ -212,18 +271,98 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
update( match event {
event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
layout, | Event::Touch(touch::Event::FingerPressed { .. }) => {
cursor, let state =
shell, tree.state.downcast_mut::<State<Renderer::Paragraph>>();
self.on_select.as_ref(),
self.on_open.as_ref(), if state.is_open {
self.on_close.as_ref(), // Event wasn't processed by overlay, so cursor was clicked either outside its
self.selected.as_ref().map(Borrow::borrow), // bounds or on the drop-down, either way we close the overlay.
self.options.borrow(), state.is_open = false;
|| tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
) 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( fn mouse_interaction(
@ -234,7 +373,14 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> 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( fn draw(
@ -429,9 +575,8 @@ where
} }
} }
/// The state of a [`PickList`].
#[derive(Debug)] #[derive(Debug)]
pub struct State<P: text::Paragraph> { struct State<P: text::Paragraph> {
menu: menu::State, menu: menu::State,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
is_open: bool, is_open: bool,
@ -504,210 +649,6 @@ pub struct Icon<Font> {
pub shaping: text::Shaping, 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`]. /// The possible status of a [`PickList`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status { pub enum Status {

View file

@ -269,20 +269,29 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout::contained(limits, self.width, self.height, |limits| {
renderer, let child_limits = layout::Limits::new(
limits, Size::new(limits.min().width, limits.min().height),
self.width, Size::new(
self.height, if self.direction.horizontal().is_some() {
&self.direction, f32::INFINITY
|renderer, limits| { } else {
self.content.as_widget().layout( limits.max().width
&mut tree.children[0], },
renderer, if self.direction.vertical().is_some() {
limits, f32::MAX
) } else {
}, limits.max().height
) },
),
);
self.content.as_widget().layout(
&mut tree.children[0],
renderer,
&child_limits,
)
})
} }
fn operate( fn operate(
@ -332,28 +341,316 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
update( let state = tree.state.downcast_mut::<State>();
tree.state.downcast_mut::<State>(), let bounds = layout.bounds();
event, let cursor_over_scrollable = cursor.position_over(bounds);
layout,
cursor, let content = layout.children().next().unwrap();
clipboard, let content_bounds = content.bounds();
shell,
self.direction, let scrollbars =
&self.on_scroll, Scrollbars::new(state, self.direction, bounds, content_bounds);
|event, layout, cursor, clipboard, shell, viewport| {
self.content.as_widget_mut().on_event( let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
&mut tree.children[0], scrollbars.is_mouse_over(cursor);
event,
layout, let mut event_status = {
cursor, let cursor = match cursor_over_scrollable {
renderer, Some(cursor_position)
clipboard, 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, 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( fn draw(
@ -551,21 +848,48 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
mouse_interaction( let state = tree.state.downcast_ref::<State>();
tree.state.downcast_ref::<State>(), let bounds = layout.bounds();
layout, let cursor_over_scrollable = cursor.position_over(bounds);
cursor,
self.direction, let content_layout = layout.children().next().unwrap();
|layout, cursor, viewport| { let content_bounds = content_layout.bounds();
self.content.as_widget().mouse_interaction(
&tree.children[0], let scrollbars =
layout, Scrollbars::new(state, self.direction, bounds, content_bounds);
cursor,
viewport, let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
renderer, 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>( fn overlay<'b>(
@ -651,386 +975,6 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset)) 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>( fn notify_on_scroll<Message>(
state: &mut State, state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, 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)] #[derive(Debug, Clone, Copy)]
pub struct State { struct State {
scroll_area_touched_at: Option<Point>, scroll_area_touched_at: Option<Point>,
offset_y: Offset, offset_y: Offset,
y_scroller_grabbed_at: Option<f32>, y_scroller_grabbed_at: Option<f32>,