use std::rc::Rc;
use std::time::{Duration, Instant};
use teksilo_canvas::{Point, Rect, Size, Vec2};
use teksilo_tokens::Corner;
use crate::environment::LayoutDirection;
use crate::pointer::PointerId;
use crate::signal::Signal;
use crate::widget_id::WidgetId;
pub mod direction;
mod placement_impl;
mod safe_triangle;
pub mod text_affordance;
mod viewport;
pub use direction::{HorizontalSide, InlineDirection, inline_band_at, inline_edge_band};
pub use text_affordance::{OverlayBand, SelectionHandleKind};
pub use viewport::OverlayViewport;
pub(crate) use safe_triangle::point_in_safe_triangle;
pub(crate) const SAFE_REGION_BUDGET: Duration = Duration::from_millis(600);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DismissReason {
Escape,
OutsidePress,
PointerLeave,
Cascade,
Programmatic,
}
pub type OverlayDismissCallback = Rc<dyn Fn(DismissReason, &mut crate::widget::EventContext)>;
pub type OverlayPlacementSource = Rc<dyn Fn(Rect, LayoutDirection) -> OverlayPlacement>;
impl OverlayPlacement {
pub fn is_modal(&self) -> bool {
matches!(self, Self::Centered | Self::TopCenter { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OverlayId(u64);
impl OverlayId {
pub(crate) fn new(id: u64) -> Self {
Self(id)
}
}
#[derive(Debug, Clone)]
pub enum OverlayPlacement {
Below,
Above,
TrailingEdge,
AtPointer(Point),
AtPointerAvoiding {
point: Point,
avoid: Rect,
},
AboveSelection {
selection: Rect,
},
BelowSelection {
selection: Rect,
},
NearAnchor { offset: Vec2 },
Centered,
BottomCenter,
TopCenter { offset: f32 },
BelowPreferred,
ViewportCorner { corner: Corner, margin: Vec2 },
FullViewport,
}
pub const ASSUMED_CONTACT_PATCH: Size = Size {
width: 24.0,
height: 24.0,
};
impl OverlayPlacement {
pub fn at_pointer_for(point: Point, pointer: &crate::pointer::PointerInfo) -> Self {
if !pointer.kind.is_coarse() {
return OverlayPlacement::AtPointer(point);
}
let contact = pointer.axes.contact.unwrap_or(ASSUMED_CONTACT_PATCH);
let patch = Size::new(
contact.width.max(ASSUMED_CONTACT_PATCH.width),
contact.height.max(ASSUMED_CONTACT_PATCH.height),
);
OverlayPlacement::AtPointerAvoiding {
point,
avoid: rect_centred_on(point, patch),
}
}
}
pub(crate) fn rect_centred_on(point: Point, size: Size) -> Rect {
Rect::new(
point.x - size.width / 2.0,
point.y - size.height / 2.0,
size.width,
size.height,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TooltipPlacement {
#[default]
Below,
Side,
}
#[derive(Debug, Clone)]
pub enum DismissBehavior {
ClickOutside,
EscapeKey,
EscapeOrClickOutside,
PointerLeave { delay: Duration },
Manual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayLayer {
InTree,
NativePopup,
Auto,
}
#[non_exhaustive]
pub struct OverlayRequest {
pub content_id: WidgetId,
pub anchor: WidgetId,
pub placement: OverlayPlacement,
pub dismiss: DismissBehavior,
pub layer: OverlayLayer,
pub parent_overlay: Option<OverlayId>,
pub on_dismiss: Option<OverlayDismissCallback>,
pub fade_duration: Option<Duration>,
pub inert: bool,
#[doc(hidden)]
pub command_source: Option<WidgetId>,
#[doc(hidden)]
pub destroy_content_on_dismiss: bool,
}
impl OverlayRequest {
pub fn new(
content_id: WidgetId,
anchor: WidgetId,
placement: OverlayPlacement,
dismiss: DismissBehavior,
) -> Self {
Self {
content_id,
anchor,
placement,
dismiss,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
inert: false,
command_source: None,
destroy_content_on_dismiss: false,
}
}
#[doc(hidden)]
pub fn command_source(mut self, id: WidgetId) -> Self {
self.command_source = Some(id);
self
}
pub fn layer(mut self, layer: OverlayLayer) -> Self {
self.layer = layer;
self
}
pub fn parent_overlay(mut self, parent: OverlayId) -> Self {
self.parent_overlay = Some(parent);
self
}
pub fn on_dismiss(mut self, callback: OverlayDismissCallback) -> Self {
self.on_dismiss = Some(callback);
self
}
pub fn with_fade(mut self, duration: Duration) -> Self {
self.fade_duration = Some(duration);
self
}
pub fn inert(mut self) -> Self {
self.inert = true;
self
}
}
#[derive(Clone)]
pub(crate) struct OverlayFadeState {
pub opacity: Signal<f32>,
pub duration: Duration,
pub dismissing_started_real: Option<Instant>,
pub dismiss_reason: Option<DismissReason>,
pub dismissing_started_sim: Option<Instant>,
}
pub(crate) struct ActiveOverlay {
pub id: OverlayId,
pub content_id: WidgetId,
pub anchor: WidgetId,
pub placement: OverlayPlacement,
pub dismiss: DismissBehavior,
pub layer: OverlayLayer,
pub band: OverlayBand,
pub parent_overlay: Option<OverlayId>,
pub bounds: Rect,
pub focus_restore: Option<WidgetId>,
pub focus_return: Option<FocusReturn>,
pub command_source: Option<WidgetId>,
pub pointer_leave_started_real: Option<std::time::Instant>,
pub pointer_leave_started_sim: Option<std::time::Instant>,
pub safe_apex: Option<Point>,
pub safe_apex_started_real: Option<std::time::Instant>,
pub safe_apex_started_sim: Option<std::time::Instant>,
pub auto_dismiss_after: Option<Duration>,
pub paused_remaining: Option<Duration>,
pub shown_at_real: std::time::Instant,
pub shown_at_sim: std::time::Instant,
pub on_dismiss: Option<OverlayDismissCallback>,
pub fade: Option<OverlayFadeState>,
pub inert: bool,
pub placement_source: Option<OverlayPlacementSource>,
pub destroy_content: bool,
}
impl ActiveOverlay {
pub(crate) fn is_dismissing(&self) -> bool {
self.fade
.as_ref()
.is_some_and(|fade| fade.dismissing_started_real.is_some())
}
}
impl std::fmt::Debug for ActiveOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActiveOverlay")
.field("id", &self.id)
.field("content_id", &self.content_id)
.field("anchor", &self.anchor)
.field("placement", &self.placement)
.field("dismiss", &self.dismiss)
.field("layer", &self.layer)
.field("band", &self.band)
.field("parent_overlay", &self.parent_overlay)
.field("bounds", &self.bounds)
.field("focus_restore", &self.focus_restore)
.field(
"pointer_leave_started_real",
&self.pointer_leave_started_real,
)
.field("pointer_leave_started_sim", &self.pointer_leave_started_sim)
.field("safe_apex", &self.safe_apex)
.field("auto_dismiss_after", &self.auto_dismiss_after)
.field("shown_at_real", &self.shown_at_real)
.field("shown_at_sim", &self.shown_at_sim)
.field(
"on_dismiss",
&self.on_dismiss.as_ref().map(|_| "<callback>"),
)
.field("fading", &self.fade.is_some())
.field("inert", &self.inert)
.field("tracks_placement", &self.placement_source.is_some())
.finish()
}
}
pub(crate) const MAX_OVERLAY_NESTING_DEPTH: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DismissArm {
pub will_dismiss: bool,
pub suppress_beneath: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ToggleAnchor {
pub(crate) anchor: WidgetId,
pub(crate) modal: bool,
}
struct ArmedDismiss {
pointer: PointerId,
overlays: Vec<OverlayId>,
anchors: Vec<ToggleAnchor>,
}
fn anchors_of(toggles: Vec<ToggleAnchor>) -> Vec<WidgetId> {
toggles.into_iter().map(|toggle| toggle.anchor).collect()
}
pub struct OverlayManager {
pub(crate) stack: Vec<ActiveOverlay>,
arms: Vec<ArmedDismiss>,
next_id: u64,
sim_clock: Instant,
version: Signal<u64>,
pending_dismiss: Vec<(OverlayDismissCallback, DismissReason)>,
removed: std::collections::VecDeque<RemovedOverlay>,
removed_seq: u64,
pending_destroy: Vec<WidgetId>,
}
const REMOVED_OVERLAY_MEMORY: usize = 64;
#[derive(Debug, Clone, Default)]
pub(crate) struct FocusReturn {
pub target: Option<WidgetId>,
pub ancestors: Vec<WidgetId>,
pub forward: Vec<(OverlayId, WidgetId, Vec<WidgetId>)>,
}
#[derive(Debug, Clone)]
pub(crate) struct RemovedOverlay {
pub seq: u64,
pub anchor: WidgetId,
pub focus_restore: Option<WidgetId>,
pub focus_return: Option<FocusReturn>,
}
impl OverlayManager {
pub fn new() -> Self {
Self {
stack: Vec::new(),
arms: Vec::new(),
next_id: 1,
sim_clock: Instant::now(),
version: Signal::new(0),
pending_dismiss: Vec::new(),
removed: std::collections::VecDeque::new(),
removed_seq: 0,
pending_destroy: Vec::new(),
}
}
fn note_removed(&mut self, mut leaving: impl FnMut(&ActiveOverlay) -> bool) {
let first_of_this_call = self.removed_seq;
for overlay in self.stack.iter().filter(|o| leaving(o)) {
if overlay.destroy_content {
self.pending_destroy.push(overlay.content_id);
}
self.removed.push_back(RemovedOverlay {
seq: self.removed_seq,
anchor: overlay.anchor,
focus_restore: overlay.focus_restore,
focus_return: overlay.focus_return.clone(),
});
self.removed_seq += 1;
}
while self.removed.len() > REMOVED_OVERLAY_MEMORY
&& self
.removed
.front()
.is_some_and(|entry| entry.seq < first_of_this_call)
{
self.removed.pop_front();
}
}
pub(crate) fn take_pending_destroy(&mut self) -> Vec<WidgetId> {
std::mem::take(&mut self.pending_destroy)
}
pub(crate) fn removal_mark(&self) -> u64 {
self.removed_seq
}
pub(crate) fn removed_since(&self, mark: u64) -> impl Iterator<Item = &RemovedOverlay> {
self.removed.iter().filter(move |r| r.seq >= mark)
}
pub fn version(&self) -> &Signal<u64> {
&self.version
}
fn bump_version(&self) {
self.version.set(self.version.get().wrapping_add(1));
}
pub(crate) fn set_sim_clock(&mut self, now_sim: Instant) {
self.sim_clock = now_sim;
}
pub fn show(&mut self, request: OverlayRequest) -> OverlayId {
self.show_with_auto_dismiss(request, None, OverlayBand::Standard)
}
pub fn show_in_band(&mut self, request: OverlayRequest, band: OverlayBand) -> OverlayId {
self.show_with_auto_dismiss(request, None, band)
}
pub fn show_for(&mut self, request: OverlayRequest, duration: Duration) -> OverlayId {
self.show_with_auto_dismiss(request, Some(duration), OverlayBand::Standard)
}
fn show_with_auto_dismiss(
&mut self,
request: OverlayRequest,
auto_dismiss_after: Option<Duration>,
band: OverlayBand,
) -> OverlayId {
let id = OverlayId::new(self.next_id);
self.next_id += 1;
if self.ancestor_depth(request.parent_overlay) >= MAX_OVERLAY_NESTING_DEPTH {
return id;
}
let now = std::time::Instant::now();
self.pending_destroy.retain(|id| *id != request.content_id);
let overlay = ActiveOverlay {
id,
content_id: request.content_id,
anchor: request.anchor,
placement: request.placement,
dismiss: request.dismiss,
layer: request.layer,
band,
parent_overlay: request.parent_overlay,
bounds: Rect::ZERO,
focus_restore: None,
focus_return: None,
command_source: request.command_source,
pointer_leave_started_real: None,
pointer_leave_started_sim: None,
safe_apex: None,
safe_apex_started_real: None,
safe_apex_started_sim: None,
auto_dismiss_after,
paused_remaining: None,
shown_at_real: now,
shown_at_sim: now,
on_dismiss: request.on_dismiss,
fade: None,
inert: request.inert,
placement_source: None,
destroy_content: request.destroy_content_on_dismiss,
};
let at = self
.stack
.iter()
.position(|existing| existing.band > band)
.unwrap_or(self.stack.len());
self.stack.insert(at, overlay);
self.bump_version();
id
}
pub(crate) fn attach_fade(&mut self, id: OverlayId, opacity: Signal<f32>, duration: Duration) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.fade = Some(OverlayFadeState {
opacity,
duration,
dismissing_started_real: None,
dismiss_reason: None,
dismissing_started_sim: None,
});
}
}
pub fn fade_duration(&self, id: OverlayId) -> Option<Duration> {
self.stack
.iter()
.find(|o| o.id == id)
.and_then(|o| o.fade.as_ref().map(|f| f.duration))
}
pub fn next_auto_dismiss_deadline(&self) -> Option<std::time::Instant> {
self.stack
.iter()
.filter_map(|overlay| {
overlay
.auto_dismiss_after
.map(|delay| overlay.shown_at_real + delay)
})
.min()
}
pub fn next_pointer_leave_deadline(&self) -> Option<std::time::Instant> {
self.stack
.iter()
.filter_map(|overlay| {
let DismissBehavior::PointerLeave { delay } = overlay.dismiss else {
return None;
};
Some(overlay.pointer_leave_started_real? + delay)
})
.min()
}
pub fn pause_auto_dismiss(&mut self, id: OverlayId) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
&& overlay.paused_remaining.is_none()
&& let Some(delay) = overlay.auto_dismiss_after.take()
{
let elapsed = overlay.shown_at_real.elapsed();
overlay.paused_remaining = Some(delay.saturating_sub(elapsed));
}
}
pub fn resume_auto_dismiss(&mut self, id: OverlayId) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
&& let Some(remaining) = overlay.paused_remaining.take()
{
overlay.auto_dismiss_after = Some(remaining);
let now = std::time::Instant::now();
overlay.shown_at_real = now;
overlay.shown_at_sim = self.sim_clock;
}
}
pub fn is_auto_dismiss_paused(&self, id: OverlayId) -> bool {
self.stack
.iter()
.find(|o| o.id == id)
.is_some_and(|o| o.paused_remaining.is_some())
}
pub(crate) fn set_shown_at_sim(&mut self, id: OverlayId, shown_at_sim: std::time::Instant) {
if let Some(overlay) = self.stack.iter_mut().find(|overlay| overlay.id == id) {
overlay.shown_at_sim = shown_at_sim;
}
}
fn ancestor_depth(&self, parent: Option<OverlayId>) -> usize {
let mut depth = 0;
let mut current = parent;
while let Some(p) = current {
depth += 1;
if depth > self.stack.len() {
break;
}
current = self
.stack
.iter()
.find(|overlay| overlay.id == p)
.and_then(|overlay| overlay.parent_overlay);
}
depth
}
pub(crate) fn is_descendant_of(&self, child: OverlayId, ancestor: OverlayId) -> bool {
let mut current = self
.stack
.iter()
.find(|overlay| overlay.id == child)
.and_then(|overlay| overlay.parent_overlay);
while let Some(parent) = current {
if parent == ancestor {
return true;
}
current = self
.stack
.iter()
.find(|overlay| overlay.id == parent)
.and_then(|overlay| overlay.parent_overlay);
}
false
}
pub(crate) fn overlay(&self, id: OverlayId) -> Option<&ActiveOverlay> {
self.stack.iter().find(|overlay| overlay.id == id)
}
pub fn bounds_for(&self, id: OverlayId) -> Option<Rect> {
self.overlay(id).map(|o| o.bounds)
}
pub(crate) fn topmost_modal(&self) -> Option<&ActiveOverlay> {
self.stack
.iter()
.rev()
.find(|overlay| overlay.placement.is_modal())
}
pub(crate) fn is_modal_scrim(&self, overlay: &ActiveOverlay) -> bool {
matches!(overlay.placement, OverlayPlacement::FullViewport)
&& overlay
.parent_overlay
.and_then(|parent| self.overlay(parent))
.is_some_and(|parent| parent.placement.is_modal())
}
pub(crate) fn has_open_modal(&self) -> bool {
self.stack
.iter()
.any(|overlay| overlay.placement.is_modal() && !overlay.is_dismissing())
}
pub fn dismiss_with_focus_restore(
&mut self,
id: OverlayId,
) -> (Vec<WidgetId>, Option<WidgetId>) {
self.dismiss_with_focus_restore_because(id, DismissReason::Programmatic)
}
pub fn dismiss_with_focus_restore_because(
&mut self,
id: OverlayId,
reason: DismissReason,
) -> (Vec<WidgetId>, Option<WidgetId>) {
let focus_restore = self
.stack
.iter()
.find(|overlay| overlay.id == id)
.and_then(|overlay| overlay.focus_restore);
let dismissed = self.dismiss_because(id, reason);
(dismissed, focus_restore)
}
pub fn dismiss_descendants_of(
&mut self,
parent: OverlayId,
preserve: Option<OverlayId>,
) -> (Vec<WidgetId>, Option<WidgetId>) {
let mut to_dismiss = Vec::new();
for overlay in &self.stack {
if !self.is_descendant_of(overlay.id, parent) {
continue;
}
if preserve
.is_some_and(|keep| overlay.id == keep || self.is_descendant_of(overlay.id, keep))
{
continue;
}
to_dismiss.push(overlay.id);
}
if to_dismiss.is_empty() {
return (Vec::new(), None);
}
let focus_restore = self
.stack
.iter()
.rev()
.find(|overlay| to_dismiss.contains(&overlay.id))
.and_then(|overlay| overlay.focus_restore);
let dismissed_content: Vec<WidgetId> = self
.stack
.iter()
.filter(|overlay| to_dismiss.contains(&overlay.id))
.map(|overlay| overlay.content_id)
.collect();
let callbacks: Vec<OverlayDismissCallback> = self
.stack
.iter()
.filter(|overlay| to_dismiss.contains(&overlay.id))
.filter_map(|overlay| overlay.on_dismiss.clone())
.collect();
self.note_removed(|overlay| to_dismiss.contains(&overlay.id));
self.stack
.retain(|overlay| !to_dismiss.contains(&overlay.id));
self.pending_dismiss
.extend(callbacks.into_iter().map(|cb| (cb, DismissReason::Cascade)));
(dismissed_content, focus_restore)
}
pub fn update_placement(&mut self, id: OverlayId, placement: OverlayPlacement) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.placement = placement;
}
}
pub(crate) fn set_placement_source(&mut self, id: OverlayId, source: OverlayPlacementSource) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.placement_source = Some(source);
}
}
pub fn set_parent_overlay(&mut self, id: OverlayId, parent: Option<OverlayId>) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.parent_overlay = parent;
}
}
pub fn dismiss(&mut self, id: OverlayId) -> Vec<WidgetId> {
self.dismiss_because(id, DismissReason::Programmatic)
}
pub fn dismiss_because(&mut self, id: OverlayId, reason: DismissReason) -> Vec<WidgetId> {
let sim_now = self.sim_clock;
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id)
&& let Some(fade) = &mut overlay.fade
&& fade.dismissing_started_real.is_none()
{
let _ = fade
.opacity
.try_animate_with_options(crate::animation::AnimationRequest {
target: 0.0,
duration: fade.duration,
easing: teksilo_tokens::Easing::EaseOut,
frame_interval: None,
looping: false,
epsilon: 0.0,
max_duration: None,
});
let now_real = Instant::now();
fade.dismissing_started_real = Some(now_real);
fade.dismissing_started_sim = Some(sim_now);
fade.dismiss_reason = Some(reason);
return Vec::new();
}
self.dismiss_immediate(id, reason)
}
pub(crate) fn dismiss_immediate(
&mut self,
id: OverlayId,
reason: DismissReason,
) -> Vec<WidgetId> {
let mut to_dismiss = vec![id];
let mut i = 0;
while i < to_dismiss.len() {
let parent = to_dismiss[i];
for overlay in &self.stack {
if overlay.parent_overlay == Some(parent) && !to_dismiss.contains(&overlay.id) {
to_dismiss.push(overlay.id);
}
}
i += 1;
}
let dismissed_content: Vec<WidgetId> = self
.stack
.iter()
.filter(|o| to_dismiss.contains(&o.id))
.map(|o| o.content_id)
.collect();
let callbacks: Vec<OverlayDismissCallback> = self
.stack
.iter()
.filter(|o| to_dismiss.contains(&o.id))
.filter_map(|o| o.on_dismiss.clone())
.collect();
self.note_removed(|o| to_dismiss.contains(&o.id));
self.stack.retain(|o| !to_dismiss.contains(&o.id));
if !to_dismiss.is_empty() {
self.bump_version();
}
let mut callbacks = callbacks.into_iter();
if let Some(first) = callbacks.next() {
self.pending_dismiss.push((first, reason));
}
self.pending_dismiss
.extend(callbacks.map(|cb| (cb, DismissReason::Cascade)));
dismissed_content
}
pub fn process_pending_fade_dismissals(
&mut self,
now: Instant,
) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
self.process_pending_fade_dismissals_with(|fade| {
let started = fade.dismissing_started_real?;
Some(now.saturating_duration_since(started) >= fade.duration)
})
}
pub fn process_pending_fade_dismissals_sim(
&mut self,
now_sim: Instant,
) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
self.process_pending_fade_dismissals_with(|fade| {
let started = fade.dismissing_started_sim?;
Some(now_sim.saturating_duration_since(started) >= fade.duration)
})
}
fn process_pending_fade_dismissals_with(
&mut self,
mut elapsed_done: impl FnMut(&OverlayFadeState) -> Option<bool>,
) -> Vec<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
let ready: Vec<(OverlayId, Option<WidgetId>, Option<DismissReason>)> = self
.stack
.iter()
.filter_map(|o| {
let fade = o.fade.as_ref()?;
if elapsed_done(fade)? {
Some((o.id, o.focus_restore, fade.dismiss_reason))
} else {
None
}
})
.collect();
ready
.into_iter()
.map(|(id, focus_restore, reason)| {
let dismissed =
self.dismiss_immediate(id, reason.unwrap_or(DismissReason::Programmatic));
(id, dismissed, focus_restore)
})
.collect()
}
pub fn next_fade_dismiss_deadline(&self) -> Option<Instant> {
self.stack
.iter()
.filter_map(|o| {
let fade = o.fade.as_ref()?;
let started = fade.dismissing_started_real?;
Some(started + fade.duration)
})
.min()
}
pub fn dismiss_top(&mut self) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
self.dismiss_top_because(DismissReason::Programmatic)
}
pub fn dismiss_top_because(
&mut self,
reason: DismissReason,
) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
if let Some(overlay) = self.stack.last() {
let id = overlay.id;
let focus_restore = overlay.focus_restore;
let content_ids = self.dismiss_because(id, reason);
Some((id, content_ids, focus_restore))
} else {
None
}
}
pub fn try_dismiss_top_on_escape(
&mut self,
) -> Option<(OverlayId, Vec<WidgetId>, Option<WidgetId>)> {
let (target, _) = self.top_escape_target()?;
let focus_restore = self
.stack
.iter()
.find(|o| o.id == target)
.and_then(|o| o.focus_restore);
let content_ids = self.dismiss_because(target, DismissReason::Escape);
Some((target, content_ids, focus_restore))
}
pub(crate) fn top_escape_target(&self) -> Option<(OverlayId, WidgetId)> {
self.stack
.iter()
.rev()
.filter(|o| !o.is_dismissing() && !o.inert)
.find_map(|o| match o.dismiss {
DismissBehavior::EscapeKey
| DismissBehavior::EscapeOrClickOutside
| DismissBehavior::PointerLeave { .. } => Some(Some((o.id, o.content_id))),
DismissBehavior::Manual => Some(None),
DismissBehavior::ClickOutside => None,
})?
}
pub(crate) fn dismiss_inert_on_escape(&mut self) -> Vec<WidgetId> {
let targets: Vec<OverlayId> = self
.stack
.iter()
.filter(|o| o.inert && !o.is_dismissing())
.filter(|o| {
matches!(
o.dismiss,
DismissBehavior::EscapeKey
| DismissBehavior::EscapeOrClickOutside
| DismissBehavior::PointerLeave { .. }
)
})
.map(|o| o.id)
.collect();
targets
.into_iter()
.flat_map(|id| self.dismiss_because(id, DismissReason::Escape))
.collect()
}
pub fn set_top_focus_restore(&mut self, focus_restore: WidgetId) {
if let Some(overlay) = self.stack.last_mut()
&& !overlay.inert
{
overlay.focus_restore = Some(focus_restore);
}
}
pub fn dismiss_all(&mut self) -> Vec<WidgetId> {
self.dismiss_all_because(DismissReason::Programmatic)
}
pub fn dismiss_all_because(&mut self, reason: DismissReason) -> Vec<WidgetId> {
let content_ids: Vec<WidgetId> = self.stack.iter().map(|o| o.content_id).collect();
if content_ids.is_empty() {
return content_ids;
}
let callbacks: Vec<OverlayDismissCallback> = self
.stack
.iter()
.filter_map(|o| o.on_dismiss.clone())
.collect();
self.note_removed(|_| true);
self.stack.clear();
self.bump_version();
self.pending_dismiss
.extend(callbacks.into_iter().map(|cb| (cb, reason)));
content_ids
}
pub fn dismiss_except(&mut self, keep: &std::collections::HashSet<WidgetId>) -> Vec<WidgetId> {
self.dismiss_except_because(keep, DismissReason::Programmatic)
}
pub fn dismiss_except_because(
&mut self,
keep: &std::collections::HashSet<WidgetId>,
reason: DismissReason,
) -> Vec<WidgetId> {
let dismissed: Vec<WidgetId> = self
.stack
.iter()
.filter(|o| !keep.contains(&o.content_id))
.map(|o| o.content_id)
.collect();
if dismissed.is_empty() {
return dismissed;
}
let callbacks: Vec<OverlayDismissCallback> = self
.stack
.iter()
.filter(|o| !keep.contains(&o.content_id))
.filter_map(|o| o.on_dismiss.clone())
.collect();
self.note_removed(|o| !keep.contains(&o.content_id));
self.stack.retain(|o| keep.contains(&o.content_id));
self.bump_version();
self.pending_dismiss
.extend(callbacks.into_iter().map(|cb| (cb, reason)));
dismissed
}
pub(crate) fn take_pending_dismiss(&mut self) -> Vec<(OverlayDismissCallback, DismissReason)> {
std::mem::take(&mut self.pending_dismiss)
}
pub fn is_empty(&self) -> bool {
self.stack.is_empty()
}
pub fn len(&self) -> usize {
self.stack.len()
}
pub fn active_content_ids(&self) -> Vec<WidgetId> {
self.stack.iter().map(|o| o.content_id).collect()
}
pub fn active_ids(&self) -> Vec<OverlayId> {
self.stack
.iter()
.filter(|o| {
o.fade
.as_ref()
.is_none_or(|f| f.dismissing_started_real.is_none())
})
.map(|o| o.id)
.collect()
}
pub fn anchor_for(&self, id: OverlayId) -> Option<WidgetId> {
self.stack.iter().find(|o| o.id == id).map(|o| o.anchor)
}
pub fn interactive_rects(&self) -> Vec<Rect> {
self.interactive()
.map(|o| o.bounds)
.filter(|r| r.width > 0.0 && r.height > 0.0)
.collect()
}
pub(crate) fn interactive(&self) -> impl Iterator<Item = &ActiveOverlay> {
self.stack.iter().filter(|o| {
!o.inert
&& o.fade
.as_ref()
.is_none_or(|f| f.dismissing_started_real.is_none())
})
}
#[allow(dead_code)] pub(crate) fn topmost(&self) -> Option<&ActiveOverlay> {
self.stack.last()
}
pub fn hit_test(&self, point: Point) -> Option<OverlayId> {
for overlay in self.stack.iter().rev() {
let fading_out = overlay
.fade
.as_ref()
.is_some_and(|f| f.dismissing_started_real.is_some());
if !fading_out && !overlay.inert && overlay.bounds.contains(point) {
return Some(overlay.id);
}
}
None
}
pub fn handle_click_outside(
&mut self,
point: Point,
) -> (Vec<WidgetId>, Option<WidgetId>, Vec<WidgetId>) {
self.dismiss_outside_press(point, &[])
}
pub fn dismiss_outside_press(
&mut self,
point: Point,
busy: &[Point],
) -> (Vec<WidgetId>, Option<WidgetId>, Vec<WidgetId>) {
let (dismissed, focus_restore, toggles) = self.dismiss_outside_press_toggles(point, busy);
(dismissed, focus_restore, anchors_of(toggles))
}
pub(crate) fn dismiss_outside_press_toggles(
&mut self,
point: Point,
busy: &[Point],
) -> (Vec<WidgetId>, Option<WidgetId>, Vec<ToggleAnchor>) {
let (to_dismiss, toggle_anchors) = self.outside_press_targets(point, busy);
self.apply_outside_press(to_dismiss, toggle_anchors)
}
fn outside_press_targets(
&self,
point: Point,
busy: &[Point],
) -> (Vec<OverlayId>, Vec<ToggleAnchor>) {
if self.stack.is_empty() {
return (Vec::new(), Vec::new());
}
let index_at = |p: Point| {
self.stack.iter().enumerate().rev().find_map(|(i, o)| {
let fading_out = o
.fade
.as_ref()
.is_some_and(|f| f.dismissing_started_real.is_some());
(!fading_out && !o.inert && o.bounds.contains(p)).then_some(i)
})
};
let hit_index = std::iter::once(point)
.chain(busy.iter().copied())
.filter_map(index_at)
.max();
let mut to_dismiss: Vec<OverlayId> = Vec::new();
let mut toggle_anchors: Vec<ToggleAnchor> = Vec::new();
for (i, o) in self.stack.iter().enumerate() {
if hit_index.is_some_and(|k| i <= k) {
continue;
}
if !o.band.dismissed_by_outside_press() {
continue;
}
match o.dismiss {
DismissBehavior::ClickOutside | DismissBehavior::EscapeOrClickOutside => {
to_dismiss.push(o.id);
toggle_anchors.push(ToggleAnchor {
anchor: o.anchor,
modal: o.placement.is_modal(),
});
}
DismissBehavior::PointerLeave { .. } => to_dismiss.push(o.id),
DismissBehavior::EscapeKey | DismissBehavior::Manual => {}
}
}
(to_dismiss, toggle_anchors)
}
fn apply_outside_press(
&mut self,
to_dismiss: Vec<OverlayId>,
toggle_anchors: Vec<ToggleAnchor>,
) -> (Vec<WidgetId>, Option<WidgetId>, Vec<ToggleAnchor>) {
if to_dismiss.is_empty() {
return (Vec::new(), None, Vec::new());
}
let focus_restore = self
.stack
.iter()
.find(|o| to_dismiss.contains(&o.id))
.and_then(|o| o.focus_restore);
let mut all_dismissed = Vec::new();
for id in to_dismiss {
all_dismissed.extend(self.dismiss_because(id, DismissReason::OutsidePress));
}
(all_dismissed, focus_restore, toggle_anchors)
}
pub fn arm_dismiss(&mut self, pointer: PointerId, point: Point, busy: &[Point]) -> DismissArm {
self.abort_dismiss(pointer);
let (overlays, anchors) = self.outside_press_targets(point, busy);
if overlays.is_empty() {
return DismissArm::default();
}
self.arms.push(ArmedDismiss {
pointer,
overlays,
anchors,
});
DismissArm {
will_dismiss: true,
suppress_beneath: true,
}
}
pub fn commit_dismiss(
&mut self,
pointer: PointerId,
point: Point,
) -> (Vec<WidgetId>, Option<WidgetId>, Vec<WidgetId>) {
let Some(index) = self.arms.iter().position(|a| a.pointer == pointer) else {
return (Vec::new(), None, Vec::new());
};
let arm = self.arms.remove(index);
let (still_outside, _) = self.outside_press_targets(point, &[]);
let to_dismiss: Vec<OverlayId> = arm
.overlays
.into_iter()
.filter(|id| still_outside.contains(id))
.collect();
let anchors = if to_dismiss.is_empty() {
Vec::new()
} else {
arm.anchors
};
let (dismissed, focus_restore, toggles) = self.apply_outside_press(to_dismiss, anchors);
(dismissed, focus_restore, anchors_of(toggles))
}
pub fn abort_dismiss(&mut self, pointer: PointerId) -> bool {
let before = self.arms.len();
self.arms.retain(|a| a.pointer != pointer);
self.arms.len() != before
}
pub fn has_armed_dismiss(&self, pointer: PointerId) -> bool {
self.arms.iter().any(|a| a.pointer == pointer)
}
pub fn armed_pointers(&self) -> Vec<PointerId> {
self.arms.iter().map(|a| a.pointer).collect()
}
pub fn set_content_bounds(&mut self, id: OverlayId, size: Size) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.bounds = Rect::new(overlay.bounds.x, overlay.bounds.y, size.width, size.height);
}
}
pub fn find_by_content(&self, content_id: WidgetId) -> Option<OverlayId> {
self.stack
.iter()
.find(|o| o.content_id == content_id)
.map(|o| o.id)
}
pub fn bounds_for_content(&self, content_id: WidgetId) -> Option<Rect> {
self.stack
.iter()
.find(|o| o.content_id == content_id)
.map(|o| o.bounds)
}
pub(crate) fn arm_safe_region(
&mut self,
content_id: WidgetId,
apex: Point,
real_now: Instant,
sim_now: Instant,
) {
if let Some(overlay) = self
.stack
.iter_mut()
.find(|o| o.content_id == content_id && !o.is_dismissing())
{
overlay.safe_apex = Some(apex);
overlay.safe_apex_started_real = Some(real_now);
overlay.safe_apex_started_sim = Some(sim_now);
}
}
pub(crate) fn clear_safe_region(&mut self, id: OverlayId) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.safe_apex = None;
overlay.safe_apex_started_real = None;
overlay.safe_apex_started_sim = None;
}
}
pub(crate) fn safe_apex_for_content(&self, content_id: WidgetId) -> Option<Point> {
self.stack
.iter()
.find(|o| o.content_id == content_id)
.and_then(|o| o.safe_apex)
}
pub(crate) fn point_in_safe_region(&self, id: OverlayId, point: Point) -> bool {
self.stack
.iter()
.find(|o| o.id == id)
.and_then(|o| o.safe_apex.map(|apex| (apex, o.bounds)))
.is_some_and(|(apex, bounds)| point_in_safe_triangle(point, apex, bounds))
}
pub fn set_dismiss(&mut self, id: OverlayId, behavior: DismissBehavior) {
if let Some(overlay) = self.stack.iter_mut().find(|o| o.id == id) {
overlay.dismiss = behavior;
overlay.pointer_leave_started_real = None;
overlay.pointer_leave_started_sim = None;
}
}
}
impl Default for OverlayManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for OverlayManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OverlayManager")
.field("active_count", &self.stack.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::KeyData;
pub(super) fn fake_id(n: u64) -> WidgetId {
KeyData::from_ffi(n).into()
}
#[test]
fn a_new_request_takes_the_documented_defaults_and_each_builder_sets_its_field() {
let request = OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::TrailingEdge,
DismissBehavior::EscapeKey,
);
assert_eq!(request.content_id, fake_id(10));
assert_eq!(request.anchor, fake_id(1));
assert!(matches!(request.placement, OverlayPlacement::TrailingEdge));
assert!(matches!(request.dismiss, DismissBehavior::EscapeKey));
assert_eq!(request.layer, OverlayLayer::InTree);
assert_eq!(request.parent_overlay, None);
assert!(request.on_dismiss.is_none());
assert_eq!(request.fade_duration, None);
assert!(!request.inert);
let callback: OverlayDismissCallback = Rc::new(|_, _| {});
let request = request.layer(OverlayLayer::NativePopup);
assert_eq!(request.layer, OverlayLayer::NativePopup);
let request = request.parent_overlay(OverlayId::new(7));
assert_eq!(request.parent_overlay, Some(OverlayId::new(7)));
let request = request.on_dismiss(callback.clone());
assert!(
request
.on_dismiss
.as_ref()
.is_some_and(|installed| Rc::ptr_eq(installed, &callback)),
);
let request = request.with_fade(Duration::from_millis(150));
assert_eq!(request.fade_duration, Some(Duration::from_millis(150)));
let request = request.inert();
assert!(request.inert);
assert_eq!(request.content_id, fake_id(10));
assert_eq!(request.anchor, fake_id(1));
assert!(matches!(request.placement, OverlayPlacement::TrailingEdge));
assert!(matches!(request.dismiss, DismissBehavior::EscapeKey));
assert_eq!(request.layer, OverlayLayer::NativePopup);
assert_eq!(request.parent_overlay, Some(OverlayId::new(7)));
assert!(request.on_dismiss.is_some());
assert_eq!(request.fade_duration, Some(Duration::from_millis(150)));
}
#[test]
fn dismiss_all_fires_on_dismiss_callbacks() {
use std::rc::Rc;
let mut mgr = OverlayManager::new();
let cb_a: OverlayDismissCallback = Rc::new(|_, _| {});
let cb_b: OverlayDismissCallback = Rc::new(|_, _| {});
mgr.show(
OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
)
.on_dismiss(cb_a),
);
mgr.show(
OverlayRequest::new(
fake_id(11),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
)
.on_dismiss(cb_b),
);
let dismissed = mgr.dismiss_all();
assert_eq!(dismissed.len(), 2);
assert!(mgr.is_empty());
let parked = mgr.take_pending_dismiss();
assert_eq!(
parked.len(),
2,
"both overlays' on_dismiss must be parked for the tree to run",
);
assert!(
parked
.iter()
.all(|(_, reason)| *reason == DismissReason::Programmatic),
"`dismiss_all` is the application asking, so that is what the \
callbacks are told",
);
assert!(
mgr.take_pending_dismiss().is_empty(),
"draining is not repeatable — a second read must not re-run them",
);
}
#[test]
fn show_and_dismiss() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
assert_eq!(mgr.len(), 1);
mgr.dismiss(id);
assert!(mgr.is_empty());
}
#[test]
fn cascade_dismissal() {
let mut mgr = OverlayManager::new();
let parent = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
let _child = mgr.show(
OverlayRequest::new(
fake_id(11),
fake_id(10),
OverlayPlacement::TrailingEdge,
DismissBehavior::ClickOutside,
)
.parent_overlay(parent),
);
assert_eq!(mgr.len(), 2);
mgr.dismiss(parent);
assert!(mgr.is_empty());
}
#[test]
fn cascade_depth_is_bounded() {
let mut mgr = OverlayManager::new();
let mut parent = mgr.show(OverlayRequest::new(
fake_id(100),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
for i in 1..MAX_OVERLAY_NESTING_DEPTH {
parent = mgr.show(
OverlayRequest::new(
fake_id(100 + i as u64),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
)
.parent_overlay(parent),
);
}
assert_eq!(
mgr.len(),
MAX_OVERLAY_NESTING_DEPTH,
"chain should fill exactly to the cap"
);
let dropped = mgr.show(
OverlayRequest::new(
fake_id(999),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
)
.parent_overlay(parent),
);
assert_eq!(
mgr.len(),
MAX_OVERLAY_NESTING_DEPTH,
"over-cap overlay must not be pushed"
);
assert!(
mgr.stack.iter().all(|o| o.id != dropped),
"the dropped overlay id must not appear in the stack"
);
}
#[test]
fn dismiss_top() {
let mut mgr = OverlayManager::new();
let _a = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
let b = mgr.show(OverlayRequest::new(
fake_id(11),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
let dismissed = mgr.dismiss_top();
assert_eq!(dismissed.map(|(id, _, _)| id), Some(b));
assert_eq!(mgr.len(), 1);
}
#[test]
fn click_outside_dismisses() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
let id = mgr.active_ids()[0];
mgr.set_content_bounds(id, Size::new(100.0, 50.0));
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(50.0, 25.0));
assert!(dismissed.is_empty());
assert_eq!(mgr.len(), 1);
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert!(!dismissed.is_empty());
assert!(mgr.is_empty());
}
#[test]
fn click_outside_returns_focus_restore() {
let mut mgr = OverlayManager::new();
let trigger = fake_id(99);
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
let id = mgr.active_ids()[0];
mgr.set_content_bounds(id, Size::new(100.0, 50.0));
mgr.set_top_focus_restore(trigger);
let (dismissed, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert_eq!(dismissed.len(), 1);
assert_eq!(focus_restore, Some(trigger));
}
#[test]
fn click_outside_focus_restore_picks_bottommost() {
let mut mgr = OverlayManager::new();
let pre_overlay_focus = fake_id(99);
let inside_a = fake_id(50);
let a = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
mgr.set_content_bounds(a, Size::new(100.0, 50.0));
mgr.set_top_focus_restore(pre_overlay_focus);
let b = mgr.show(OverlayRequest::new(
fake_id(11),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
mgr.set_content_bounds(b, Size::new(100.0, 50.0));
mgr.set_top_focus_restore(inside_a);
let (_, focus_restore, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert_eq!(focus_restore, Some(pre_overlay_focus));
}
#[test]
fn manual_dismiss_ignores_click_outside() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert!(dismissed.is_empty());
assert_eq!(mgr.len(), 1);
}
#[test]
fn escape_dismisses_escape_or_click_outside() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::EscapeOrClickOutside,
));
let dismissed = mgr.try_dismiss_top_on_escape();
assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
assert!(mgr.is_empty());
}
#[test]
fn escape_dismisses_escape_key_only() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::EscapeKey,
));
let dismissed = mgr.try_dismiss_top_on_escape();
assert_eq!(dismissed.map(|(oid, _, _)| oid), Some(id));
assert!(mgr.is_empty());
}
#[test]
fn escape_does_not_dismiss_click_outside_only() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
));
assert!(mgr.try_dismiss_top_on_escape().is_none());
assert_eq!(mgr.len(), 1);
}
#[test]
fn escape_does_not_dismiss_manual() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
assert!(mgr.try_dismiss_top_on_escape().is_none());
assert_eq!(mgr.len(), 1);
}
#[test]
fn click_outside_dismisses_escape_or_click_outside() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::EscapeOrClickOutside,
));
let id = mgr.active_ids()[0];
mgr.set_content_bounds(id, Size::new(100.0, 50.0));
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert!(!dismissed.is_empty());
assert!(mgr.is_empty());
}
#[test]
fn click_outside_reports_click_opened_anchors_only() {
let mut mgr = OverlayManager::new();
let click_anchor = fake_id(1);
let hover_anchor = fake_id(2);
let click_overlay = mgr.show(OverlayRequest::new(
fake_id(10),
click_anchor,
OverlayPlacement::Below,
DismissBehavior::EscapeOrClickOutside,
));
mgr.set_content_bounds(click_overlay, Size::new(100.0, 50.0));
let hover_overlay = mgr.show(OverlayRequest::new(
fake_id(11),
hover_anchor,
OverlayPlacement::Below,
DismissBehavior::PointerLeave {
delay: std::time::Duration::from_millis(150),
},
));
mgr.set_content_bounds(hover_overlay, Size::new(100.0, 50.0));
let (dismissed, _focus, toggle_anchors) =
mgr.handle_click_outside(Point::new(500.0, 500.0));
assert_eq!(dismissed.len(), 2);
assert!(mgr.is_empty());
assert_eq!(toggle_anchors, vec![click_anchor]);
}
#[test]
fn click_outside_does_not_dismiss_escape_key_only() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::EscapeKey,
));
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(500.0, 500.0));
assert!(dismissed.is_empty());
assert_eq!(mgr.len(), 1);
}
#[test]
fn click_outside_is_layered_over_a_modal() {
fn set_bounds(mgr: &mut OverlayManager, id: OverlayId, r: Rect) {
mgr.stack.iter_mut().find(|o| o.id == id).unwrap().bounds = r;
}
fn build() -> (OverlayManager, OverlayId, OverlayId) {
let mut mgr = OverlayManager::new();
let modal = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::EscapeOrClickOutside,
));
set_bounds(&mut mgr, modal, Rect::new(300.0, 60.0, 600.0, 680.0));
let tooltip = mgr.show(OverlayRequest::new(
fake_id(11),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::EscapeOrClickOutside,
));
set_bounds(&mut mgr, tooltip, Rect::new(400.0, 620.0, 200.0, 140.0));
(mgr, modal, tooltip)
}
let (mut mgr, modal, _tooltip) = build();
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(350.0, 100.0));
assert!(dismissed.contains(&fake_id(11)), "tooltip should dismiss");
assert!(
mgr.active_ids().contains(&modal),
"modal must survive a click inside itself"
);
let (mut mgr, modal, tooltip) = build();
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(450.0, 750.0));
assert!(
dismissed.is_empty(),
"clicking the tooltip dismisses nothing"
);
assert!(mgr.active_ids().contains(&modal));
assert!(mgr.active_ids().contains(&tooltip));
let (mut mgr, _modal, _tooltip) = build();
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(10.0, 10.0));
assert!(dismissed.contains(&fake_id(10)));
assert!(dismissed.contains(&fake_id(11)));
assert!(mgr.is_empty());
}
#[test]
fn active_content_ids() {
let mut mgr = OverlayManager::new();
mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
mgr.show(OverlayRequest::new(
fake_id(20),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
let ids = mgr.active_content_ids();
assert_eq!(ids.len(), 2);
assert_eq!(ids[0], fake_id(10));
assert_eq!(ids[1], fake_id(20));
}
#[test]
fn hit_test_topmost_first() {
let mut mgr = OverlayManager::new();
let a = mgr.show(OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
let b = mgr.show(OverlayRequest::new(
fake_id(11),
fake_id(2),
OverlayPlacement::Below,
DismissBehavior::Manual,
));
mgr.set_content_bounds(a, Size::new(100.0, 50.0));
mgr.set_content_bounds(b, Size::new(100.0, 50.0));
assert_eq!(mgr.hit_test(Point::new(50.0, 25.0)), Some(b));
}
#[test]
fn hit_test_skips_a_fading_out_overlay() {
let mut mgr = OverlayManager::new();
let id = mgr.show(
OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Below,
DismissBehavior::ClickOutside,
)
.with_fade(Duration::from_millis(150)),
);
mgr.set_content_bounds(id, Size::new(100.0, 50.0));
let point = Point::new(50.0, 25.0);
assert_eq!(mgr.hit_test(point), Some(id));
assert!(mgr.active_ids().contains(&id));
mgr.attach_fade(id, Signal::new(1.0), Duration::from_millis(150));
let dismissed = mgr.dismiss(id);
assert!(dismissed.is_empty(), "fade-out defers removal");
assert_eq!(mgr.stack.len(), 1, "overlay lingers during the fade");
assert_eq!(
mgr.hit_test(point),
None,
"fading overlay no longer eats clicks"
);
assert!(!mgr.active_ids().contains(&id));
}
#[test]
fn pause_auto_dismiss_removes_overlay_from_deadline_set() {
let mut mgr = OverlayManager::new();
let id = mgr.show_for(
OverlayRequest::new(
fake_id(10),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::Manual,
),
Duration::from_secs(10),
);
assert!(mgr.next_auto_dismiss_deadline().is_some());
assert!(!mgr.is_auto_dismiss_paused(id));
mgr.pause_auto_dismiss(id);
assert!(mgr.is_auto_dismiss_paused(id));
assert!(
mgr.next_auto_dismiss_deadline().is_none(),
"paused overlay must drop out of the deadline-min query"
);
mgr.resume_auto_dismiss(id);
assert!(!mgr.is_auto_dismiss_paused(id));
assert!(mgr.next_auto_dismiss_deadline().is_some());
}
#[test]
fn pause_then_resume_restores_remaining_time() {
let mut mgr = OverlayManager::new();
let id = mgr.show_for(
OverlayRequest::new(
fake_id(11),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::Manual,
),
Duration::from_secs(10),
);
mgr.pause_auto_dismiss(id);
let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
let remaining = overlay.paused_remaining.unwrap();
assert!(
remaining >= Duration::from_secs(9),
"remaining should be near the original 10s, got {remaining:?}"
);
assert!(remaining <= Duration::from_secs(10));
mgr.resume_auto_dismiss(id);
let overlay = mgr.stack.iter().find(|o| o.id == id).unwrap();
assert_eq!(overlay.auto_dismiss_after, Some(remaining));
assert!(overlay.paused_remaining.is_none());
}
#[test]
fn pause_is_idempotent() {
let mut mgr = OverlayManager::new();
let id = mgr.show_for(
OverlayRequest::new(
fake_id(12),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::Manual,
),
Duration::from_secs(10),
);
mgr.pause_auto_dismiss(id);
let first_remaining = mgr.stack[0].paused_remaining;
mgr.pause_auto_dismiss(id); let second_remaining = mgr.stack[0].paused_remaining;
assert_eq!(
first_remaining, second_remaining,
"double-pause must preserve the original stashed remaining"
);
}
#[test]
fn resume_on_unpaused_is_noop() {
let mut mgr = OverlayManager::new();
let id = mgr.show_for(
OverlayRequest::new(
fake_id(13),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::Manual,
),
Duration::from_secs(10),
);
let before = mgr.stack[0].auto_dismiss_after;
mgr.resume_auto_dismiss(id); let after = mgr.stack[0].auto_dismiss_after;
assert_eq!(before, after);
}
#[test]
fn pause_on_persistent_overlay_is_noop() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest::new(
fake_id(14),
fake_id(1),
OverlayPlacement::Centered,
DismissBehavior::Manual,
));
mgr.pause_auto_dismiss(id);
assert!(!mgr.is_auto_dismiss_paused(id));
assert!(mgr.stack[0].paused_remaining.is_none());
}
#[test]
fn pause_on_unknown_id_is_noop() {
let mut mgr = OverlayManager::new();
mgr.pause_auto_dismiss(OverlayId::new(9999)); mgr.resume_auto_dismiss(OverlayId::new(9999));
}
fn show_at(
mgr: &mut OverlayManager,
content: u64,
bounds: Rect,
dismiss: DismissBehavior,
band: OverlayBand,
) -> OverlayId {
let id = mgr.show_in_band(
OverlayRequest::new(
fake_id(content),
fake_id(content + 100),
OverlayPlacement::Centered,
dismiss,
),
band,
);
if let Some(overlay) = mgr.stack.iter_mut().find(|o| o.id == id) {
overlay.bounds = bounds;
}
id
}
#[test]
fn a_text_affordance_is_inserted_below_the_menus_already_open() {
let mut mgr = OverlayManager::new();
let menu = show_at(
&mut mgr,
1,
Rect::new(0.0, 0.0, 100.0, 100.0),
DismissBehavior::ClickOutside,
OverlayBand::Standard,
);
let handle = show_at(
&mut mgr,
2,
Rect::new(200.0, 200.0, 44.0, 44.0),
DismissBehavior::Manual,
OverlayBand::TextAffordance,
);
let order: Vec<OverlayId> = mgr.stack.iter().map(|o| o.id).collect();
assert_eq!(order, vec![handle, menu], "the handle sits under the menu");
assert_eq!(mgr.topmost().map(|o| o.id), Some(menu));
}
#[test]
fn an_outside_press_leaves_the_text_affordance_band_alone() {
let mut mgr = OverlayManager::new();
let handle = show_at(
&mut mgr,
2,
Rect::new(200.0, 200.0, 44.0, 44.0),
DismissBehavior::ClickOutside,
OverlayBand::TextAffordance,
);
let menu = show_at(
&mut mgr,
1,
Rect::new(0.0, 0.0, 100.0, 100.0),
DismissBehavior::ClickOutside,
OverlayBand::Standard,
);
let (dismissed, _, _) = mgr.handle_click_outside(Point::new(600.0, 600.0));
assert_eq!(dismissed, vec![fake_id(1)], "only the menu closes");
assert!(mgr.overlay(handle).is_some());
assert!(mgr.overlay(menu).is_none());
}
fn contact(n: u64) -> PointerId {
crate::pointer::PointerIdAllocator::global()
.begin(crate::pointer::BackendDeviceKey::new(0x0FA1), n)
}
fn with_one_menu() -> (OverlayManager, OverlayId) {
let mut mgr = OverlayManager::new();
let menu = show_at(
&mut mgr,
1,
Rect::new(100.0, 100.0, 200.0, 200.0),
DismissBehavior::ClickOutside,
OverlayBand::Standard,
);
(mgr, menu)
}
#[test]
fn an_armed_press_suppresses_the_down_and_dismisses_on_the_up() {
let (mut mgr, menu) = with_one_menu();
let finger = contact(1);
let arm = mgr.arm_dismiss(finger, Point::new(500.0, 500.0), &[]);
assert!(arm.will_dismiss && arm.suppress_beneath);
assert!(mgr.overlay(menu).is_some(), "nothing closes on the press");
let (dismissed, _, anchors) = mgr.commit_dismiss(finger, Point::new(500.0, 500.0));
assert_eq!(dismissed, vec![fake_id(1)]);
assert_eq!(anchors, vec![fake_id(101)]);
assert!(!mgr.has_armed_dismiss(finger));
}
#[test]
fn a_press_with_nothing_to_close_arms_nothing() {
let mut mgr = OverlayManager::new();
let finger = contact(2);
assert_eq!(
mgr.arm_dismiss(finger, Point::new(10.0, 10.0), &[]),
DismissArm::default()
);
assert!(!mgr.has_armed_dismiss(finger));
}
#[test]
fn a_cancelled_press_aborts_the_arm() {
let (mut mgr, menu) = with_one_menu();
let finger = contact(3);
assert!(
mgr.arm_dismiss(finger, Point::new(500.0, 500.0), &[])
.will_dismiss
);
assert!(mgr.abort_dismiss(finger));
assert!(mgr.overlay(menu).is_some(), "the menu survives a cancel");
assert!(!mgr.abort_dismiss(finger), "aborting twice is a no-op");
let (dismissed, restore, anchors) = mgr.commit_dismiss(finger, Point::new(500.0, 500.0));
assert!(dismissed.is_empty() && restore.is_none() && anchors.is_empty());
}
#[test]
fn a_press_that_slides_onto_the_menu_dismisses_nothing() {
let (mut mgr, menu) = with_one_menu();
let finger = contact(4);
assert!(
mgr.arm_dismiss(finger, Point::new(500.0, 500.0), &[])
.will_dismiss
);
let (dismissed, _, anchors) = mgr.commit_dismiss(finger, Point::new(150.0, 150.0));
assert!(dismissed.is_empty(), "the release landed inside the menu");
assert!(anchors.is_empty());
assert!(mgr.overlay(menu).is_some());
}
#[test]
fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
let (mut mgr, menu) = with_one_menu();
let second = contact(5);
let busy = [Point::new(150.0, 150.0)]; assert_eq!(
mgr.arm_dismiss(second, Point::new(500.0, 500.0), &busy),
DismissArm::default()
);
assert!(mgr.overlay(menu).is_some());
assert!(
mgr.arm_dismiss(second, Point::new(500.0, 500.0), &[])
.will_dismiss
);
let (dismissed, _, _) = mgr.commit_dismiss(second, Point::new(500.0, 500.0));
assert_eq!(dismissed, vec![fake_id(1)]);
}
#[test]
fn arms_are_tracked_per_pointer() {
let mut mgr = OverlayManager::new();
let lower = show_at(
&mut mgr,
1,
Rect::new(0.0, 0.0, 100.0, 100.0),
DismissBehavior::ClickOutside,
OverlayBand::Standard,
);
let upper = show_at(
&mut mgr,
2,
Rect::new(400.0, 0.0, 100.0, 100.0),
DismissBehavior::ClickOutside,
OverlayBand::Standard,
);
let a = contact(6);
let b = contact(7);
assert!(mgr.arm_dismiss(a, Point::new(50.0, 50.0), &[]).will_dismiss);
assert!(
mgr.arm_dismiss(b, Point::new(700.0, 700.0), &[])
.will_dismiss
);
assert_eq!(mgr.armed_pointers().len(), 2);
let (dismissed, _, _) = mgr.commit_dismiss(a, Point::new(50.0, 50.0));
assert_eq!(dismissed, vec![fake_id(2)], "only the overlay above `a`");
assert!(mgr.overlay(lower).is_some());
assert!(mgr.overlay(upper).is_none());
let (dismissed, _, _) = mgr.commit_dismiss(b, Point::new(700.0, 700.0));
assert_eq!(dismissed, vec![fake_id(1)]);
assert!(mgr.armed_pointers().is_empty());
}
#[test]
fn a_second_arm_for_one_pointer_replaces_the_first() {
let (mut mgr, _menu) = with_one_menu();
let finger = contact(8);
assert!(
mgr.arm_dismiss(finger, Point::new(500.0, 500.0), &[])
.will_dismiss
);
assert!(
mgr.arm_dismiss(finger, Point::new(600.0, 600.0), &[])
.will_dismiss
);
assert_eq!(mgr.armed_pointers(), vec![finger]);
}
}