1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! The nodes that can take a window over, counted while an accessibility
//! client needs the modal walk, so the shell skips the walk while there are
//! none.
//!
//! Modality comes from the generic `.semantics` closure, which backs almost
//! every interactive node, so no node type can be ruled out without running
//! its closure. The count runs closures only for nodes whose semantics may
//! have changed, and only outside composition, where a closure's state reads
//! subscribe nothing.
use std::cell::{Cell, RefCell};
use cranpose_core::{Applier, MemoryApplier, Node, NodeId};
use cranpose_foundation::SemanticsReach;
use smallvec::SmallVec;
use crate::{subcompose_layout::SubcomposeLayoutNode, widgets::nodes::LayoutNode};
thread_local! {
/// App contexts on this thread that count their modal nodes. While none
/// does, a semantics change costs one read of this.
static COUNTING_CONTEXTS: Cell<usize> = const { Cell::new(0) };
}
/// One app context's modal nodes.
#[derive(Default)]
pub(crate) struct ModalNodes {
counting: Cell<bool>,
/// Nodes whose reach was modal when last read.
modal: RefCell<SmallVec<[NodeId; 2]>>,
/// Nodes whose reach may have changed since the count was settled.
changed: RefCell<Vec<NodeId>>,
}
impl ModalNodes {
fn set_counting(&self, counting: bool) {
if self.counting.replace(counting) == counting {
return;
}
let _ = COUNTING_CONTEXTS.try_with(|contexts| {
contexts.set(if counting {
contexts.get() + 1
} else {
contexts.get().saturating_sub(1)
});
});
self.modal.borrow_mut().clear();
self.changed.borrow_mut().clear();
}
fn mark(&self, id: NodeId, modal: bool) {
let mut ids = self.modal.borrow_mut();
match (modal, ids.iter().position(|&known| known == id)) {
(true, None) => ids.push(id),
(false, Some(index)) => {
ids.swap_remove(index);
}
_ => {}
}
}
/// Reads the reach of every node queued since the last settle, and drops
/// the modal nodes that left the applier. No borrow is held while a
/// closure runs.
fn settle(&self, applier: &mut MemoryApplier) {
loop {
let Some(id) = self.changed.borrow_mut().pop() else {
break;
};
self.mark(id, modal_at(applier, id));
}
let mut index = 0;
loop {
let Some(id) = self.modal.borrow().get(index).copied() else {
break;
};
if modal_at(applier, id) {
index += 1;
} else {
self.modal.borrow_mut().swap_remove(index);
}
}
}
}
/// Whether the node `applier` holds under `id` is a modal layout node.
fn modal_at(applier: &mut MemoryApplier, id: NodeId) -> bool {
applier
.get_mut(id)
.ok()
.and_then(node_reach)
.is_some_and(|(node_id, reach)| node_id == id && reach.is_modal)
}
impl Drop for ModalNodes {
fn drop(&mut self) {
self.set_counting(false);
}
}
/// A layout node's id and reach, or `None` for any other node.
fn node_reach(node: &mut dyn Node) -> Option<(NodeId, SemanticsReach)> {
let node = node.as_any_mut();
if let Some(layout) = node.downcast_ref::<LayoutNode>() {
return Some((layout.node_id()?, layout.semantics_reach()));
}
let subcompose = node.downcast_ref::<SubcomposeLayoutNode>()?;
Some((subcompose.id()?, subcompose.semantics_reach()))
}
/// Queues `node` for the modal count after its semantics may have changed:
/// its modifiers synced, it was marked for semantics, or it was mounted.
pub(crate) fn reach_changed(node: Option<NodeId>) {
let Some(node) = node else {
return;
};
if COUNTING_CONTEXTS.try_with(Cell::get).unwrap_or(0) == 0 {
return;
}
let _ = crate::render_state::with_current_modal_nodes(|nodes| {
if nodes.counting.get() {
nodes.changed.borrow_mut().push(node);
}
});
}
/// Whether a modal node may be live in the current app context.
///
/// While `count` holds, the context keeps its modal nodes: the first call
/// reads every node of `applier` once, and later calls read only the nodes
/// whose semantics changed since. Without `count` nothing is kept and the
/// answer is always `true`, leaving the caller to walk.
pub fn modal_node_may_be_live(applier: &mut MemoryApplier, count: bool) -> bool {
crate::render_state::with_current_modal_nodes(|nodes| {
if !count {
nodes.set_counting(false);
return true;
}
if nodes.counting.get() {
nodes.settle(applier);
} else {
nodes.set_counting(true);
applier.for_each_node_mut(|node| {
if let Some((id, reach)) = node_reach(node) {
nodes.mark(id, reach.is_modal);
}
});
}
!nodes.modal.borrow().is_empty()
})
.unwrap_or(true)
}
#[cfg(test)]
#[path = "tests/modal_nodes_tests.rs"]
mod tests;