From 2fe08fc9e9f2c28923e0151ba7d3d796dc2311f3 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 7 Dec 2023 09:51:07 -0800 Subject: [PATCH] Added hover support to OverlayLayer --- Cargo.lock | 22 ++++++++ Cargo.toml | 1 + examples/overlays.rs | 89 +++++++++++++++----------------- src/context.rs | 10 +++- src/value.rs | 10 +++- src/widgets/layers.rs | 116 +++++++++++++++++++++++++++++++++++++++--- 6 files changed, 192 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 499ed8f..bdfeb27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,7 @@ dependencies = [ "kludgine", "palette", "pollster", + "rand", "tracing", "tracing-subscriber", "unicode-segmentation", @@ -1711,6 +1712,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "presser" version = "0.3.1" @@ -1809,6 +1816,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1817,6 +1836,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "range-alloc" diff --git a/Cargo.toml b/Cargo.toml index 694f518..c7f4237 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ opt-level = 2 [dev-dependencies] pollster = "0.3.0" +rand = "0.8.5" [profile.release] debug = true diff --git a/examples/overlays.rs b/examples/overlays.rs index 3220d03..1335140 100644 --- a/examples/overlays.rs +++ b/examples/overlays.rs @@ -1,65 +1,60 @@ +use std::panic::UnwindSafe; + use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag}; -use gooey::widgets::layers::OverlayLayer; +use gooey::widgets::layers::{OverlayBuilder, OverlayLayer}; use gooey::Run; +use kludgine::Color; +use rand::{thread_rng, Rng}; fn main() -> gooey::Result { let overlay = OverlayLayer::default(); - test_widget(&overlay) + test_widget(&overlay, true) .centered() .and(overlay) .into_layers() .run() } -fn test_widget(overlay: &OverlayLayer) -> impl MakeWidget { +fn test_widget(overlay: &OverlayLayer, is_root: bool) -> impl MakeWidget { let (my_tag, my_id) = WidgetTag::new(); - let right = "Right".into_button().on_click({ - let overlay = overlay.clone(); - move |()| { - overlay - .build_overlay(test_widget(&overlay)) - .right_of(my_id) - .show() - .forget(); - } - }); - let left = "Left".into_button().on_click({ - let overlay = overlay.clone(); - move |()| { - overlay - .build_overlay(test_widget(&overlay)) - .left_of(my_id) - .show() - .forget(); - } - }); - let up = "Up".into_button().on_click({ - let overlay = overlay.clone(); - move |()| { - overlay - .build_overlay(test_widget(&overlay)) - .above(my_id) - .show() - .forget(); - } - }); - let down = "Down".into_button().on_click({ - let overlay = overlay.clone(); - move |()| { - overlay - .build_overlay(test_widget(&overlay)) - .below(my_id) - .show() - .forget(); - } - }); + let right = show_overlay_button("Right", overlay, move |overlay| overlay.right_of(my_id)); + let left = show_overlay_button("Left", overlay, move |overlay| overlay.left_of(my_id)); + let up = show_overlay_button("Up", overlay, move |overlay| overlay.above(my_id)); + let down = show_overlay_button("Down", overlay, move |overlay| overlay.below(my_id)); - up.centered() + let mut buttons = up + .centered() .and(left.and(right).into_columns()) .and(down.centered()) .into_rows() - .contain() - .pad() - .make_with_id(my_tag) + .contain(); + + if !is_root { + buttons = buttons.background_color(Color::new( + thread_rng().gen(), + thread_rng().gen(), + thread_rng().gen(), + 255, + )) + } + + buttons.pad().make_with_id(my_tag) +} + +fn show_overlay_button( + label: &str, + overlay: &OverlayLayer, + direction_func: impl for<'a> Fn(OverlayBuilder<'a>) -> OverlayBuilder<'a> + + Send + + UnwindSafe + + 'static, +) -> impl MakeWidget { + let overlay = overlay.clone(); + label.into_button().on_click(move |()| { + direction_func(overlay.build_overlay(test_widget(&overlay, false))) + .hide_on_unhover() + .show() + .forget(); + }) } diff --git a/src/context.rs b/src/context.rs index c38ba17..1b171c7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -170,7 +170,13 @@ impl<'context, 'window> EventContext<'context, 'window> { let mut cursor = None; for hover in changes.hovered.into_iter().rev() { let mut context = self.for_other(&hover); - let widget_cursor = hover.lock().as_widget().hover(location, &mut context); + let Some(last_layout) = context.last_layout() else { + continue; + }; + let widget_cursor = hover + .lock() + .as_widget() + .hover(location - last_layout.origin, &mut context); if cursor.is_none() { cursor = widget_cursor; @@ -330,7 +336,7 @@ impl<'context, 'window> EventContext<'context, 'window> { let relative = location - widget_layout.origin; if widget_context.hit_test(relative) { - widget_context.hover(relative); + widget_context.hover(location); drop(widget_context); self.cursor.widget = Some(widget.id()); break; diff --git a/src/value.rs b/src/value.rs index 2cc2692..583bbfe 100644 --- a/src/value.rs +++ b/src/value.rs @@ -536,6 +536,7 @@ impl Dynamic { DynamicGuard { guard: self.0.state().expect("deadlocked"), accessed_mut: false, + prevent_notifications: false, } } @@ -1100,6 +1101,7 @@ impl DerefMut for GenerationalValue { pub struct DynamicGuard<'a, T> { guard: DynamicMutexGuard<'a, T>, accessed_mut: bool, + prevent_notifications: bool, } impl DynamicGuard<'_, T> { @@ -1111,6 +1113,12 @@ impl DynamicGuard<'_, T> { pub fn generation(&self) -> Generation { self.guard.wrapped.generation } + + /// Prevent any access through [`DerefMut`] from triggering change + /// notifications. + pub fn prevent_notifications(&mut self) { + self.prevent_notifications = true; + } } impl<'a, T> Deref for DynamicGuard<'a, T> { @@ -1130,7 +1138,7 @@ impl<'a, T> DerefMut for DynamicGuard<'a, T> { impl Drop for DynamicGuard<'_, T> { fn drop(&mut self) { - if self.accessed_mut { + if self.accessed_mut && !self.prevent_notifications { let mut callbacks = Some(self.guard.note_changed()); run_in_bg(move || drop(callbacks.take())); } diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 9f987a6..583d388 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -9,7 +9,7 @@ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; -use crate::value::{Dynamic, Generation, IntoValue, Value}; +use crate::value::{Dynamic, DynamicGuard, Generation, IntoValue, Value}; use crate::widget::{ Children, MakeWidget, ManagedWidget, OnceCallback, Widget, WidgetId, WidgetRef, }; @@ -194,8 +194,7 @@ impl OverlayLayer { impl Widget for OverlayLayer { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - let mut guard = self.state.lock(); - let state = &mut *guard; + let state = self.state.lock(); for child in &state.overlays { let WidgetRef::Mounted(mounted) = &child.widget else { @@ -211,8 +210,8 @@ impl Widget for OverlayLayer { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let mut guard = self.state.lock(); - let state = &mut *guard; + let mut state = self.state.lock(); + state.prevent_notifications(); let available_space = available_space.map(ConstraintLimit::max); @@ -237,7 +236,7 @@ impl Widget for OverlayLayer { context.set_child_layout(&widget, layout); } - drop(guard); + drop(state); // Now that we're done mutating state, we can register for invalidation // tracking. @@ -249,15 +248,120 @@ impl Widget for OverlayLayer { // when shown. Size::ZERO } + + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + let state = self.state.lock(); + if let Some(index) = state.test_point(location, false, context) { + index > 0 + } else { + !(state.overlays.is_empty() || state.point_is_in_root_relative(location, context)) + } + } + + fn hover( + &mut self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> Option { + let mut state = self.state.lock(); + + let hovering = state.test_point(location, true, context); + if let Some(hovering) = hovering { + let should_remove = state.hovering > Some(hovering); + state.hovering = Some(hovering); + if should_remove { + remove_children_after(state, hovering); + } + } else { + state.hovering = None; + } + + None + } + + fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { + let mut state = self.state.lock(); + state.hovering = None; + + let mut remove_starting_at = None; + for (index, overlay) in state.overlays.iter().enumerate() { + if overlay.requires_hover { + remove_starting_at = Some(index); + break; + } + } + + if let Some(remove_starting_at) = remove_starting_at { + remove_children_after(state, remove_starting_at); + } + } } #[derive(Debug, Eq, PartialEq, Default)] struct OverlayState { overlays: OrderedLots, new_overlays: usize, + hovering: Option, +} + +fn remove_children_after(mut state: DynamicGuard<'_, OverlayState>, remove_starting_at: usize) { + let mut removed = Vec::with_capacity(state.overlays.len() - remove_starting_at); + while remove_starting_at < state.overlays.len() && !state.overlays.is_empty() { + removed.push(state.overlays.pop()); + state.new_overlays = state.new_overlays.saturating_sub(1); + } + drop(state); + // We delay dropping the removed widgets, as they may contain a + // reference to this OverlayLayer. + drop(removed); } impl OverlayState { + fn test_point( + &self, + location: Point, + check_original_relative: bool, + context: &mut EventContext<'_, '_>, + ) -> Option { + for (index, overlay) in self.overlays.iter().enumerate() { + if overlay.requires_hover + && !overlay + .layout + .map_or(false, |check| !check.contains(location)) + { + return Some(index + 1); + } + } + + if check_original_relative + && !self.overlays.is_empty() + && self.point_is_in_root_relative(location, context) + { + Some(0) + } else { + None + } + } + + fn point_is_in_root_relative( + &self, + location: Point, + context: &mut EventContext<'_, '_>, + ) -> bool { + if let Some(relative_to) = self + .overlays + .get_by_index(0) + .and_then(|overlay| overlay.relative_to) + .and_then(|relative_to| context.widget.for_other(&relative_to)) + .and_then(|c| c.widget().last_layout()) + { + if !relative_to.contains(location) { + return true; + } + } + false + } + fn process_new_overlays(&mut self, context: &mut EventContext<'_, '_>) { while self.new_overlays > 0 { let new_index = self.overlays.len() - self.new_overlays;