Add scoped overlay hosts
This commit is contained in:
1 parent
b53a17c436
commit
599d33287c
24 files changed
+907
-57
No files matched your search
@@ -93,7 +93,7 @@ impl DefaultAppState for Client {
|
|||||||
.pad(dp(16))
|
.pad(dp(16))
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any();
|
.any();
|
||||||
ui_state.set_root(text);
|
ui_state.set_root(rsc, text);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ impl AndroidAppState for BenchClient {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any();
|
.any();
|
||||||
ui_state.set_root(tree);
|
ui_state.set_root(rsc, tree);
|
||||||
|
|
||||||
let font = rsc.ui.text.font_diagnostics();
|
let font = rsc.ui.text.font_diagnostics();
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl AndroidAppState for TranscriptClient {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add_strong(rsc)
|
.add_strong(rsc)
|
||||||
.any();
|
.any();
|
||||||
ui_state.set_root(tree);
|
ui_state.set_root(rsc, tree);
|
||||||
|
|
||||||
let mut client = Self {
|
let mut client = Self {
|
||||||
ui_state,
|
ui_state,
|
||||||
|
|||||||
@@ -99,12 +99,15 @@ where
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> Result<Opened, String>
|
pub fn open<Rsc: HasEvents>(
|
||||||
|
rsc: &mut Rsc,
|
||||||
|
ui_state: &mut impl HasRoot<Rsc>,
|
||||||
|
) -> Result<Opened, String>
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
let (opened, tree) = build_screen(rsc)?;
|
let (opened, tree) = build_screen(rsc)?;
|
||||||
ui_state.set_root(tree);
|
ui_state.set_root(rsc, tree);
|
||||||
Ok(opened)
|
Ok(opened)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,14 +224,14 @@ impl TranscriptScreen {
|
|||||||
|
|
||||||
pub fn build<Rsc: HasEvents>(
|
pub fn build<Rsc: HasEvents>(
|
||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
ui_state: &mut impl HasRoot,
|
ui_state: &mut impl HasRoot<Rsc>,
|
||||||
rows: Vec<FoldedRow>,
|
rows: Vec<FoldedRow>,
|
||||||
) -> TranscriptScreen
|
) -> TranscriptScreen
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost + OpenUrl,
|
Rsc::State: FocusHost + OpenUrl,
|
||||||
{
|
{
|
||||||
let (screen, tree) = build_tree(rsc, rows);
|
let (screen, tree) = build_tree(rsc, rows);
|
||||||
ui_state.set_root(tree);
|
ui_state.set_root(rsc, tree);
|
||||||
screen
|
screen
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,10 +281,9 @@ where
|
|||||||
{
|
{
|
||||||
list.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
list.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
||||||
let input = &ctx.data;
|
let input = &ctx.data;
|
||||||
rsc.with_nearest_controller::<SelectionController, _>(
|
rsc.with_nearest_controller::<SelectionController, _>(list, |id, selection, rsc| {
|
||||||
list,
|
selection.drag(id, rsc, input)
|
||||||
|id, selection, rsc| selection.drag(id, rsc, input),
|
});
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,10 +214,9 @@ where
|
|||||||
let (pos, size) = (ctx.data.pos, ctx.data.size);
|
let (pos, size) = (ctx.data.pos, ctx.data.size);
|
||||||
let input = &ctx.data;
|
let input = &ctx.data;
|
||||||
let outcome = rsc
|
let outcome = rsc
|
||||||
.with_nearest_controller::<SelectionController, _>(
|
.with_nearest_controller::<SelectionController, _>(field, |id, selection, rsc| {
|
||||||
field,
|
selection.drag(id, rsc, input)
|
||||||
|id, selection, rsc| selection.drag(id, rsc, input),
|
})
|
||||||
)
|
|
||||||
.unwrap_or(SelectionInput::Tapped);
|
.unwrap_or(SelectionInput::Tapped);
|
||||||
// A *tap*, decided by the same `DragArbiter` the pan and
|
// A *tap*, decided by the same `DragArbiter` the pan and
|
||||||
// the selection are: a gesture that panned the list past
|
// the selection are: a gesture that panned the list past
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ pub(crate) fn on_tap<Rsc: HasEvents>(
|
|||||||
ptr.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
ptr.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
||||||
let input = &ctx.data;
|
let input = &ctx.data;
|
||||||
let outcome = rsc
|
let outcome = rsc
|
||||||
.with_nearest_controller::<SelectionController, _>(
|
.with_nearest_controller::<SelectionController, _>(list, |id, selection, rsc| {
|
||||||
list,
|
selection.drag(id, rsc, input)
|
||||||
|id, selection, rsc| selection.drag(id, rsc, input),
|
})
|
||||||
)
|
|
||||||
.unwrap_or(SelectionInput::Tapped);
|
.unwrap_or(SelectionInput::Tapped);
|
||||||
if outcome == SelectionInput::Tapped {
|
if outcome == SelectionInput::Tapped {
|
||||||
f(rsc);
|
f(rsc);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
.any();
|
.any();
|
||||||
h.state.set_root(root);
|
h.state.set_root(&mut h.rsc, root);
|
||||||
h.frame(0);
|
h.frame(0);
|
||||||
h.frame(PHONE_FRAME_MS);
|
h.frame(PHONE_FRAME_MS);
|
||||||
(h, opened.screen)
|
(h, opened.screen)
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ Framework capabilities needed by `RUST.md`'s port plan:
|
|||||||
range. The shared `TextSelection` engine already computes the same geometry
|
range. The shared `TextSelection` engine already computes the same geometry
|
||||||
for selection highlights; expose one shared primitive rather than giving
|
for selection highlights; expose one shared primitive rather than giving
|
||||||
the app a second text-layout path.
|
the app a second text-layout path.
|
||||||
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
|
|
||||||
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
|
|
||||||
equivalent, and the delete-with-`deleteForeign` confirmation with its
|
|
||||||
toggle switch. Build once, wherever it is first needed, rather than
|
|
||||||
once per screen that wants one.
|
|
||||||
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
|
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
|
||||||
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
|
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
|
||||||
nothing fancier.
|
nothing fancier.
|
||||||
|
|||||||
+8
-8
@@ -678,8 +678,8 @@ it is both. This is an `app-rust` defect, not an iris framework item.
|
|||||||
`AI_SANDBOX_BIG_MB` and `--delay`.
|
`AI_SANDBOX_BIG_MB` and `--delay`.
|
||||||
- [ ] **P1d — images, the session settings dialog, attachments,
|
- [ ] **P1d — images, the session settings dialog, attachments,
|
||||||
usage bar.** `SessionImage` thumbnails (the scaled image
|
usage bar.** `SessionImage` thumbnails (the scaled image
|
||||||
widget), the modal primitive and `SessionSettingsDialog`/
|
widget), the existing overlay facility and
|
||||||
`UsageDialog`, `PendingAttachments` over the attachments
|
`SessionSettingsDialog`/`UsageDialog`, `PendingAttachments` over the attachments
|
||||||
route (`api.rs` gap), `SessionUsageBar` (the gauge widget).
|
route (`api.rs` gap), `SessionUsageBar` (the gauge widget).
|
||||||
- [ ] **P1e — keyboard and insets behaviours** from AGENTS.md's
|
- [ ] **P1e — keyboard and insets behaviours** from AGENTS.md's
|
||||||
"Things that have bitten", re-verified on the phone build:
|
"Things that have bitten", re-verified on the phone build:
|
||||||
@@ -715,11 +715,11 @@ it is both. This is an `app-rust` defect, not an iris framework item.
|
|||||||
**iris widgets missing, → `IRIS_TODO.md`'s "Build (for the port)"
|
**iris widgets missing, → `IRIS_TODO.md`'s "Build (for the port)"
|
||||||
section**: the distance-to-unloaded-edge query P1c needs for its
|
section**: the distance-to-unloaded-edge query P1c needs for its
|
||||||
viewport-sized history cushion; per-range text backgrounds for inline
|
viewport-sized history cushion; per-range text backgrounds for inline
|
||||||
code; a fitted image widget; one modal/dialog primitive reused by the
|
code; a fitted image widget; and a horizontal gauge for
|
||||||
settings and usage dialogs; and a horizontal gauge for
|
`SessionUsageBar`. The settings and usage dialogs can use Iris's
|
||||||
`SessionUsageBar`. Tappable links already exist. Row accessibility
|
existing local overlay hosts. Tappable links already exist. Row
|
||||||
names are app content applied through iris's existing `.label()` API,
|
accessibility names are app content applied through iris's existing
|
||||||
not a missing framework widget.
|
`.label()` API, not a missing framework widget.
|
||||||
|
|
||||||
**Pass condition**: `app/ui-sandbox.sh`'s fixtures driven by
|
**Pass condition**: `app/ui-sandbox.sh`'s fixtures driven by
|
||||||
`ui-trace record --do "tap '<label>'"` — a session with the big
|
`ui-trace record --do "tap '<label>'"` — a session with the big
|
||||||
@@ -790,7 +790,7 @@ it is both. This is an `app-rust` defect, not an iris framework item.
|
|||||||
dimmed, drained of colour and labelled with the operation in progress
|
dimmed, drained of colour and labelled with the operation in progress
|
||||||
without blocking the list's scroll — so it belongs in `src/ui`, not
|
without blocking the list's scroll — so it belongs in `src/ui`, not
|
||||||
iris. `uniqueItems` is app logic there too. Iris itself still needs
|
iris. `uniqueItems` is app logic there too. Iris itself still needs
|
||||||
the modal primitive P1 flagged and a toggle switch for the
|
a toggle switch for the
|
||||||
delete-with-`deleteForeign` flow.
|
delete-with-`deleteForeign` flow.
|
||||||
|
|
||||||
**Pass condition**: `ui-trace` tap-by-name on all four tabs against
|
**Pass condition**: `ui-trace` tap-by-name on all four tabs against
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ impl ControllerId {
|
|||||||
pub fn host(self) -> WidgetId {
|
pub fn host(self) -> WidgetId {
|
||||||
self.host
|
self.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is<C: 'static>(self) -> bool {
|
||||||
|
self.kind == TypeId::of::<C>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[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 {
|
fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult {
|
||||||
CommandResult::Unused
|
CommandResult::Unused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn blocks_prior_input(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControllerManager<Rsc> {
|
pub struct ControllerManager<Rsc> {
|
||||||
@@ -206,6 +214,16 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
|
|||||||
self.command_target
|
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 {
|
pub(crate) fn command_target_revision(&self) -> u64 {
|
||||||
self.command_target_revision
|
self.command_target_revision
|
||||||
}
|
}
|
||||||
@@ -214,6 +232,18 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
|
|||||||
self.command_boundary
|
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>) {
|
pub(crate) fn set_command_boundary(&mut self, boundary: Option<WidgetId>) {
|
||||||
self.command_boundary = boundary;
|
self.command_boundary = boundary;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
|||||||
|
|
||||||
fn run_command(&mut self, command: Command) -> CommandResult {
|
fn run_command(&mut self, command: Command) -> CommandResult {
|
||||||
let revision = self.events().controllers.command_target_revision();
|
let revision = self.events().controllers.command_target_revision();
|
||||||
if self
|
if let Some(boundary) = self.events().controllers.command_boundary()
|
||||||
|
&& self
|
||||||
.events()
|
.events()
|
||||||
.controllers
|
.controllers
|
||||||
.command_target()
|
.command_target()
|
||||||
.is_some_and(|target| {
|
.is_none_or(|target| !self.events().controllers.is_below(target.host(), boundary))
|
||||||
self.events().controllers.command_boundary() == Some(target.host())
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return CommandResult::Unused;
|
return CommandResult::Unused;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
let id = self.add_strong(rsc);
|
||||||
root.set_root(id);
|
root.set_root(rsc, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HasRoot {
|
pub trait HasRoot<Rsc> {
|
||||||
fn set_root(&mut self, root: StrongWidget);
|
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl DefaultAppState for State {
|
|||||||
.ui
|
.ui
|
||||||
.widgets
|
.widgets
|
||||||
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End));
|
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End));
|
||||||
ui_state.set_root(root.any());
|
ui_state.set_root(rsc, root.any());
|
||||||
Self {
|
Self {
|
||||||
ui_state,
|
ui_state,
|
||||||
span: span_weak,
|
span: span_weak,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ impl DefaultAppState for State {
|
|||||||
.masked()
|
.masked()
|
||||||
.background(rect(PaintId::WHITE))
|
.background(rect(PaintId::WHITE))
|
||||||
.add_strong(rsc);
|
.add_strong(rsc);
|
||||||
ui_state.set_root(root.any());
|
ui_state.set_root(rsc, root.any());
|
||||||
|
|
||||||
Self { ui_state }
|
Self { ui_state }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
|
|||||||
|
|
||||||
impl<State: AndroidAppState> IrisViewPeer<State> {
|
impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||||
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
|
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) {
|
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ impl AndroidUiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasRoot for AndroidUiState {
|
impl<State: 'static> HasRoot<AndroidRsc<State>> for AndroidUiState {
|
||||||
fn set_root(&mut self, root: StrongWidget) {
|
fn set_root(&mut self, rsc: &mut AndroidRsc<State>, root: StrongWidget) {
|
||||||
self.root = Some(root);
|
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
|
// `android/insets.rs`'s doc comment for why insets could not take
|
||||||
// the same shortcut.
|
// the same shortcut.
|
||||||
if key_code == Keycode::Back {
|
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);
|
let handled = self.state.back_pressed(&mut self.rsc, &mut self.render);
|
||||||
if handled {
|
if handled {
|
||||||
self.after_input(ctx);
|
self.after_input(ctx);
|
||||||
}
|
}
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
if self.rsc.events.controllers.command_target_blocks_input() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
let handled = super::input::on_key(
|
let handled = super::input::on_key(
|
||||||
&mut self.rsc,
|
&mut self.rsc,
|
||||||
&mut self.state,
|
&mut self.state,
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ pub struct DefaultUiState {
|
|||||||
pub access: AccessTree,
|
pub access: AccessTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasRoot for DefaultUiState {
|
impl<State: 'static> HasRoot<DefaultRsc<State>> for DefaultUiState {
|
||||||
fn set_root(&mut self, root: StrongWidget) {
|
fn set_root(&mut self, rsc: &mut DefaultRsc<State>, root: StrongWidget) {
|
||||||
self.root = Some(root);
|
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +371,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
CommandResult::Unused => false,
|
CommandResult::Unused => false,
|
||||||
};
|
};
|
||||||
if !command_used
|
if !command_used
|
||||||
|
&& !rsc.events.controllers.command_target_blocks_input()
|
||||||
&& let Some(sel) = ui_state.focus
|
&& let Some(sel) = ui_state.focus
|
||||||
&& event.state.is_pressed()
|
&& event.state.is_pressed()
|
||||||
{
|
{
|
||||||
@@ -402,7 +403,9 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::Ime(ime) => {
|
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);
|
let mut text = sel.edit(rsc);
|
||||||
match ime {
|
match ime {
|
||||||
Ime::Enabled | Ime::Disabled => (),
|
Ime::Enabled | Ime::Disabled => (),
|
||||||
|
|||||||
+3
-3
@@ -135,9 +135,9 @@ impl HarnessState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasRoot for HarnessState {
|
impl HasRoot<HarnessRsc> for HarnessState {
|
||||||
fn set_root(&mut self, root: StrongWidget) {
|
fn set_root(&mut self, rsc: &mut HarnessRsc, root: StrongWidget) {
|
||||||
self.root = Some(root);
|
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub mod attr;
|
|||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod harness;
|
pub mod harness;
|
||||||
|
pub mod overlay;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod sense;
|
pub mod sense;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
@@ -43,6 +44,7 @@ pub mod prelude {
|
|||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use iris_core::*;
|
pub use iris_core::*;
|
||||||
pub use iris_macro::*;
|
pub use iris_macro::*;
|
||||||
|
pub use overlay::*;
|
||||||
pub use platform::*;
|
pub use platform::*;
|
||||||
pub use sense::*;
|
pub use sense::*;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -624,7 +624,7 @@ mod controller_tests {
|
|||||||
let found = rsc
|
let found = rsc
|
||||||
.events()
|
.events()
|
||||||
.controllers
|
.controllers
|
||||||
.nearest_id::<SelectionController>(leaf.id(), &render)
|
.nearest_id::<SelectionController>(leaf.id())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(found.host(), inner.id());
|
assert_eq!(found.host(), inner.id());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ pub struct ClientWidgets {
|
|||||||
pub info: WeakWidget<Text>,
|
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
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fn solid_paints_and_images_round_trip_through_an_srgb_target() {
|
|||||||
.span(Dir::RIGHT)
|
.span(Dir::RIGHT)
|
||||||
.add_strong(&mut harness.rsc)
|
.add_strong(&mut harness.rsc)
|
||||||
.any();
|
.any();
|
||||||
harness.state.set_root(root);
|
harness.state.set_root(&mut harness.rsc, root);
|
||||||
harness.frame(0);
|
harness.frame(0);
|
||||||
|
|
||||||
let mut renderer =
|
let mut renderer =
|
||||||
|
|||||||
Reference in new issue
Block a user