diff --git a/core/src/event/controller.rs b/core/src/event/controller.rs index 597ac6e..0b9f19e 100644 --- a/core/src/event/controller.rs +++ b/core/src/event/controller.rs @@ -14,6 +14,10 @@ impl ControllerId { pub fn host(self) -> WidgetId { self.host } + + pub fn is(self) -> bool { + self.kind == TypeId::of::() + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -44,6 +48,10 @@ pub trait Controller: ControllerValue { fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult { CommandResult::Unused } + + fn blocks_prior_input(&self) -> bool { + false + } } pub struct ControllerManager { @@ -206,6 +214,16 @@ impl ControllerManager { 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 ControllerManager { 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) { self.command_boundary = boundary; } diff --git a/core/src/event/rsc.rs b/core/src/event/rsc.rs index d422dbb..2a72997 100644 --- a/core/src/event/rsc.rs +++ b/core/src/event/rsc.rs @@ -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; } diff --git a/core/src/widget/like.rs b/core/src/widget/like.rs index 5dec9f2..5a54371 100644 --- a/core/src/widget/like.rs +++ b/core/src/widget/like.rs @@ -22,14 +22,14 @@ pub trait WidgetLike: Sized { } } - fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) { + fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) { 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 { + fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget); } pub trait WidgetArrLike { diff --git a/examples/bench_images.rs b/examples/bench_images.rs index 4ece2c6..e08a670 100644 --- a/examples/bench_images.rs +++ b/examples/bench_images.rs @@ -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, diff --git a/examples/message_list.rs b/examples/message_list.rs index 4f98211..c8da116 100644 --- a/examples/message_list.rs +++ b/examples/message_list.rs @@ -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 } } diff --git a/src/android/ime.rs b/src/android/ime.rs index 92cb0f9..b76a523 100644 --- a/src/android/ime.rs +++ b/src/android/ime.rs @@ -31,7 +31,9 @@ fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize { impl IrisViewPeer { fn focus(&self) -> Option> { - 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) { diff --git a/src/android/view.rs b/src/android/view.rs index 48b2de5..7328c7d 100644 --- a/src/android/view.rs +++ b/src/android/view.rs @@ -109,9 +109,9 @@ impl AndroidUiState { } } -impl HasRoot for AndroidUiState { - fn set_root(&mut self, root: StrongWidget) { - self.root = Some(root); +impl HasRoot> for AndroidUiState { + fn set_root(&mut self, rsc: &mut AndroidRsc, root: StrongWidget) { + self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } @@ -463,12 +463,19 @@ impl ViewPeer for IrisViewPeer { // `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, diff --git a/src/default/mod.rs b/src/default/mod.rs index 74efc7d..c785efb 100644 --- a/src/default/mod.rs +++ b/src/default/mod.rs @@ -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 HasRoot> for DefaultUiState { + fn set_root(&mut self, rsc: &mut DefaultRsc, root: StrongWidget) { + self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } @@ -371,6 +371,7 @@ impl AppState for DefaultApp { 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 AppState for DefaultApp { } } 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 => (), diff --git a/src/harness.rs b/src/harness.rs index f75040d..7eebb5a 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -135,9 +135,9 @@ impl HarnessState { } } -impl HasRoot for HarnessState { - fn set_root(&mut self, root: StrongWidget) { - self.root = Some(root); +impl HasRoot for HarnessState { + fn set_root(&mut self, rsc: &mut HarnessRsc, root: StrongWidget) { + self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } diff --git a/src/lib.rs b/src/lib.rs index af647fe..b7dd1fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; diff --git a/src/overlay.rs b/src/overlay.rs new file mode 100644 index 0000000..bdfedc0 --- /dev/null +++ b/src/overlay.rs @@ -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(self, rsc: &mut Rsc) -> bool { + match self.kind { + OverlayKind::Single => { + let Some(id) = rsc + .events() + .controllers + .id::(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::(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, + cancel: CancelBehavior, + blocks_prior_input: bool, +} + +pub struct SingleOverlayController { + stack: WeakWidget, + active: Option, + next_serial: u64, +} + +impl SingleOverlayController { + fn new(stack: WeakWidget) -> Self { + Self { + stack, + active: None, + next_serial: 1, + } + } + + fn open( + &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(&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( + &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 Controller 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, + entries: Vec, + next_serial: u64, +} + +impl StackableOverlayController { + fn new(stack: WeakWidget) -> Self { + Self { + stack, + entries: Vec::new(), + next_serial: 1, + } + } + + fn open( + &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(&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 Controller 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::() + }); + 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(stack: WeakWidget, 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(host: WidgetId, rsc: &mut Rsc, replacement: bool) { + let Some(id) = rsc.events().controllers.id::(host) else { + return; + }; + rsc.with_controller(id, |single: &mut SingleOverlayController, rsc| { + single.dismiss(id, rsc, replacement); + }); +} + +fn add_host( + rsc: &mut Rsc, + base: StrongWidget, + options: OverlayHostOptions, +) -> WeakWidget { + 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( + content: W, + options: OverlayHostOptions, +) -> impl WidgetIdFn +where + Rsc: HasEvents, + W: WidgetLike, +{ + move |rsc: &mut Rsc| { + let content = content.add_strong(rsc); + add_host(rsc, content, options) + } +} + +pub(crate) fn default_overlay_root( + rsc: &mut Rsc, + root: StrongWidget, +) -> StrongWidget { + add_host(rsc, root, OverlayHostOptions::BOTH) + .upgrade(rsc) + .any() +} + +pub trait OverlayRscExt: HasEvents { + fn open_popup(&mut self, origin: impl IdLike, popup: W) -> Option + where + W: WidgetLike, + { + self.open_overlay( + origin, + popup, + OverlayOptions::single(CancelBehavior::PassThrough), + ) + } + + fn open_modal(&mut self, origin: impl IdLike, modal: W) -> Option + where + W: WidgetLike, + { + let (target, path) = self + .events() + .controllers + .path_to::(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( + &mut self, + origin: impl IdLike, + overlay: W, + options: OverlayOptions, + ) -> Option + where + W: WidgetLike, + { + match options.kind { + OverlayKind::Single => { + let target = self + .events() + .controllers + .nearest_id::(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::(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 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(path: &[WidgetId], rsc: &mut Rsc) { + let controllers: Vec = path + .iter() + .filter_map(|&host| rsc.events().controllers.id::(host)) + .collect(); + for id in controllers { + rsc.with_controller(id, |single: &mut SingleOverlayController, rsc| { + single.dismiss(id, rsc, true); + }); + } +} + +fn open_stackable_at( + target: ControllerId, + overlay: StrongWidget, + cancel: CancelBehavior, + blocks_prior_input: bool, + rsc: &mut Rsc, +) -> Option { + 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>); + + impl Controller 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::() + .unwrap() + .children + .len() + } + + fn rooted_harness() -> (Harness, WeakWidget, Rc>) { + 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::(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()); + } +} diff --git a/src/widget/text/selection.rs b/src/widget/text/selection.rs index 63c9e5d..9e0b5d4 100644 --- a/src/widget/text/selection.rs +++ b/src/widget/text/selection.rs @@ -624,7 +624,7 @@ mod controller_tests { let found = rsc .events() .controllers - .nearest_id::(leaf.id(), &render) + .nearest_id::(leaf.id()) .unwrap(); assert_eq!(found.host(), inner.id()); } diff --git a/tabs-ui/src/lib.rs b/tabs-ui/src/lib.rs index f661f95..0e92873 100644 --- a/tabs-ui/src/lib.rs +++ b/tabs-ui/src/lib.rs @@ -5,7 +5,7 @@ pub struct ClientWidgets { pub info: WeakWidget, } -pub fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets +pub fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets where Rsc::State: FocusHost, { diff --git a/tests/color_space.rs b/tests/color_space.rs index 88d4d52..3758595 100644 --- a/tests/color_space.rs +++ b/tests/color_space.rs @@ -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 =