Add scoped overlay hosts
This commit is contained in:
1 parent
a33fbca966
commit
44a5da378b
14 files changed
+882
-27
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
.events()
|
||||
.controllers
|
||||
.nearest_id::<SelectionController>(leaf.id(), &render)
|
||||
.nearest_id::<SelectionController>(leaf.id())
|
||||
.unwrap();
|
||||
assert_eq!(found.host(), inner.id());
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in new issue
Block a user