//! Widgets that stack in the Z-direction. use std::fmt::{self, Debug}; use std::sync::{Arc, Mutex}; use std::time::Duration; use alot::{LotId, OrderedLots}; use cushy::widget::{RootBehavior, WidgetInstance}; use figures::units::{Lp, Px, UPx}; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; use intentional::Assert; use crate::animation::easings::EaseOutQuadradic; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::utils::IgnorePoison; use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; use crate::widget::{ Callback, MakeWidget, MountedChildren, MountedWidget, Widget, WidgetId, WidgetList, WidgetRef, WrapperWidget, }; use crate::widgets::container::ContainerShadow; use crate::ConstraintLimit; /// A Z-direction stack of widgets. #[derive(Debug)] pub struct Layers { /// The children that are laid out as layers with index 0 being the lowest (bottom). pub children: Value, mounted: MountedChildren, } impl Layers { /// Returns a new instance that lays out `children` as layers. pub fn new(children: impl IntoValue) -> Self { Self { children: children.into_value(), mounted: MountedChildren::default(), } } fn synchronize_children(&mut self, context: &mut EventContext<'_>) { self.children.invalidate_when_changed(context); self.mounted.synchronize_with(&self.children, context); } } impl Widget for Layers { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.synchronize_children(&mut context.as_event_context()); for mounted in self.mounted.children() { context.for_other(mounted).redraw(); } } fn summarize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.children.map(|children| { let mut f = f.debug_tuple("Layered"); for child in children { f.field(child); } f.finish() }) } fn layout( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.synchronize_children(&mut context.as_event_context()); let mut size = Size::ZERO; for child in self.mounted.children() { size = size.max( context .for_other(child) .as_temporary() .layout(available_space), ); } // Now we know the size of the widget, we can request the widgets fill // the allocated space. 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.children() { context .for_other(child) .layout(size.map(ConstraintLimit::Fill)); context.set_child_layout(child, layout); } size } fn mounted(&mut self, context: &mut EventContext<'_>) { self.synchronize_children(context); } fn unmounted(&mut self, context: &mut EventContext<'_>) { for child in self.mounted.drain() { context.remove_child(&child); } } fn root_behavior( &mut self, context: &mut EventContext<'_>, ) -> Option<(RootBehavior, WidgetInstance)> { self.synchronize_children(context); for child in self.mounted.children() { let Some((child_behavior, next_in_chain)) = context.for_other(child).root_behavior() else { continue; }; return Some((child_behavior, next_in_chain)); } None } } /// A widget that displays other widgets relative to widgets in another layer. /// /// This widget is for use inside of a [`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, opacity: Dynamic::default(), }, } } /// Returns a new wudget that shows a `tooltip` when `content` is hovered. pub fn new_tooltip(&self, tooltip: impl MakeWidget, content: impl MakeWidget) -> Tooltipped { Tooltipped { child: WidgetRef::new(content), data: TooltipData { target_layer: self.clone(), tooltip: tooltip.make_widget(), direction: Direction::Down, shown_tooltip: Dynamic::default(), }, show_animation: None, } } /// Dismisses all currently presented overlays. pub fn dismiss_all(&self) { let mut state = self.state.lock(); state.hovering = None; let removed = state.overlays.drain().collect::>(); state.new_overlays = 0; drop(state); // Since overlays contain references back to this structure, we need to // ensure their drop implementations happen after we've dropped our // lock. drop(removed); } } impl Widget for OverlayLayer { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let state = self.state.lock(); for child in &state.overlays { let Some(mounted) = child.widget.as_mounted(context) else { continue; }; let opacity = child.opacity.get_tracking_redraw(context); let mut context = context.for_other(mounted); context.apply_opacity(opacity); context.redraw(); } } fn layout( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let mut state = self.state.lock(); state.prevent_notifications(); 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(state); // 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 } fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { let state = self.state.lock(); state.test_point(location, false, context).is_some() } 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| relative_to.find_in(context)) .and_then(|w| w.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; 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: &MountedWidget, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, relative_to: WidgetId, ) -> Option> { let direction = self.overlays[index].direction; let relative_to = relative_to.find_in(context)?.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| relative_to.find_in(context)) .and_then(|w| w.last_layout()) { if relative_to.intersects(layout) { return true; } } } false } fn layout_overlay( &mut self, index: usize, widget: &MountedWidget, 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`]. #[derive(Debug, Clone)] 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(self, id: WidgetId) -> Self { self.near(id, Direction::Left) } /// Show this overlay to the right of the specified widget. #[must_use] pub fn right_of(self, id: WidgetId) -> Self { self.near(id, Direction::Right) } /// Show this overlay to show below the specified widget. #[must_use] pub fn below(self, id: WidgetId) -> Self { self.near(id, Direction::Down) } /// Show this overlay to show above the specified widget. #[must_use] pub fn above(self, id: WidgetId) -> Self { self.near(id, Direction::Up) } /// Shows this overlay near `id` off to the `direction` side. #[must_use] pub fn near(mut self, id: WidgetId, direction: Direction) -> Self { self.layout.relative_to = Some(id); self.layout.direction = direction; self } /// Sets `callback` to be invoked once this overlay is dismissed. #[must_use] pub fn on_dismiss(mut self, callback: Callback) -> Self { self.layout.on_dismiss = Some(Arc::new(Mutex::new(callback))); self } /// Shows this overlay, returning a handle that to the displayed overlay. #[must_use] pub fn show(self) -> OverlayHandle { self.fade_in(); self.overlay.state.map_mut(|mut state| { state.new_overlays += 1; OverlayHandle { state: self.overlay.state.clone(), id: state.overlays.push(self.layout), dismiss_on_drop: true, } }) } fn fade_in(&self) { self.layout .opacity .transition_to(ZeroToOne::ONE) .over(Duration::from_millis(250)) .with_easing(EaseOutQuadradic) .launch(); } } #[derive(Debug, Clone)] struct OverlayLayout { widget: WidgetRef, opacity: Dynamic, 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 { let mut on_dismiss = on_dismiss.lock().ignore_poison(); on_dismiss.invoke(()); } } } impl Eq for OverlayLayout {} impl PartialEq for OverlayLayout { fn eq(&self, other: &Self) -> bool { self.widget == other.widget && self.opacity == other.opacity && self.relative_to == other.relative_to && self.direction == other.direction && self.requires_hover == other.requires_hover && self.layout == other.layout && match (&self.on_dismiss, &other.on_dismiss) { (Some(this), Some(other)) => Arc::ptr_eq(this, other), (None, None) => true, _ => false, } } } /// 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`]. #[derive(PartialEq, Eq)] 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() - state.new_overlays > index { let _removed = state.overlays.remove_by_index(index); } } } } impl Debug for OverlayHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("OverlayHandle") .field("id", &self.id) .field("dismiss_on_drop", &self.dismiss_on_drop) .finish_non_exhaustive() } } /// A widget that shows a tooltip when hovered. #[derive(Debug)] pub struct Tooltipped { child: WidgetRef, show_animation: Option, data: TooltipData, } #[derive(Debug, Clone)] struct TooltipData { target_layer: OverlayLayer, tooltip: WidgetInstance, direction: Direction, shown_tooltip: Dynamic>, } impl WrapperWidget for Tooltipped { fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } fn hover( &mut self, _location: Point, context: &mut EventContext<'_>, ) -> Option { let background_color = context.theme().surface.highest_container; let data = self.data.clone(); let my_id = self.child.widget().id(); self.show_animation = Some( Duration::from_millis(500) .on_complete(move || { let mut shown_tooltip = data.shown_tooltip.lock(); if shown_tooltip.is_none() { *shown_tooltip = Some( data.target_layer .build_overlay( data.tooltip .clone() .contain() .background_color(background_color) .shadow(ContainerShadow::drop(Lp::mm(1), Lp::mm(2))), ) .hide_on_unhover() .near(my_id, data.direction) .show(), ); } }) .spawn(), ); None } fn unhover(&mut self, _context: &mut EventContext<'_>) { self.show_animation = None; self.data.shown_tooltip.set(None); } }