diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 528d63c1da..c435054768 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 Message + 'a>>, class: Theme::Class<'a>, + last_status: Option, } 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),