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.
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>,
}

View file

@ -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;

View file

@ -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 {

View file

@ -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>,