Implement reactive-rendering for scrollable

This commit is contained in:
Héctor Ramón Jiménez 2024-10-23 21:07:45 +02:00
parent 908af3fed7
commit 7fbc195b11
No known key found for this signature in database
GPG key ID: 4C07CEC81AFA161F

View file

@ -81,6 +81,7 @@ pub struct Scrollable<
content: Element<'a, Message, Theme, Renderer>,
on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
class: Theme::Class<'a>,
last_status: Option<Status>,
}
impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer>
@ -108,6 +109,7 @@ where
content: content.into(),
on_scroll: None,
class: Theme::default(),
last_status: None,
}
.validate()
}
@ -531,6 +533,8 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
let last_offsets = (state.offset_x, state.offset_y);
if let Some(last_scrolled) = state.last_scrolled {
let clear_transaction = match event {
Event::Mouse(
@ -549,309 +553,65 @@ where
}
}
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
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,
);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
return 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);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
}
return event::Status::Captured;
}
_ => {}
}
}
if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::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,
);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
}
return event::Status::Captured;
}
_ => {}
}
} else if mouse_over_x_scrollbar {
match event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
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);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
return event::Status::Captured;
}
}
_ => {}
}
}
let content_status = if state.last_scrolled.is_some()
&& matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
{
event::Status::Ignored
} else {
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(),
content,
cursor,
renderer,
clipboard,
shell,
&Rectangle {
y: bounds.y + translation.y,
x: bounds.x + translation.x,
..bounds
},
)
};
if matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(
touch::Event::FingerLifted { .. }
| touch::Event::FingerLost { .. }
)
) {
state.scroll_area_touched_at = None;
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
return content_status;
}
if let event::Status::Captured = content_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 } => {
let is_shift_pressed = state.keyboard_modifiers.shift();
// macOS automatically inverts the axes when Shift is pressed
let (x, y) =
if cfg!(target_os = "macos") && is_shift_pressed {
(y, x)
} else {
(x, y)
};
let is_vertical = match self.direction {
Direction::Vertical(_) => true,
Direction::Horizontal(_) => false,
Direction::Both { .. } => !is_shift_pressed,
};
let movement = if is_vertical {
Vector::new(x, y)
} else {
Vector::new(y, x)
};
// TODO: Configurable speed/friction (?)
-movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => -Vector::new(x, y),
};
state.scroll(
self.direction.align(delta),
bounds,
content_bounds,
);
let has_scrolled = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
let in_transaction = state.last_scrolled.is_some();
if has_scrolled || in_transaction {
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
|| !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
{
let mut update = || {
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
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
{
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;
};
let delta = Vector::new(
scroll_box_touched_at.x - cursor_position.x,
scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
self.direction.align(delta),
state.scroll_y_to(
scrollbar.scroll_percentage_y(
scroller_grabbed_at,
cursor_position,
),
bounds,
content_bounds,
);
state.scroll_area_touched_at =
Some(cursor_position);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
return 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);
// TODO: bubble up touch movements if not consumed.
let _ = notify_scroll(
state,
&self.on_scroll,
@ -860,25 +620,321 @@ where
shell,
);
}
return event::Status::Captured;
}
_ => {}
}
event::Status::Captured
}
Event::Window(window::Event::RedrawRequested(_)) => {
let _ = notify_viewport(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
match event {
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,
);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
}
return event::Status::Captured;
}
_ => {}
}
} else if mouse_over_x_scrollbar {
match event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
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);
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
return event::Status::Captured;
}
}
_ => {}
}
}
let content_status = if state.last_scrolled.is_some()
&& matches!(
event,
Event::Mouse(mouse::Event::WheelScrolled { .. })
) {
event::Status::Ignored
} else {
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(),
content,
cursor,
renderer,
clipboard,
shell,
&Rectangle {
y: bounds.y + translation.y,
x: bounds.x + translation.x,
..bounds
},
)
};
if matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(
touch::Event::FingerLifted { .. }
| touch::Event::FingerLost { .. }
)
) {
state.scroll_area_touched_at = None;
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
return content_status;
}
_ => event::Status::Ignored,
if let event::Status::Captured = content_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 } => {
let is_shift_pressed =
state.keyboard_modifiers.shift();
// macOS automatically inverts the axes when Shift is pressed
let (x, y) = if cfg!(target_os = "macos")
&& is_shift_pressed
{
(y, x)
} else {
(x, y)
};
let is_vertical = match self.direction {
Direction::Vertical(_) => true,
Direction::Horizontal(_) => false,
Direction::Both { .. } => !is_shift_pressed,
};
let movement = if is_vertical {
Vector::new(x, y)
} else {
Vector::new(y, x)
};
// TODO: Configurable speed/friction (?)
-movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => {
-Vector::new(x, y)
}
};
state.scroll(
self.direction.align(delta),
bounds,
content_bounds,
);
let has_scrolled = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
let in_transaction = state.last_scrolled.is_some();
if has_scrolled || in_transaction {
event::Status::Captured
} else {
event::Status::Ignored
}
}
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(
scroll_box_touched_at.x - cursor_position.x,
scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
self.direction.align(delta),
bounds,
content_bounds,
);
state.scroll_area_touched_at =
Some(cursor_position);
// TODO: bubble up touch movements if not consumed.
let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
}
}
_ => {}
}
event::Status::Captured
}
Event::Window(window::Event::RedrawRequested(_)) => {
let _ = notify_viewport(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
);
event::Status::Ignored
}
_ => event::Status::Ignored,
}
};
let event_status = update();
let status = if state.y_scroller_grabbed_at.is_some()
|| state.x_scroller_grabbed_at.is_some()
{
Status::Dragged {
is_horizontal_scrollbar_dragged: state
.x_scroller_grabbed_at
.is_some(),
is_vertical_scrollbar_dragged: state
.y_scroller_grabbed_at
.is_some(),
}
} else if cursor_over_scrollable.is_some() {
Status::Hovered {
is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
}
} else {
Status::Active
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(status);
}
if last_offsets != (state.offset_x, state.offset_y)
|| self
.last_status
.is_some_and(|last_status| last_status != status)
{
shell.request_redraw(window::RedrawRequest::NextFrame);
}
event_status
}
fn draw(
@ -920,27 +976,8 @@ where
_ => mouse::Cursor::Unavailable,
};
let status = if state.y_scroller_grabbed_at.is_some()
|| state.x_scroller_grabbed_at.is_some()
{
Status::Dragged {
is_horizontal_scrollbar_dragged: state
.x_scroller_grabbed_at
.is_some(),
is_vertical_scrollbar_dragged: state
.y_scroller_grabbed_at
.is_some(),
}
} else if cursor_over_scrollable.is_some() {
Status::Hovered {
is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar,
is_vertical_scrollbar_hovered: mouse_over_y_scrollbar,
}
} else {
Status::Active
};
let style = theme.style(&self.class, status);
let style = theme
.style(&self.class, self.last_status.unwrap_or(Status::Active));
container::draw_background(renderer, &style.container, layout.bounds());
@ -1323,7 +1360,7 @@ impl operation::Scrollable for State {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
enum Offset {
Absolute(f32),
Relative(f32),