Files
iris/src/rsc/overlay.rs
T

816 lines
25 KiB
Rust

//! Optional overlays over an ordinary [`Stack`].
//!
//! An overlay host owns independent single and stackable controllers. Opening
//! any overlay removes the active single on each host the request traverses;
//! stackables remain in opening order, and the host's optional single is
//! always its final child. Controller lookup follows the active parent map
//! maintained by `EventManager`, so callers need only the requesting widget,
//! not access to `UiRenderState`.
use crate::prelude::*;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OverlayKind {
Single,
Stackable,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CancelBehavior {
Consume,
PassThrough,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OverlayOptions {
pub kind: OverlayKind,
pub cancel: CancelBehavior,
pub blocks_prior_input: bool,
}
impl OverlayOptions {
pub const fn single(cancel: CancelBehavior) -> Self {
Self {
kind: OverlayKind::Single,
cancel,
blocks_prior_input: false,
}
}
pub const fn stackable(cancel: CancelBehavior) -> Self {
Self {
kind: OverlayKind::Stackable,
cancel,
blocks_prior_input: false,
}
}
pub const fn blocking(mut self) -> Self {
self.blocks_prior_input = true;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OverlayHostOptions {
pub single: bool,
pub stackable: bool,
}
impl OverlayHostOptions {
pub const BOTH: Self = Self {
single: true,
stackable: true,
};
pub const SINGLE: Self = Self {
single: true,
stackable: false,
};
pub const STACKABLE: Self = Self {
single: false,
stackable: true,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OverlayHandle {
host: WidgetId,
kind: OverlayKind,
serial: u64,
}
impl OverlayHandle {
pub fn close<Rsc: HasEvents>(self, rsc: &mut Rsc) -> bool {
match self.kind {
OverlayKind::Single => {
let Some(id) = rsc
.events()
.controllers
.id::<SingleOverlayController>(self.host)
else {
return false;
};
rsc.with_controller(id, |controller: &mut SingleOverlayController, rsc| {
controller.close(id, self.serial, rsc)
})
.unwrap_or(false)
}
OverlayKind::Stackable => {
let Some(id) = rsc
.events()
.controllers
.id::<StackableOverlayController>(self.host)
else {
return false;
};
rsc.with_controller(id, |controller: &mut StackableOverlayController, rsc| {
controller.close(id, self.serial, rsc)
})
.unwrap_or(false)
}
}
}
}
struct OverlayEntry {
serial: u64,
widget: WidgetId,
previous_target: Option<ControllerId>,
cancel: CancelBehavior,
blocks_prior_input: bool,
}
pub struct SingleOverlayController {
stack: WeakWidget<Stack>,
active: Option<OverlayEntry>,
next_serial: u64,
}
impl SingleOverlayController {
fn new(stack: WeakWidget<Stack>) -> Self {
Self {
stack,
active: None,
next_serial: 1,
}
}
fn open<Rsc: HasEvents>(
&mut self,
id: ControllerId,
popup: StrongWidget,
cancel: CancelBehavior,
blocks_prior_input: bool,
rsc: &mut Rsc,
) -> OverlayHandle {
self.dismiss(id, rsc, true);
let serial = self.take_serial();
let widget = popup.id();
let previous_target = rsc.events().controllers.command_target();
(self.stack)(rsc).children.push(popup);
self.active = Some(OverlayEntry {
serial,
widget,
previous_target,
cancel,
blocks_prior_input,
});
rsc.set_command_target(Some(id));
OverlayHandle {
host: id.host(),
kind: OverlayKind::Single,
serial,
}
}
fn close<Rsc: HasEvents>(&mut self, id: ControllerId, serial: u64, rsc: &mut Rsc) -> bool {
if self.active.as_ref().map(|entry| entry.serial) != Some(serial) {
return false;
}
self.dismiss(id, rsc, false);
true
}
fn dismiss<Rsc: HasEvents>(
&mut self,
id: ControllerId,
rsc: &mut Rsc,
replacement: bool,
) -> bool {
let Some(entry) = self.active.take() else {
return false;
};
let current_target = rsc.events().controllers.command_target();
let was_target = current_target == Some(id);
remove_child(self.stack, entry.widget, rsc);
if was_target {
rsc.set_command_target(entry.previous_target);
}
if replacement && entry.cancel == CancelBehavior::PassThrough {
rsc.set_command_target(entry.previous_target);
let _ = rsc.run_command_before(Command::Escape, self.stack);
if !was_target {
rsc.set_command_target(current_target);
}
}
true
}
fn take_serial(&mut self) -> u64 {
let serial = self.next_serial;
self.next_serial = self.next_serial.wrapping_add(1).max(1);
serial
}
}
impl<Rsc: HasEvents> Controller<Rsc> for SingleOverlayController {
fn command(&mut self, command: Command, rsc: &mut Rsc) -> CommandResult {
if command != Command::Escape {
return CommandResult::Unused;
}
let Some(entry) = self.active.take() else {
return CommandResult::Unused;
};
remove_child(self.stack, entry.widget, rsc);
rsc.set_command_target(entry.previous_target);
if entry.cancel == CancelBehavior::PassThrough {
let _ = rsc.run_command(command);
}
CommandResult::Used
}
fn blocks_prior_input(&self) -> bool {
self.active
.as_ref()
.is_some_and(|entry| entry.blocks_prior_input)
}
}
pub struct StackableOverlayController {
stack: WeakWidget<Stack>,
entries: Vec<OverlayEntry>,
next_serial: u64,
}
impl StackableOverlayController {
fn new(stack: WeakWidget<Stack>) -> Self {
Self {
stack,
entries: Vec::new(),
next_serial: 1,
}
}
fn open<Rsc: HasEvents>(
&mut self,
id: ControllerId,
overlay: StrongWidget,
cancel: CancelBehavior,
blocks_prior_input: bool,
rsc: &mut Rsc,
) -> OverlayHandle {
let serial = self.take_serial();
let widget = overlay.id();
let previous_target = rsc.events().controllers.command_target();
(self.stack)(rsc).children.push(overlay);
self.entries.push(OverlayEntry {
serial,
widget,
previous_target,
cancel,
blocks_prior_input,
});
rsc.set_command_target(Some(id));
OverlayHandle {
host: id.host(),
kind: OverlayKind::Stackable,
serial,
}
}
fn close<Rsc: HasEvents>(&mut self, id: ControllerId, serial: u64, rsc: &mut Rsc) -> bool {
let Some(index) = self.entries.iter().position(|entry| entry.serial == serial) else {
return false;
};
close_single_at(id.host(), rsc, false);
let previous_target = self.entries[index].previous_target;
for entry in self.entries.drain(index..) {
remove_child(self.stack, entry.widget, rsc);
}
if rsc.events().controllers.command_target() == Some(id) {
rsc.set_command_target(previous_target);
}
true
}
fn take_serial(&mut self) -> u64 {
let serial = self.next_serial;
self.next_serial = self.next_serial.wrapping_add(1).max(1);
serial
}
}
impl<Rsc: HasEvents> Controller<Rsc> for StackableOverlayController {
fn command(&mut self, command: Command, rsc: &mut Rsc) -> CommandResult {
if command != Command::Escape {
return CommandResult::Unused;
}
let mut used = false;
loop {
let Some(entry) = self.entries.pop() else {
return if used {
CommandResult::Used
} else {
CommandResult::Unused
};
};
used = true;
remove_child(self.stack, entry.widget, rsc);
rsc.set_command_target(entry.previous_target);
if entry.cancel == CancelBehavior::Consume {
return CommandResult::Used;
}
let same_controller = entry.previous_target.is_some_and(|previous| {
previous.host() == self.stack.id() && previous.is::<StackableOverlayController>()
});
if !same_controller {
let _ = rsc.run_command(command);
return CommandResult::Used;
}
}
}
fn blocks_prior_input(&self) -> bool {
self.entries
.last()
.is_some_and(|entry| entry.blocks_prior_input)
}
}
fn remove_child<Rsc: UiRsc>(stack: WeakWidget<Stack>, widget: WidgetId, rsc: &mut Rsc) {
if let Some(stack) = rsc.widgets_mut().get_mut(&stack)
&& let Some(index) = stack.children.iter().position(|child| child.id() == widget)
{
stack.children.remove(index);
}
}
fn close_single_at<Rsc: HasEvents>(host: WidgetId, rsc: &mut Rsc, replacement: bool) {
let Some(id) = rsc.events().controllers.id::<SingleOverlayController>(host) else {
return;
};
rsc.with_controller(id, |single: &mut SingleOverlayController, rsc| {
single.dismiss(id, rsc, replacement);
});
}
fn add_host<Rsc: HasEvents>(
rsc: &mut Rsc,
base: StrongWidget,
options: OverlayHostOptions,
) -> WeakWidget<Stack> {
let stack = Stack {
children: vec![base],
size: StackSize::Default,
}
.add(rsc);
if options.single {
rsc.register_controller(stack, SingleOverlayController::new(stack));
}
if options.stackable {
rsc.register_controller(stack, StackableOverlayController::new(stack));
}
stack
}
pub fn overlay_host<Rsc, W, Tag>(
content: W,
options: OverlayHostOptions,
) -> impl WidgetIdFn<Rsc, Stack>
where
Rsc: HasEvents,
W: WidgetLike<Rsc, Tag>,
{
move |rsc: &mut Rsc| {
let content = content.add_strong(rsc);
add_host(rsc, content, options)
}
}
pub(crate) fn default_overlay_root<Rsc: HasEvents>(
rsc: &mut Rsc,
root: StrongWidget,
) -> StrongWidget {
add_host(rsc, root, OverlayHostOptions::BOTH)
.upgrade(rsc)
.any()
}
pub trait OverlayRscExt: HasEvents {
fn open_popup<W, Tag>(&mut self, origin: impl IdLike, popup: W) -> Option<OverlayHandle>
where
W: WidgetLike<Self, Tag>,
{
self.open_overlay(
origin,
popup,
OverlayOptions::single(CancelBehavior::PassThrough),
)
}
fn open_modal<W, Tag>(&mut self, origin: impl IdLike, modal: W) -> Option<OverlayHandle>
where
W: WidgetLike<Self, Tag>,
{
let render_handle = self.ui().render_state();
let (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self);
let content = modal.add(self);
let content_strong = content.upgrade(self).any();
let backdrop = rect(Srgba8::new(0, 0, 0, 96)).add(self);
let backdrop_strong = backdrop.upgrade(self).any();
let centered = Aligned {
inner: content_strong,
align: Align::CENTER.into(),
}
.add_strong(self)
.any();
let layer = Stack {
children: vec![backdrop_strong, centered],
size: StackSize::Default,
}
.add_strong(self)
.any();
let handle = open_stackable_at(target, layer, CancelBehavior::Consume, true, self)?;
let blocking = modal_pointer_senses();
self.register_event(content, blocking.clone(), |_, _| {});
self.register_event(backdrop, blocking, move |ctx, rsc| {
if matches!(ctx.data.sense, CursorSense::PressStart(_)) {
handle.close(rsc);
}
});
Some(handle)
}
fn open_overlay<W, Tag>(
&mut self,
origin: impl IdLike,
overlay: W,
options: OverlayOptions,
) -> Option<OverlayHandle>
where
W: WidgetLike<Self, Tag>,
{
match options.kind {
OverlayKind::Single => {
let render_handle = self.ui().render_state();
let target = self
.events()
.controllers
.nearest_id::<SingleOverlayController>(origin.id(), &render_handle.get())?;
let overlay = overlay.add_strong(self).any();
self.with_controller(target, |single: &mut SingleOverlayController, rsc| {
single.open(
target,
overlay,
options.cancel,
options.blocks_prior_input,
rsc,
)
})
}
OverlayKind::Stackable => {
let render_handle = self.ui().render_state();
let (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self);
let overlay = overlay.add_strong(self).any();
open_stackable_at(
target,
overlay,
options.cancel,
options.blocks_prior_input,
self,
)
}
}
}
}
impl<Rsc: HasEvents> OverlayRscExt for Rsc {}
fn modal_pointer_senses() -> CursorSenses {
CursorSense::drag_senses()
| CursorSense::PressStart(CursorButton::Right)
| CursorSense::Pressing(CursorButton::Right)
| CursorSense::PressEnd(CursorButton::Right)
| CursorSense::PressStart(CursorButton::Middle)
| CursorSense::Pressing(CursorButton::Middle)
| CursorSense::PressEnd(CursorButton::Middle)
| CursorSense::Scroll(Axis::X)
| CursorSense::Scroll(Axis::Y)
}
fn clear_singles<Rsc: HasEvents>(path: &[WidgetId], rsc: &mut Rsc) {
let controllers: Vec<ControllerId> = path
.iter()
.filter_map(|&host| rsc.events().controllers.id::<SingleOverlayController>(host))
.collect();
for id in controllers {
rsc.with_controller(id, |single: &mut SingleOverlayController, rsc| {
single.dismiss(id, rsc, true);
});
}
}
fn open_stackable_at<Rsc: HasEvents>(
target: ControllerId,
overlay: StrongWidget,
cancel: CancelBehavior,
blocks_prior_input: bool,
rsc: &mut Rsc,
) -> Option<OverlayHandle> {
rsc.with_controller(target, |stackable: &mut StackableOverlayController, rsc| {
stackable.open(target, overlay, cancel, blocks_prior_input, rsc)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::harness::{Harness, HarnessState, TouchAction};
use std::{cell::Cell, rc::Rc};
struct EscapeCounter(Rc<Cell<usize>>);
impl Controller<StdRsc<HarnessState>> for EscapeCounter {
fn command(&mut self, command: Command, _rsc: &mut StdRsc<HarnessState>) -> CommandResult {
if command == Command::Escape {
self.0.set(self.0.get() + 1);
CommandResult::Used
} else {
CommandResult::Unused
}
}
}
fn stack_len(harness: &Harness, handle: OverlayHandle) -> usize {
harness
.rsc
.widgets()
.get_dyn(handle.host)
.unwrap()
.as_any()
.downcast_ref::<Stack>()
.unwrap()
.children
.len()
}
fn rooted_harness() -> (Harness, WeakWidget<Rect>, Rc<Cell<usize>>) {
let mut harness = Harness::new(Vec2::new(200.0, 300.0), 1.0);
let base = rect(PaintId::WHITE).add(&mut harness.rsc);
let count = Rc::new(Cell::new(0));
harness
.rsc
.register_controller(base, EscapeCounter(count.clone()));
let target = harness
.rsc
.events()
.controllers
.id::<EscapeCounter>(base.id())
.unwrap();
harness.rsc.set_command_target(Some(target));
let root = base.upgrade(&mut harness.rsc).any();
harness.state.set_root(&mut harness.rsc, root);
harness.frame(0);
(harness, base, count)
}
#[test]
fn popup_is_single_and_passes_escape_through() {
let (mut harness, base, escapes) = rooted_harness();
let first = harness.rsc.open_popup(base, rect(PaintId::RED)).unwrap();
assert_eq!(stack_len(&harness, first), 2);
let second = harness.rsc.open_popup(base, rect(PaintId::BLUE)).unwrap();
assert_eq!(escapes.get(), 1, "replacement reaches the old owner");
assert!(!first.close(&mut harness.rsc), "a replaced handle is stale");
assert_eq!(stack_len(&harness, second), 2);
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(
escapes.get(),
1,
"the replacement had no prior command target"
);
assert_eq!(stack_len(&harness, second), 1);
}
#[test]
fn stackables_survive_singles_and_close_from_the_top() {
let (mut harness, base, escapes) = rooted_harness();
let options = OverlayOptions::stackable(CancelBehavior::Consume);
let first = harness
.rsc
.open_overlay(base, rect(PaintId::RED), options)
.unwrap();
let single = harness.rsc.open_popup(base, rect(PaintId::GREEN)).unwrap();
let second = harness
.rsc
.open_overlay(base, rect(PaintId::BLUE), options)
.unwrap();
assert!(!single.close(&mut harness.rsc));
assert_eq!(stack_len(&harness, second), 3);
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(stack_len(&harness, first), 2);
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(stack_len(&harness, first), 1);
assert_eq!(escapes.get(), 0, "stackable cancellation is consumed");
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(escapes.get(), 1);
}
#[test]
fn an_explicit_single_can_consume_cancellation() {
let (mut harness, base, escapes) = rooted_harness();
let overlay = harness
.rsc
.open_overlay(
base,
rect(PaintId::RED),
OverlayOptions::single(CancelBehavior::Consume),
)
.unwrap();
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(escapes.get(), 0);
assert_eq!(stack_len(&harness, overlay), 1);
}
#[test]
fn a_stackable_can_pass_cancellation_to_the_entry_under_it() {
let (mut harness, base, escapes) = rooted_harness();
let first = harness
.rsc
.open_overlay(
base,
rect(PaintId::RED),
OverlayOptions::stackable(CancelBehavior::Consume),
)
.unwrap();
harness
.rsc
.open_overlay(
base,
rect(PaintId::BLUE),
OverlayOptions::stackable(CancelBehavior::PassThrough),
)
.unwrap();
assert_eq!(
harness.rsc.run_command(Command::Escape),
CommandResult::Used
);
assert_eq!(stack_len(&harness, first), 1);
assert_eq!(escapes.get(), 0);
}
#[test]
fn closing_a_stackable_handle_closes_everything_above_it() {
let (mut harness, base, _) = rooted_harness();
let options = OverlayOptions::stackable(CancelBehavior::Consume);
let first = harness
.rsc
.open_overlay(base, rect(PaintId::RED), options)
.unwrap();
let second = harness
.rsc
.open_overlay(base, rect(PaintId::BLUE), options)
.unwrap();
let single = harness.rsc.open_popup(base, rect(PaintId::GREEN)).unwrap();
assert!(first.close(&mut harness.rsc));
assert!(!second.close(&mut harness.rsc));
assert!(!single.close(&mut harness.rsc));
assert_eq!(stack_len(&harness, first), 1);
}
#[test]
fn backdrop_dismisses_modal_without_clicking_the_base() {
let mut harness = Harness::new(Vec2::new(200.0, 300.0), 1.0);
let clicks = Rc::new(Cell::new(0));
let base = rect(PaintId::WHITE).add(&mut harness.rsc);
let counted = clicks.clone();
harness
.rsc
.register_event(base, CursorSense::click(), move |_, _| {
counted.set(counted.get() + 1);
});
let root = base.upgrade(&mut harness.rsc).any();
harness.state.set_root(&mut harness.rsc, root);
harness.frame(0);
let modal = harness
.rsc
.open_modal(base, rect(PaintId::RED).sized((80, 80)))
.unwrap();
harness.frame(1);
assert_eq!(stack_len(&harness, modal), 2);
assert!(
harness
.rsc
.events()
.controllers
.command_target_blocks_input()
);
harness.touch(TouchAction::Down, Vec2::new(100.0, 150.0), 2);
harness.touch(TouchAction::Up, Vec2::new(100.0, 150.0), 3);
assert_eq!(stack_len(&harness, modal), 2);
harness.touch(TouchAction::Down, Vec2::new(5.0, 5.0), 4);
assert_eq!(clicks.get(), 0);
assert_eq!(stack_len(&harness, modal), 1);
assert!(
!harness
.rsc
.events()
.controllers
.command_target_blocks_input()
);
}
#[test]
fn local_single_host_passes_a_modal_to_the_root() {
let mut harness = Harness::new(Vec2::new(200.0, 300.0), 1.0);
let leaf = rect(PaintId::WHITE).add(&mut harness.rsc);
let local = overlay_host(leaf, OverlayHostOptions::SINGLE).add(&mut harness.rsc);
let root = local.upgrade(&mut harness.rsc).any();
harness.state.set_root(&mut harness.rsc, root);
harness.frame(0);
let popup = harness.rsc.open_popup(leaf, rect(PaintId::RED)).unwrap();
assert_eq!(popup.host, local.id());
let modal = harness
.rsc
.open_modal(leaf, rect(PaintId::BLUE).sized((80, 80)))
.unwrap();
assert!(!popup.close(&mut harness.rsc));
assert_ne!(modal.host, local.id());
}
#[test]
fn replacing_a_local_single_does_not_cancel_an_ancestor_overlay() {
let mut harness = Harness::new(Vec2::new(200.0, 300.0), 1.0);
let leaf = rect(PaintId::WHITE).add(&mut harness.rsc);
let local = overlay_host(leaf, OverlayHostOptions::SINGLE).add(&mut harness.rsc);
let root = local.upgrade(&mut harness.rsc).any();
harness.state.set_root(&mut harness.rsc, root);
harness.frame(0);
let first_modal = harness
.rsc
.open_modal(leaf, rect(PaintId::RED).sized((80, 80)))
.unwrap();
harness.frame(1);
let popup = harness.rsc.open_popup(leaf, rect(PaintId::GREEN)).unwrap();
let second_modal = harness
.rsc
.open_modal(leaf, rect(PaintId::BLUE).sized((80, 80)))
.unwrap();
assert!(!popup.close(&mut harness.rsc));
assert_eq!(stack_len(&harness, first_modal), 3);
assert!(second_modal.close(&mut harness.rsc));
assert_eq!(stack_len(&harness, first_modal), 2);
}
#[test]
fn inactive_origins_have_no_overlay_route() {
let (mut harness, _, _) = rooted_harness();
let inactive = rect(PaintId::WHITE).add(&mut harness.rsc);
assert!(
harness
.rsc
.open_popup(inactive, rect(PaintId::RED))
.is_none()
);
}
#[test]
fn replacing_the_root_forgets_the_old_routing_tree() {
let (mut harness, old, _) = rooted_harness();
let new = rect(PaintId::WHITE).add(&mut harness.rsc);
let root = new.upgrade(&mut harness.rsc).any();
harness.state.set_root(&mut harness.rsc, root);
harness.frame(1);
assert!(harness.rsc.open_popup(old, rect(PaintId::RED)).is_none());
assert!(harness.rsc.open_popup(new, rect(PaintId::RED)).is_some());
}
}