From 0d34924ddfe13388472b5399a483167e41754120 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 6 Dec 2023 15:53:25 -0800 Subject: [PATCH] OverlayLayer Refs #37 --- examples/overlays.rs | 65 ++++++ src/context.rs | 8 + src/tree.rs | 18 ++ src/widget.rs | 78 ++++++- src/widgets.rs | 2 +- src/widgets/layers.rs | 461 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 examples/overlays.rs diff --git a/examples/overlays.rs b/examples/overlays.rs new file mode 100644 index 0000000..3220d03 --- /dev/null +++ b/examples/overlays.rs @@ -0,0 +1,65 @@ +use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag}; +use gooey::widgets::layers::OverlayLayer; +use gooey::Run; + +fn main() -> gooey::Result { + let overlay = OverlayLayer::default(); + + test_widget(&overlay) + .centered() + .and(overlay) + .into_layers() + .run() +} + +fn test_widget(overlay: &OverlayLayer) -> 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(); + } + }); + + up.centered() + .and(left.and(right).into_columns()) + .and(down.centered()) + .into_rows() + .contain() + .pad() + .make_with_id(my_tag) +} diff --git a/src/context.rs b/src/context.rs index 09643cf..c38ba17 100644 --- a/src/context.rs +++ b/src/context.rs @@ -937,6 +937,14 @@ impl<'context, 'window> WidgetContext<'context, 'window> { }) } + /// Returns true if `possible_parent` is in this widget's parent list. + #[must_use] + pub fn is_child_of(&self, possible_parent: &WidgetInstance) -> bool { + self.current_node + .tree + .is_child(self.current_node.node_id, possible_parent) + } + /// Returns true if this widget is enabled. #[must_use] pub const fn enabled(&self) -> bool { diff --git a/src/tree.rs b/src/tree.rs index afbca75..2744825 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -343,6 +343,24 @@ impl Tree { data.nodes.get(id).expect("missing widget").parent } + pub(crate) fn is_child(&self, mut id: LotId, possible_parent: &WidgetInstance) -> bool { + let data = self.data.lock().ignore_poison(); + while let Some(node) = data.nodes.get(id) { + if &node.widget == possible_parent { + return true; + } + + match node.parent { + Some(parent) => { + id = parent; + } + None => break, + } + } + + false + } + pub(crate) fn attach_styles(&self, id: LotId, styles: Value) { let mut data = self.data.lock().ignore_poison(); data.attach_styles(id, styles); diff --git a/src/widget.rs b/src/widget.rs index 989919b..1e3da00 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -10,6 +10,7 @@ use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, MutexGuard}; use alot::LotId; +use intentional::Assert; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; @@ -1367,6 +1368,14 @@ impl Debug for Callback { } } +impl Eq for Callback {} + +impl PartialEq for Callback { + fn eq(&self, _other: &Self) -> bool { + false + } +} + impl Callback { /// Returns a new instance that calls `function` each time the callback is /// invoked. @@ -1396,6 +1405,56 @@ where } } +/// A function that can be invoked once with a parameter (`T`) and returns `R`. +/// +/// This type is used by widgets to signal an event that can happen only onceq. +pub struct OnceCallback(Box>); + +impl Debug for OnceCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("OnceCallback") + .field(&(self as *const Self)) + .finish() + } +} + +impl Eq for OnceCallback {} + +impl PartialEq for OnceCallback { + fn eq(&self, _other: &Self) -> bool { + false + } +} + +impl OnceCallback { + /// Returns a new instance that calls `function` when the callback is + /// invoked. + pub fn new(function: F) -> Self + where + F: FnOnce(T) -> R + Send + UnwindSafe + 'static, + { + Self(Box::new(Some(function))) + } + + /// Invokes the wrapped function and returns the produced value. + pub fn invoke(mut self, value: T) -> R { + self.0.invoke(value) + } +} + +trait OnceCallbackFunction: Send + UnwindSafe { + fn invoke(&mut self, value: T) -> R; +} + +impl OnceCallbackFunction for Option +where + F: FnOnce(T) -> R + Send + UnwindSafe, +{ + fn invoke(&mut self, value: T) -> R { + (self.take().assert("invoked once"))(value) + } +} + /// A [`Widget`] that has been attached to a widget hierarchy. #[derive(Clone)] pub struct ManagedWidget { @@ -1763,10 +1822,15 @@ impl WidgetRef { } /// Returns this child, mounting it in the process if necessary. - pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget { + pub fn mount_if_needed(&mut self, context: &mut EventContext<'_, '_>) { if let WidgetRef::Unmounted(instance) = self { *self = WidgetRef::Mounted(context.push_child(instance.clone())); } + } + + /// Returns this child, mounting it in the process if necessary. + pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget { + self.mount_if_needed(context); let Self::Mounted(widget) = self else { unreachable!("just initialized") @@ -1802,6 +1866,18 @@ impl Debug for WidgetRef { } } +impl Eq for WidgetRef {} + +impl PartialEq for WidgetRef { + fn eq(&self, other: &Self) -> bool { + if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) { + this == other + } else { + self.widget() == other.widget() + } + } +} + /// The unique id of a [`WidgetInstance`]. /// /// Each [`WidgetInstance`] is guaranteed to have a unique [`WidgetId`] across diff --git a/src/widgets.rs b/src/widgets.rs index 069415e..680a9ee 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -12,7 +12,7 @@ mod expand; pub mod grid; pub mod input; pub mod label; -mod layers; +pub mod layers; mod mode_switch; pub mod progress; mod radio; diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 2b8ca75..9f987a6 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -1,12 +1,18 @@ +//! Widgets that stack in the Z-direction. + use std::fmt; +use alot::{LotId, OrderedLots}; use gooey::widget::{RootBehavior, WidgetInstance}; -use kludgine::figures::units::UPx; -use kludgine::figures::{IntoSigned, Rect, Size, Zero}; +use intentional::Assert; +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::{Generation, IntoValue, Value}; -use crate::widget::{Children, ManagedWidget, Widget}; +use crate::value::{Dynamic, Generation, IntoValue, Value}; +use crate::widget::{ + Children, MakeWidget, ManagedWidget, OnceCallback, Widget, WidgetId, WidgetRef, +}; use crate::ConstraintLimit; /// A Z-direction stack of widgets. @@ -111,7 +117,15 @@ impl Widget for Layers { // Now we know the size of the widget, we can request the widgets fill // the allocated space. - let layout = Rect::from(size).into_signed(); + let size = Size::new( + available_space + .width + .fit_measured(size.width, context.gfx.scale()), + available_space + .height + .fit_measured(size.height, context.gfx.scale()), + ); + let layout = Rect::from(size.into_signed()); for child in &self.mounted { context .for_other(child) @@ -151,3 +165,440 @@ impl Widget for Layers { None } } + +/// A widget that displays other widgets relative to widgets in another layer. +/// +/// This widget is for use inside of a [`Layers`](crate::widgets::Layers) +/// widget. +#[derive(Debug, Clone, Default)] +pub struct OverlayLayer { + state: Dynamic, +} + +impl OverlayLayer { + /// Returns a builder for a new overlay that can be shown on this layer. + pub fn build_overlay(&self, overlay: impl MakeWidget) -> OverlayBuilder<'_> { + OverlayBuilder { + overlay: self, + layout: OverlayLayout { + widget: WidgetRef::new(overlay), + relative_to: None, + direction: Direction::Right, + requires_hover: false, + on_dismiss: None, + layout: None, + }, + } + } +} + +impl Widget for OverlayLayer { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + let mut guard = self.state.lock(); + let state = &mut *guard; + + for child in &state.overlays { + let WidgetRef::Mounted(mounted) = &child.widget else { + continue; + }; + + context.for_other(mounted).redraw(); + } + } + + fn layout( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + let mut guard = self.state.lock(); + let state = &mut *guard; + + let available_space = available_space.map(ConstraintLimit::max); + + state.process_new_overlays(&mut context.as_event_context()); + + for index in 0..state.overlays.len() { + let widget = state.overlays[index] + .widget + .mounted(&mut context.as_event_context()); + let Some(layout) = state.overlays[index] + .layout + .or_else(|| state.layout_overlay(index, &widget, available_space, context)) + else { + continue; + }; + + let _ignored = context + .for_other(&widget) + .layout(layout.size.into_unsigned().map(ConstraintLimit::Fill)); + + state.overlays[index].layout = Some(layout); + context.set_child_layout(&widget, layout); + } + + drop(guard); + + // Now that we're done mutating state, we can register for invalidation + // tracking. + context.invalidate_when_changed(&self.state); + + // The overlay widget should never actualy impact the layout of other + // layers, despite what layouts its children are assigned. This may seem + // weird, but it would also be weird for a tooltop to expand its window + // when shown. + Size::ZERO + } +} + +#[derive(Debug, Eq, PartialEq, Default)] +struct OverlayState { + overlays: OrderedLots, + new_overlays: usize, +} + +impl OverlayState { + fn process_new_overlays(&mut self, context: &mut EventContext<'_, '_>) { + while self.new_overlays > 0 { + let new_index = self.overlays.len() - self.new_overlays; + self.new_overlays -= 1; + + // Determine if new_overlay is relative to an existing overlay + let new_overlay = self.overlays.get_mut_by_index(new_index).assert_expected(); + new_overlay.widget.mount_if_needed(context); + + let mut dismiss_from = 0; + if let Some(context) = new_overlay + .relative_to + .and_then(|id| context.for_other(&id)) + { + for existing in (0..new_index).rev() { + if context.is_child_of(self.overlays[existing].widget.widget()) { + // Relative to this overlay. Dismiss any overlays + // between this and the new one. + dismiss_from = existing + 1; + break; + } + } + } + + // Dismiss any overlays that are no longer going to be shown. + for index in (dismiss_from..new_index).rev() { + self.overlays.remove_by_index(index); + } + } + } + + fn layout_overlay_relative( + &mut self, + index: usize, + widget: &ManagedWidget, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + relative_to: WidgetId, + ) -> Option> { + // TODO resolving a widgetid should probably be easier + let direction = self.overlays[index].direction; + let relative_to = context + .widget + .for_other(&relative_to) + .map(|c| c.widget().clone())? + .last_layout()?; + let relative_to_unsigned = relative_to.into_unsigned(); + + let constraints = match direction { + Direction::Up => Size::new( + relative_to_unsigned.size.width, + relative_to_unsigned.origin.y, + ), + Direction::Down => Size::new( + relative_to_unsigned.size.width, + available_space.height + - relative_to_unsigned.origin.y + - relative_to_unsigned.size.height, + ), + Direction::Left => Size::new( + relative_to_unsigned.origin.x, + relative_to_unsigned.size.height, + ), + Direction::Right => Size::new( + available_space.width.saturating_sub( + relative_to_unsigned + .origin + .x + .saturating_add(relative_to_unsigned.size.width), + ), + relative_to_unsigned.size.height, + ), + }; + + let size = context + .for_other(widget) + .layout(constraints.map(ConstraintLimit::SizeToFit)) + .into_signed(); + + let mut layout_direction = direction; + let mut layout; + loop { + let origin = match layout_direction { + Direction::Up => Point::new( + relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, + relative_to.origin.y - size.height, + ), + Direction::Down => Point::new( + relative_to.origin.x + relative_to.size.width / 2 - size.width / 2, + relative_to.origin.y + relative_to.size.height, + ), + Direction::Left => Point::new( + relative_to.origin.x - size.width, + relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + ), + Direction::Right => Point::new( + relative_to.origin.x + relative_to.size.width, + relative_to.origin.y + relative_to.size.height / 2 - size.height / 2, + ), + }; + + layout = Rect::new(origin.max(Point::ZERO), size); + + let bottom_right = layout.extent(); + if bottom_right.x > available_space.width { + layout.origin.x -= bottom_right.x - available_space.width.into_signed(); + } + if bottom_right.y > available_space.height { + layout.origin.y -= bottom_right.y - available_space.height.into_signed(); + } + + if layout.intersects(&relative_to) || self.layout_intersects(index, &layout, context) { + layout_direction = layout_direction.next_clockwise(); + if layout_direction == direction { + // No layout worked optimally. + break; + } + } else { + break; + } + } + Some(layout) + } + + fn layout_intersects( + &self, + checking_index: usize, + layout: &Rect, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> bool { + for index in (0..self.overlays.len()).filter(|&i| i != checking_index) { + if self.overlays[index] + .layout + .map_or(false, |check| check.intersects(layout)) + { + return true; + } + } + + // Verify that the the popup won't also obscure the original content. + if checking_index != 0 { + if let Some(relative_to) = self.overlays[0] + .relative_to + .and_then(|relative_to| context.widget.for_other(&relative_to)) + .and_then(|c| c.widget().last_layout()) + { + if relative_to.intersects(layout) { + return true; + } + } + } + + false + } + + fn layout_overlay( + &mut self, + index: usize, + widget: &ManagedWidget, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Option> { + if let Some(relative_to) = self.overlays[index].relative_to { + self.layout_overlay_relative(index, widget, available_space, context, relative_to) + } else { + let direction = self.overlays[index].direction; + let size = context + .for_other(widget) + .layout(available_space.map(ConstraintLimit::SizeToFit)) + .into_signed(); + + let available_space = available_space.into_signed(); + + let origin = match direction { + Direction::Up => Point::new( + available_space.width / 2, + (available_space.height - size.height) / 2, + ), + Direction::Down => Point::new( + available_space.width / 2, + available_space.height / 2 + size.height / 2, + ), + Direction::Right => Point::new( + available_space.width / 2 + size.width / 2, + available_space.height / 2, + ), + Direction::Left => Point::new( + (available_space.width - size.width) / 2, + available_space.height / 2, + ), + }; + + Some(Rect::new(origin, size)) + } + } +} + +/// A builder for overlaying a widget on an [`OverlayLayer`]. +pub struct OverlayBuilder<'a> { + overlay: &'a OverlayLayer, + layout: OverlayLayout, +} + +impl OverlayBuilder<'_> { + /// Sets this overlay to hide automatically when it or its relative widget + /// are no longer hovered by the mouse cursor. + #[must_use] + pub fn hide_on_unhover(mut self) -> Self { + self.layout.requires_hover = true; + self + } + + /// Show this overlay to the left of the specified widget. + #[must_use] + pub fn left_of(mut self, id: WidgetId) -> Self { + self.layout.relative_to = Some(id); + self.layout.direction = Direction::Left; + self + } + + /// Show this overlay to the right of the specified widget. + #[must_use] + pub fn right_of(mut self, id: WidgetId) -> Self { + self.layout.relative_to = Some(id); + self.layout.direction = Direction::Right; + self + } + + /// Show this overlay to show below the specified widget. + #[must_use] + pub fn below(mut self, id: WidgetId) -> Self { + self.layout.relative_to = Some(id); + self.layout.direction = Direction::Down; + self + } + + /// Show this overlay to show above the specified widget. + #[must_use] + pub fn above(mut self, id: WidgetId) -> Self { + self.layout.relative_to = Some(id); + self.layout.direction = Direction::Up; + self + } + + /// Sets `callback` to be invoked once this overlay is dismissed. + #[must_use] + pub fn on_dismiss(mut self, callback: OnceCallback) -> Self { + self.layout.on_dismiss = Some(callback); + self + } + + /// Shows this overlay, returning a handle that to the displayed overlay. + #[must_use] + pub fn show(self) -> OverlayHandle { + self.overlay.state.map_mut(|state| { + state.new_overlays += 1; + OverlayHandle { + state: self.overlay.state.clone(), + id: state.overlays.push(self.layout), + dismiss_on_drop: true, + } + }) + } +} + +#[derive(Debug, Eq, PartialEq)] +struct OverlayLayout { + widget: WidgetRef, + relative_to: Option, + direction: Direction, + requires_hover: bool, + layout: Option>, + on_dismiss: Option, +} + +impl Drop for OverlayLayout { + fn drop(&mut self) { + if let Some(on_dismiss) = self.on_dismiss.take() { + on_dismiss.invoke(()); + } + } +} + +/// A relative direction. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Direction { + /// Negative along the Y axis. + Up, + /// Positive along the X axis. + Right, + /// Positive along the Y axis. + Down, + /// Legative along the X axis. + Left, +} + +impl Direction { + /// Returns the next direction when rotating clockwise. + #[must_use] + pub fn next_clockwise(&self) -> Self { + match self { + Direction::Up => Direction::Right, + Direction::Down => Direction::Left, + Direction::Right => Direction::Down, + Direction::Left => Direction::Up, + } + } +} + +/// A handle to an overlay that was shown in an [`OverlayLayer`]. +pub struct OverlayHandle { + state: Dynamic, + id: LotId, + dismiss_on_drop: bool, +} + +impl OverlayHandle { + /// Dismisses this overlay and any overlays that have been displayed + /// relative to it. + pub fn dismiss(self) { + drop(self); + } + + /// Drops this handle without dismissing the overlay. + pub fn forget(mut self) { + self.dismiss_on_drop = false; + drop(self); + } +} + +impl Drop for OverlayHandle { + fn drop(&mut self) { + if self.dismiss_on_drop { + let mut state = self.state.lock(); + let Some(index) = state.overlays.index_of_id(self.id) else { + return; + }; + + while state.overlays.len() > index { + let _removed = state.overlays.pop(); + } + } + } +}