Add scoped overlay hosts

This commit is contained in:
iris committed 2026-09-10 18:49:03 -04:00
1 parent a33fbca966
commit 44a5da378b
14 files changed
+882 -27

No files matched your search

+30
View File
@@ -14,6 +14,10 @@ impl ControllerId {
pub fn host(self) -> WidgetId {
self.host
}
pub fn is<C: 'static>(self) -> bool {
self.kind == TypeId::of::<C>()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -44,6 +48,10 @@ pub trait Controller<Rsc>: ControllerValue {
fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult {
CommandResult::Unused
}
fn blocks_prior_input(&self) -> bool {
false
}
}
pub struct ControllerManager<Rsc> {
@@ -206,6 +214,16 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
self.command_target
}
pub fn command_target_blocks_input(&self) -> bool {
let Some(id) = self.command_target else {
return false;
};
self.by_widget
.get(&id.host)
.and_then(|controllers| controllers.get(&id.kind))
.is_some_and(|controller| controller.blocks_prior_input())
}
pub(crate) fn command_target_revision(&self) -> u64 {
self.command_target_revision
}
@@ -214,6 +232,18 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
self.command_boundary
}
pub(crate) fn is_below(&self, mut widget: WidgetId, ancestor: WidgetId) -> bool {
loop {
let Some(parent) = self.parents.get(&widget).copied().flatten() else {
return false;
};
if parent == ancestor {
return true;
}
widget = parent;
}
}
pub(crate) fn set_command_boundary(&mut self, boundary: Option<WidgetId>) {
self.command_boundary = boundary;
}
+6 -7
View File
@@ -54,13 +54,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
fn run_command(&mut self, command: Command) -> CommandResult {
let revision = self.events().controllers.command_target_revision();
if self
.events()
.controllers
.command_target()
.is_some_and(|target| {
self.events().controllers.command_boundary() == Some(target.host())
})
if let Some(boundary) = self.events().controllers.command_boundary()
&& self
.events()
.controllers
.command_target()
.is_none_or(|target| !self.events().controllers.is_below(target.host(), boundary))
{
return CommandResult::Unused;
}
+4 -4
View File
@@ -22,14 +22,14 @@ pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
}
}
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) {
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot<Rsc>) {
let id = self.add_strong(rsc);
root.set_root(id);
root.set_root(rsc, id);
}
}
pub trait HasRoot {
fn set_root(&mut self, root: StrongWidget);
pub trait HasRoot<Rsc> {
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget);
}
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
+1 -1
View File
@@ -31,7 +31,7 @@ impl DefaultAppState for State {
.ui
.widgets
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End));
ui_state.set_root(root.any());
ui_state.set_root(rsc, root.any());
Self {
ui_state,
span: span_weak,
+1 -1
View File
@@ -79,7 +79,7 @@ impl DefaultAppState for State {
.masked()
.background(rect(PaintId::WHITE))
.add_strong(rsc);
ui_state.set_root(root.any());
ui_state.set_root(rsc, root.any());
Self { ui_state }
}
+3 -1
View File
@@ -31,7 +31,9 @@ fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
impl<State: AndroidAppState> IrisViewPeer<State> {
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
self.state.android_state().focus
(!self.rsc.events.controllers.command_target_blocks_input())
.then_some(self.state.android_state().focus)
.flatten()
}
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
+10 -3
View File
@@ -109,9 +109,9 @@ impl AndroidUiState {
}
}
impl HasRoot for AndroidUiState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
impl<State: 'static> HasRoot<AndroidRsc<State>> for AndroidUiState {
fn set_root(&mut self, rsc: &mut AndroidRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
}
}
@@ -463,12 +463,19 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
// `android/insets.rs`'s doc comment for why insets could not take
// the same shortcut.
if key_code == Keycode::Back {
if self.rsc.run_command(Command::Escape) != CommandResult::Unused {
self.after_input(ctx);
return true;
}
let handled = self.state.back_pressed(&mut self.rsc, &mut self.render);
if handled {
self.after_input(ctx);
}
return handled;
}
if self.rsc.events.controllers.command_target_blocks_input() {
return true;
}
let handled = super::input::on_key(
&mut self.rsc,
&mut self.state,
+7 -4
View File
@@ -65,9 +65,9 @@ pub struct DefaultUiState {
pub access: AccessTree,
}
impl HasRoot for DefaultUiState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
impl<State: 'static> HasRoot<DefaultRsc<State>> for DefaultUiState {
fn set_root(&mut self, rsc: &mut DefaultRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
}
}
@@ -371,6 +371,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
CommandResult::Unused => false,
};
if !command_used
&& !rsc.events.controllers.command_target_blocks_input()
&& let Some(sel) = ui_state.focus
&& event.state.is_pressed()
{
@@ -402,7 +403,9 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = ui_state.focus {
if !rsc.events.controllers.command_target_blocks_input()
&& let Some(sel) = ui_state.focus
{
let mut text = sel.edit(rsc);
match ime {
Ime::Enabled | Ime::Disabled => (),
+3 -3
View File
@@ -135,9 +135,9 @@ impl HarnessState {
}
}
impl HasRoot for HarnessState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
impl HasRoot<HarnessRsc> for HarnessState {
fn set_root(&mut self, rsc: &mut HarnessRsc, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
}
}
+2
View File
@@ -16,6 +16,7 @@ pub mod attr;
pub mod diagnostics;
pub mod event;
pub mod harness;
pub mod overlay;
pub mod platform;
pub mod sense;
pub mod state;
@@ -43,6 +44,7 @@ pub mod prelude {
pub use event::*;
pub use iris_core::*;
pub use iris_macro::*;
pub use overlay::*;
pub use platform::*;
pub use sense::*;
pub use state::*;
+812
View File
@@ -0,0 +1,812 @@
//! 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 (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id())?;
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 target = self
.events()
.controllers
.nearest_id::<SingleOverlayController>(origin.id())?;
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 (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id())?;
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, HarnessRsc, TouchAction};
use std::{cell::Cell, rc::Rc};
struct EscapeCounter(Rc<Cell<usize>>);
impl Controller<HarnessRsc> for EscapeCounter {
fn command(&mut self, command: Command, _rsc: &mut HarnessRsc) -> 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());
}
}
+1 -1
View File
@@ -624,7 +624,7 @@ mod controller_tests {
let found = rsc
.events()
.controllers
.nearest_id::<SelectionController>(leaf.id(), &render)
.nearest_id::<SelectionController>(leaf.id())
.unwrap();
assert_eq!(found.host(), inner.id());
}
+1 -1
View File
@@ -5,7 +5,7 @@ pub struct ClientWidgets {
pub info: WeakWidget<Text>,
}
pub fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets
pub fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) -> ClientWidgets
where
Rsc::State: FocusHost,
{
+1 -1
View File
@@ -30,7 +30,7 @@ fn solid_paints_and_images_round_trip_through_an_srgb_target() {
.span(Dir::RIGHT)
.add_strong(&mut harness.rsc)
.any();
harness.state.set_root(root);
harness.state.set_root(&mut harness.rsc, root);
harness.frame(0);
let mut renderer =