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.
This commit is contained in:
Finn Evers 2025-05-28 00:16:04 +02:00 committed by GitHub
parent 0145e2c101
commit 233b73b385
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<Pixels> {
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<Cell<Option<Pixels>>>,
thumb_state: Rc<Cell<ThumbState>>,
parent_id: Option<EntityId>,
scroll_handle: Arc<dyn ScrollableHandle>,
}
@ -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<Range<f32>> {
@ -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);