From 233b73b385e39fdacfc65e576de0ec425b4b97bb Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 28 May 2025 00:16:04 +0200 Subject: [PATCH] ui: Implement hover color for scrollbar component (#25525) This PR implements color changing for the scrollbar component based upon user mouse interaction. https://github.com/user-attachments/assets/2fd14e2d-cc5c-4272-906e-bd39bfb007e4 This PR also already adds the state for a scrollbar being actively dragged. However, as themes currently do not provide a color for this scenario, this implementation re-uses the hover color as a placeholder instead. If this feature is at all wanted, I can quickly open up a follow-up PR which adds support for that property to themes as well as this component. Release Notes: - Added hover state to scrollbars outside of the editor. --- crates/ui/src/components/scrollbar.rs | 77 ++++++++++++++++++--------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 468f90a578..9756243f44 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -14,6 +14,14 @@ pub struct Scrollbar { kind: ScrollbarAxis, } +#[derive(Default, Debug, Clone, Copy)] +enum ThumbState { + #[default] + Inactive, + Hover, + Dragging(Pixels), +} + impl ScrollableHandle for UniformListScrollHandle { fn content_size(&self) -> Size { self.0.borrow().base_handle.content_size() @@ -88,8 +96,7 @@ pub trait ScrollableHandle: Any + Debug { /// A scrollbar state that should be persisted across frames. #[derive(Clone, Debug)] pub struct ScrollbarState { - // If Some(), there's an active drag, offset by percentage from the origin of a thumb. - drag: Rc>>, + thumb_state: Rc>, parent_id: Option, scroll_handle: Arc, } @@ -97,7 +104,7 @@ pub struct ScrollbarState { impl ScrollbarState { pub fn new(scroll: impl ScrollableHandle) -> Self { Self { - drag: Default::default(), + thumb_state: Default::default(), parent_id: None, scroll_handle: Arc::new(scroll), } @@ -114,7 +121,24 @@ impl ScrollbarState { } pub fn is_dragging(&self) -> bool { - self.drag.get().is_some() + matches!(self.thumb_state.get(), ThumbState::Dragging(_)) + } + + fn set_dragging(&self, drag_offset: Pixels) { + self.set_thumb_state(ThumbState::Dragging(drag_offset)); + self.scroll_handle.drag_started(); + } + + fn set_thumb_hovered(&self, hovered: bool) { + self.set_thumb_state(if hovered { + ThumbState::Hover + } else { + ThumbState::Inactive + }); + } + + fn set_thumb_state(&self, state: ThumbState) { + self.thumb_state.set(state); } fn thumb_range(&self, axis: ScrollbarAxis) -> Option> { @@ -222,9 +246,13 @@ impl Element for Scrollbar { window.with_content_mask(Some(ContentMask { bounds }), |window| { let axis = self.kind; let colors = cx.theme().colors(); - let thumb_background = colors - .surface_background - .blend(colors.scrollbar_thumb_background); + let thumb_base_color = match self.state.thumb_state.get() { + ThumbState::Dragging(_) => colors.scrollbar_thumb_active_background, + ThumbState::Hover => colors.scrollbar_thumb_hover_background, + ThumbState::Inactive => colors.scrollbar_thumb_background, + }; + + let thumb_background = colors.surface_background.blend(thumb_base_color); let padded_bounds = Bounds::from_corners( bounds @@ -302,11 +330,9 @@ impl Element for Scrollbar { return; } - scroll.drag_started(); - if thumb_bounds.contains(&event.position) { let offset = event.position.along(axis) - thumb_bounds.origin.along(axis); - state.drag.set(Some(offset)); + state.set_dragging(offset); } else { let click_offset = compute_click_offset( event.position, @@ -332,26 +358,29 @@ impl Element for Scrollbar { let state = self.state.clone(); window.on_mouse_event(move |event: &MouseMoveEvent, _, window, cx| { - if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) { - let drag_offset = compute_click_offset( - event.position, - scroll.content_size(), - ScrollbarMouseEvent::ThumbDrag(drag_state), - ); - scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset)); - window.refresh(); - if let Some(id) = state.parent_id { - cx.notify(id); + match state.thumb_state.get() { + ThumbState::Dragging(drag_state) if event.dragging() => { + let drag_offset = compute_click_offset( + event.position, + scroll.content_size(), + ScrollbarMouseEvent::ThumbDrag(drag_state), + ); + scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset)); + window.refresh(); + if let Some(id) = state.parent_id { + cx.notify(id); + } } - } else { - state.drag.set(None); + _ => state.set_thumb_hovered(thumb_bounds.contains(&event.position)), } }); let state = self.state.clone(); let scroll = self.state.scroll_handle.clone(); - window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| { + window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| { if phase.bubble() { - state.drag.take(); + if state.is_dragging() { + state.set_thumb_hovered(thumb_bounds.contains(&event.position)); + } scroll.drag_ended(); if let Some(id) = state.parent_id { cx.notify(id);