summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-10-23 21:07:45 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-11-05 23:52:57 +0100
commit7fbc195b11f9a858bcc8f56f76907af82c966c26 (patch)
tree93f325f8a42d9a960fb2b954aed1003d13e81625
parent908af3fed72131d21d7c7ffbc503c9b1e8a65144 (diff)
downloadiced-7fbc195b11f9a858bcc8f56f76907af82c966c26.tar.gz
iced-7fbc195b11f9a858bcc8f56f76907af82c966c26.tar.bz2
iced-7fbc195b11f9a858bcc8f56f76907af82c966c26.zip
Implement `reactive-rendering` for `scrollable`
-rw-r--r--widget/src/scrollable.rs625
1 files changed, 331 insertions, 294 deletions
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 528d63c1..c4350547 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -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,336 +553,388 @@ 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;
- };
+ let mut update = || {
+ 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,
- );
+ 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,
- );
+ let _ = notify_scroll(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- return event::Status::Captured;
+ 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);
+ } 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;
+ };
- let _ = notify_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
+ 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,
+ );
- return event::Status::Captured;
- }
- _ => {}
- }
- }
+ state.y_scroller_grabbed_at =
+ Some(scroller_grabbed_at);
- 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,
+ );
+ }
- let _ = notify_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
+ return event::Status::Captured;
}
-
- 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,
- );
+ 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;
+ };
- state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
+ 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,
- );
+ 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;
+ };
- 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,
+ 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;
+ }
+ }
+ _ => {}
}
- _ => mouse::Cursor::Unavailable,
- };
+ }
- let translation =
- state.translation(self.direction, bounds, content_bounds);
+ 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,
+ };
- 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
- },
- )
- };
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
- if matches!(
- event,
- Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
- | Event::Touch(
- touch::Event::FingerLifted { .. }
- | touch::Event::FingerLost { .. }
+ 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
+ },
)
- ) {
- state.scroll_area_touched_at = None;
- state.x_scroller_grabbed_at = None;
- state.y_scroller_grabbed_at = None;
+ };
- return content_status;
- }
+ 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;
- if let event::Status::Captured = content_status {
- return event::Status::Captured;
- }
+ return content_status;
+ }
- if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) =
- event
- {
- state.keyboard_modifiers = modifiers;
+ if let event::Status::Captured = content_status {
+ return event::Status::Captured;
+ }
- return event::Status::Ignored;
- }
+ if let Event::Keyboard(keyboard::Event::ModifiersChanged(
+ modifiers,
+ )) = event
+ {
+ state.keyboard_modifiers = modifiers;
- match event {
- Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- if cursor_over_scrollable.is_none() {
- return event::Status::Ignored;
- }
+ 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();
+ 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 {
+ // 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 is_vertical = match self.direction {
+ Direction::Vertical(_) => true,
+ Direction::Horizontal(_) => false,
+ Direction::Both { .. } => !is_shift_pressed,
+ };
- let in_transaction = state.last_scrolled.is_some();
+ let movement = if is_vertical {
+ Vector::new(x, y)
+ } else {
+ Vector::new(y, x)
+ };
- 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;
- };
+ // TODO: Configurable speed/friction (?)
+ -movement * 60.0
+ }
+ mouse::ScrollDelta::Pixels { x, y } => {
+ -Vector::new(x, y)
+ }
+ };
- state.scroll_area_touched_at = Some(cursor_position);
+ 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
}
- touch::Event::FingerMoved { .. } => {
- if let Some(scroll_box_touched_at) =
- state.scroll_area_touched_at
- {
+ }
+ 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;
};
- 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,
- );
}
+ 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::Captured
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
}
- Event::Window(window::Event::RedrawRequested(_)) => {
- let _ = notify_viewport(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
+ };
- 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,
}
- _ => event::Status::Ignored,
+ } 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),