From 723d6530e85c41c3801ba23dc5c17289c7119d41 Mon Sep 17 00:00:00 2001 From: Zoxc Date: Wed, 25 Oct 2023 22:16:40 +0200 Subject: [PATCH] Add hover effects to scroll bars (#122) --- src/app_handle.rs | 4 +- src/context.rs | 42 +++++++++++++++ src/event.rs | 18 +++++-- src/id.rs | 2 +- src/style.rs | 18 +++++++ src/view.rs | 16 ++++-- src/views/scroll.rs | 124 +++++++++++++++++++++++++++++++++++++++---- src/window_handle.rs | 33 ++++++++++-- 8 files changed, 232 insertions(+), 25 deletions(-) diff --git a/src/app_handle.rs b/src/app_handle.rs index 3355b7b0..6e2fb21e 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -129,7 +129,9 @@ impl ApplicationHandle { window_handle.pointer_move(point); } WindowEvent::CursorEntered { .. } => {} - WindowEvent::CursorLeft { .. } => {} + WindowEvent::CursorLeft { .. } => { + window_handle.pointer_leave(); + } WindowEvent::MouseWheel { delta, .. } => { window_handle.mouse_wheel(delta); } diff --git a/src/context.rs b/src/context.rs index 9dd05810..7052d28b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -667,6 +667,9 @@ pub struct LayoutCx<'a> { pub(crate) viewport: Option, pub(crate) color: Option, pub(crate) scroll_bar_color: Option, + pub(crate) scroll_bar_hover_color: Option, + pub(crate) scroll_bar_drag_color: Option, + pub(crate) scroll_bar_bg_active_color: Option, pub(crate) scroll_bar_rounded: Option, pub(crate) scroll_bar_thickness: Option, pub(crate) scroll_bar_edge_width: Option, @@ -679,6 +682,9 @@ pub struct LayoutCx<'a> { pub(crate) saved_viewports: Vec>, pub(crate) saved_colors: Vec>, pub(crate) saved_scroll_bar_colors: Vec>, + pub(crate) saved_scroll_bar_hover_colors: Vec>, + pub(crate) saved_scroll_bar_drag_colors: Vec>, + pub(crate) saved_scroll_bar_bg_active_colors: Vec>, pub(crate) saved_scroll_bar_roundeds: Vec>, pub(crate) saved_scroll_bar_thicknesses: Vec>, pub(crate) saved_scroll_bar_edge_widths: Vec>, @@ -711,10 +717,16 @@ impl<'a> LayoutCx<'a> { saved_line_heights: Vec::new(), saved_window_origins: Vec::new(), scroll_bar_color: None, + scroll_bar_hover_color: None, + scroll_bar_drag_color: None, + scroll_bar_bg_active_color: None, scroll_bar_rounded: None, scroll_bar_thickness: None, scroll_bar_edge_width: None, saved_scroll_bar_colors: Vec::new(), + saved_scroll_bar_hover_colors: Vec::new(), + saved_scroll_bar_drag_colors: Vec::new(), + saved_scroll_bar_bg_active_colors: Vec::new(), saved_scroll_bar_roundeds: Vec::new(), saved_scroll_bar_thicknesses: Vec::new(), saved_scroll_bar_edge_widths: Vec::new(), @@ -724,6 +736,9 @@ impl<'a> LayoutCx<'a> { pub(crate) fn clear(&mut self) { self.viewport = None; self.scroll_bar_color = None; + self.scroll_bar_hover_color = None; + self.scroll_bar_drag_color = None; + self.scroll_bar_bg_active_color = None; self.scroll_bar_rounded = None; self.scroll_bar_thickness = None; self.scroll_bar_edge_width = None; @@ -732,6 +747,9 @@ impl<'a> LayoutCx<'a> { self.saved_colors.clear(); self.saved_viewports.clear(); self.saved_scroll_bar_colors.clear(); + self.saved_scroll_bar_hover_colors.clear(); + self.saved_scroll_bar_drag_colors.clear(); + self.saved_scroll_bar_bg_active_colors.clear(); self.saved_scroll_bar_roundeds.clear(); self.saved_scroll_bar_thicknesses.clear(); self.saved_scroll_bar_edge_widths.clear(); @@ -747,6 +765,12 @@ impl<'a> LayoutCx<'a> { self.saved_viewports.push(self.viewport); self.saved_colors.push(self.color); self.saved_scroll_bar_colors.push(self.scroll_bar_color); + self.saved_scroll_bar_hover_colors + .push(self.scroll_bar_hover_color); + self.saved_scroll_bar_drag_colors + .push(self.scroll_bar_drag_color); + self.saved_scroll_bar_bg_active_colors + .push(self.scroll_bar_bg_active_color); self.saved_scroll_bar_roundeds.push(self.scroll_bar_rounded); self.saved_scroll_bar_thicknesses .push(self.scroll_bar_thickness); @@ -764,6 +788,12 @@ impl<'a> LayoutCx<'a> { self.viewport = self.saved_viewports.pop().unwrap_or_default(); self.color = self.saved_colors.pop().unwrap_or_default(); self.scroll_bar_color = self.saved_scroll_bar_colors.pop().unwrap_or_default(); + self.scroll_bar_hover_color = self.saved_scroll_bar_hover_colors.pop().unwrap_or_default(); + self.scroll_bar_drag_color = self.saved_scroll_bar_drag_colors.pop().unwrap_or_default(); + self.scroll_bar_bg_active_color = self + .saved_scroll_bar_bg_active_colors + .pop() + .unwrap_or_default(); self.scroll_bar_rounded = self.saved_scroll_bar_roundeds.pop().unwrap_or_default(); self.scroll_bar_thickness = self.saved_scroll_bar_thicknesses.pop().unwrap_or_default(); self.scroll_bar_edge_width = self.saved_scroll_bar_edge_widths.pop().unwrap_or_default(); @@ -787,6 +817,18 @@ impl<'a> LayoutCx<'a> { self.scroll_bar_color } + pub fn current_scroll_bar_hover_color(&self) -> Option { + self.scroll_bar_hover_color + } + + pub fn current_scroll_bar_drag_color(&self) -> Option { + self.scroll_bar_drag_color + } + + pub fn current_scroll_bar_bg_active_color(&self) -> Option { + self.scroll_bar_bg_active_color + } + pub fn current_scroll_bar_rounded(&self) -> Option { self.scroll_bar_rounded } diff --git a/src/event.rs b/src/event.rs index db550eed..77671336 100644 --- a/src/event.rs +++ b/src/event.rs @@ -49,6 +49,7 @@ pub enum Event { PointerUp(PointerInputEvent), PointerMove(PointerMoveEvent), PointerWheel(PointerWheelEvent), + PointerLeave, KeyDown(KeyEvent), KeyUp(KeyEvent), ImeEnabled, @@ -76,6 +77,7 @@ impl Event { | Event::PointerUp(_) | Event::PointerMove(_) | Event::PointerWheel(_) + | Event::PointerLeave | Event::FocusGained | Event::FocusLost | Event::ImeEnabled @@ -98,7 +100,8 @@ impl Event { Event::PointerDown(_) | Event::PointerUp(_) | Event::PointerMove(_) - | Event::PointerWheel(_) => true, + | Event::PointerWheel(_) + | Event::PointerLeave => true, Event::KeyDown(_) | Event::KeyUp(_) | Event::FocusGained @@ -145,7 +148,8 @@ impl Event { | Event::ImeCommit(_) | Event::KeyDown(_) | Event::KeyUp(_) => false, - Event::PointerMove(_) + Event::PointerLeave + | Event::PointerMove(_) | Event::ThemeChanged(_) | Event::WindowClosed | Event::WindowResized(_) @@ -163,7 +167,8 @@ impl Event { } Event::PointerMove(pointer_event) => Some(pointer_event.pos), Event::PointerWheel(pointer_event) => Some(pointer_event.pos), - Event::KeyDown(_) + Event::PointerLeave + | Event::KeyDown(_) | Event::KeyUp(_) | Event::FocusGained | Event::FocusLost @@ -195,7 +200,8 @@ impl Event { pointer_event.pos.x /= scale; pointer_event.pos.y /= scale; } - Event::KeyDown(_) + Event::PointerLeave + | Event::KeyDown(_) | Event::KeyUp(_) | Event::FocusGained | Event::FocusLost @@ -225,7 +231,8 @@ impl Event { Event::PointerWheel(pointer_event) => { pointer_event.pos -= offset; } - Event::KeyDown(_) + Event::PointerLeave + | Event::KeyDown(_) | Event::KeyUp(_) | Event::FocusGained | Event::FocusLost @@ -250,6 +257,7 @@ impl Event { Event::PointerUp(_) => Some(EventListener::PointerUp), Event::PointerMove(_) => Some(EventListener::PointerMove), Event::PointerWheel(_) => Some(EventListener::PointerWheel), + Event::PointerLeave => Some(EventListener::PointerLeave), Event::KeyDown(_) => Some(EventListener::KeyDown), Event::KeyUp(_) => Some(EventListener::KeyUp), Event::ImeEnabled => Some(EventListener::ImeEnabled), diff --git a/src/id.rs b/src/id.rs index 1c75c250..2a78229a 100644 --- a/src/id.rs +++ b/src/id.rs @@ -29,7 +29,7 @@ thread_local! { /// A stable identifier for an element. pub struct Id(u64); -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct IdPath(pub(crate) Vec); impl Id { diff --git a/src/style.rs b/src/style.rs index dc8b54c8..6db84ff4 100644 --- a/src/style.rs +++ b/src/style.rs @@ -310,6 +310,9 @@ define_styles!( background background_sv nocb: Option = None, box_shadow box_shadow_sv nocb: Option = None, scroll_bar_color scroll_bar_color_sv nocb: Option = None, + scroll_bar_hover_color scroll_bar_hover_color_sv nocb: Option = None, + scroll_bar_drag_color scroll_bar_drag_color_sv nocb: Option = None, + scroll_bar_bg_active_color scroll_bar_bg_active_color_sv nocb: Option = None, scroll_bar_rounded scroll_bar_rounded_sv nocb: Option = None, scroll_bar_thickness scroll_bar_thickness_sv nocb: Option = None, scroll_bar_edge_width scroll_bar_edge_width_sv nocb: Option = None, @@ -678,6 +681,21 @@ impl Style { self } + pub fn scroll_bar_hover_color(mut self, color: impl Into>) -> Self { + self.scroll_bar_hover_color = color.into().map(Some); + self + } + + pub fn scroll_bar_drag_color(mut self, color: impl Into>) -> Self { + self.scroll_bar_drag_color = color.into().map(Some); + self + } + + pub fn scroll_bar_bg_active_color(mut self, color: impl Into>) -> Self { + self.scroll_bar_bg_active_color = color.into().map(Some); + self + } + pub fn scroll_bar_rounded(mut self, rounded: impl Into>) -> Self { self.scroll_bar_rounded = rounded.into().map(Some); self diff --git a/src/view.rs b/src/view.rs index de49ade9..c4ce0dbe 100644 --- a/src/view.rs +++ b/src/view.rs @@ -202,6 +202,18 @@ pub trait View { if style.scroll_bar_color.is_some() { cx.scroll_bar_color = style.scroll_bar_color; } + if style.scroll_bar_hover_color.is_some() { + cx.scroll_bar_hover_color = style.scroll_bar_hover_color; + } + if style.scroll_bar_drag_color.is_some() { + cx.scroll_bar_drag_color = style.scroll_bar_drag_color; + } + if style.scroll_bar_hover_color.is_some() { + cx.scroll_bar_hover_color = style.scroll_bar_hover_color; + } + if style.scroll_bar_bg_active_color.is_some() { + cx.scroll_bar_bg_active_color = style.scroll_bar_bg_active_color; + } if style.scroll_bar_rounded.is_some() { cx.scroll_bar_rounded = style.scroll_bar_rounded; } @@ -389,9 +401,7 @@ pub trait View { // we're the parent of the event destination, so pass it on to the child if !id_path.is_empty() { if let Some(child) = self.child_mut(id_path[0]) { - if child.event_main(cx, Some(id_path), event.clone()) { - return true; - } + return child.event_main(cx, Some(id_path), event.clone()); } else { // we don't have the child, stop the event propagation return false; diff --git a/src/views/scroll.rs b/src/views/scroll.rs index 29addd32..20165d85 100644 --- a/src/views/scroll.rs +++ b/src/views/scroll.rs @@ -41,6 +41,9 @@ enum BarHeldState { pub struct ScrollBarStyle { color: Color, + hover_color: Color, + drag_color: Color, + bg_active_color: Color, rounded: bool, hide: bool, thickness: f32, @@ -48,8 +51,11 @@ pub struct ScrollBarStyle { } impl ScrollBarStyle { pub const BASE: Self = ScrollBarStyle { - // 179 is 70% of 255 so a 70% alpha factor is the default - color: Color::rgba8(0, 0, 0, 179), + // 120 is 40% of 255 so a 40% alpha factor is the default + color: Color::rgba8(0, 0, 0, 120), + hover_color: Color::rgba8(0, 0, 0, 140), + drag_color: Color::rgba8(0, 0, 0, 160), + bg_active_color: Color::rgba8(0, 0, 0, 25), rounded: cfg!(target_os = "macos"), thickness: 10., edge_width: 0., @@ -60,6 +66,14 @@ impl ScrollBarStyle { self.color = color; self } + pub fn hover_color(mut self, color: Color) -> Self { + self.hover_color = color; + self + } + pub fn drag_color(mut self, color: Color) -> Self { + self.drag_color = color; + self + } pub fn rounded(mut self, rounded: bool) -> Self { self.rounded = rounded; self @@ -85,6 +99,10 @@ pub struct Scroll { child_viewport: Rect, onscroll: Option>, held: BarHeldState, + vbar_hover: bool, + hbar_hover: bool, + vbar_whole_hover: bool, + hbar_whole_hover: bool, virtual_node: Option, propagate_pointer_wheel: bool, vertical_scroll_as_horizontal: bool, @@ -101,6 +119,10 @@ pub fn scroll(child: V) -> Scroll { child_viewport: Rect::ZERO, onscroll: None, held: BarHeldState::None, + vbar_hover: false, + hbar_hover: false, + vbar_whole_hover: false, + hbar_whole_hover: false, virtual_node: None, propagate_pointer_wheel: false, vertical_scroll_as_horizontal: false, @@ -337,8 +359,20 @@ impl Scroll { } }; - let color = self.scroll_bar_style.color; if let Some(bounds) = self.calc_vertical_bar_bounds(cx.app_state) { + let color = if let BarHeldState::Vertical(..) = self.held { + self.scroll_bar_style.drag_color + } else if self.vbar_hover { + self.scroll_bar_style.hover_color + } else { + self.scroll_bar_style.color + }; + if self.vbar_whole_hover || matches!(self.held, BarHeldState::Vertical(..)) { + let mut bounds = bounds; + bounds.y0 = self.actual_rect.y0; + bounds.y1 = self.actual_rect.y1; + cx.fill(&bounds, self.scroll_bar_style.bg_active_color, 0.0); + } let rect = (bounds - scroll_offset).inset(-edge_width / 2.0); let rect = rect.to_rounded_rect(radius(rect, true)); cx.fill(&rect, color, 0.0); @@ -349,6 +383,19 @@ impl Scroll { // Horizontal bar if let Some(bounds) = self.calc_horizontal_bar_bounds(cx.app_state) { + let color = if let BarHeldState::Horizontal(..) = self.held { + self.scroll_bar_style.drag_color + } else if self.hbar_hover { + self.scroll_bar_style.hover_color + } else { + self.scroll_bar_style.color + }; + if self.hbar_whole_hover || matches!(self.held, BarHeldState::Horizontal(..)) { + let mut bounds = bounds; + bounds.x0 = self.actual_rect.x0; + bounds.x1 = self.actual_rect.x1; + cx.fill(&bounds, self.scroll_bar_style.bg_active_color, 0.0); + } let rect = (bounds - scroll_offset).inset(-edge_width / 2.0); let rect = rect.to_rounded_rect(radius(rect, false)); cx.fill(&rect, color, 0.0); @@ -498,6 +545,31 @@ impl Scroll { fn are_bars_held(&self) -> bool { !matches!(self.held, BarHeldState::None) } + + fn update_hover_states(&mut self, app_state: &mut AppState, pos: Point) { + let scroll_offset = self.child_viewport.origin().to_vec2(); + let pos = pos + scroll_offset; + let hover = self.point_hits_vertical_bar(app_state, pos); + if self.vbar_hover != hover { + self.vbar_hover = hover; + app_state.request_layout(self.id); + } + let hover = self.point_hits_horizontal_bar(app_state, pos); + if self.hbar_hover != hover { + self.hbar_hover = hover; + app_state.request_layout(self.id); + } + let hover = self.point_within_vertical_bar(app_state, pos); + if self.vbar_whole_hover != hover { + self.vbar_whole_hover = hover; + app_state.request_layout(self.id); + } + let hover = self.point_within_horizontal_bar(app_state, pos); + if self.hbar_whole_hover != hover { + self.hbar_whole_hover = hover; + app_state.request_layout(self.id); + } + } } impl View for Scroll { @@ -571,6 +643,15 @@ impl View for Scroll { if let Some(color) = cx.scroll_bar_color { self.scroll_bar_style.color = color; } + if let Some(color) = cx.scroll_bar_hover_color { + self.scroll_bar_style.hover_color = color; + } + if let Some(color) = cx.scroll_bar_drag_color { + self.scroll_bar_style.drag_color = color; + } + if let Some(color) = cx.scroll_bar_bg_active_color { + self.scroll_bar_style.bg_active_color = color; + } if let Some(rounded) = cx.scroll_bar_rounded { self.scroll_bar_style.rounded = rounded; } @@ -643,6 +724,8 @@ impl View for Scroll { scroll_offset, ); cx.update_active(self.id); + // Force a repaint. + cx.app_state.request_layout(self.id); return true; } self.click_vertical_bar_area(cx.app_state, event.pos); @@ -662,6 +745,8 @@ impl View for Scroll { scroll_offset, ); cx.update_active(self.id); + // Force a repaint. + cx.app_state.request_layout(self.id); return true; } self.click_horizontal_bar_area(cx.app_state, event.pos); @@ -676,9 +761,18 @@ impl View for Scroll { } } } - Event::PointerUp(_event) => self.held = BarHeldState::None, + Event::PointerUp(_event) => { + if self.are_bars_held() { + self.held = BarHeldState::None; + // Force a repaint. + cx.app_state.request_layout(self.id); + } + } Event::PointerMove(event) => { if !self.scroll_bar_style.hide { + let pos = event.pos + scroll_offset; + self.update_hover_states(cx.app_state, event.pos); + if self.are_bars_held() { match self.held { BarHeldState::Vertical(offset, initial_scroll_offset) => { @@ -701,16 +795,20 @@ impl View for Scroll { } BarHeldState::None => {} } - } else { - let pos = event.pos + scroll_offset; - if self.point_within_vertical_bar(cx.app_state, pos) - || self.point_within_horizontal_bar(cx.app_state, pos) - { - return true; - } + } else if self.point_within_vertical_bar(cx.app_state, pos) + || self.point_within_horizontal_bar(cx.app_state, pos) + { + return false; } } } + Event::PointerLeave => { + self.vbar_hover = false; + self.hbar_hover = false; + self.vbar_whole_hover = false; + self.hbar_whole_hover = false; + cx.app_state.request_layout(self.id); + } _ => {} } @@ -735,6 +833,10 @@ impl View for Scroll { delta }; self.clamp_child_viewport(cx.app_state, self.child_viewport + delta); + + // Check if the scroll bars now hover + self.update_hover_states(cx.app_state, pointer_event.pos); + return !self.propagate_pointer_wheel; } diff --git a/src/window_handle.rs b/src/window_handle.rs index decad332..20b8c1c5 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -240,10 +240,12 @@ impl WindowHandle { if let Some(action) = cx.get_event_listener(*id, &EventListener::PointerEnter) { (*action)(&event); } - } else if let Some(action) = - cx.get_event_listener(*id, &EventListener::PointerLeave) - { - (*action)(&event); + } else { + let id_path = ID_PATHS.with(|paths| paths.borrow().get(id).cloned()); + if let Some(id_path) = id_path { + self.view + .event_main(&mut cx, Some(&id_path.0), Event::PointerLeave); + } } } let dragging_over = &cx.app_state.dragging_over.clone(); @@ -328,6 +330,29 @@ impl WindowHandle { } } + pub(crate) fn pointer_leave(&mut self) { + set_current_view(self.view.id()); + let mut cx = EventCx { + app_state: &mut self.app_state, + }; + let was_hovered = std::mem::take(&mut cx.app_state.hovered); + for id in was_hovered { + let view_state = cx.app_state.view_state(id); + if view_state.hover_style.is_some() + || view_state.active_style.is_some() + || view_state.animation.is_some() + { + cx.app_state.request_layout(id); + } + let id_path = ID_PATHS.with(|paths| paths.borrow().get(&id).cloned()); + if let Some(id_path) = id_path { + self.view + .event_main(&mut cx, Some(&id_path.0), Event::PointerLeave); + } + } + self.process_update(); + } + pub(crate) fn mouse_wheel(&mut self, delta: MouseScrollDelta) { let delta = match delta { MouseScrollDelta::LineDelta(x, y) => Vec2::new(-x as f64 * 60.0, -y as f64 * 60.0),