1use std::cell::Cell;
11use std::sync::{Arc, Mutex};
12
13use kurbo::{Rect, Size};
14
15use crate::event::PassBracket;
16use crate::view::AnyView;
17
18thread_local! {
19 static PUBLISHED: Cell<Option<SelectionToolbarRequest>> = const { Cell::new(None) };
22
23 static PASS_OPEN: Cell<bool> = const { Cell::new(false) };
24}
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
29pub struct SelectionToolbarActions {
30 pub copy: bool,
31 pub cut: bool,
32 pub paste: bool,
33 pub select_all: bool,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct SelectionToolbarRequest {
40 pub anchor: Rect,
43 pub actions: SelectionToolbarActions,
45 pub present_menu: bool,
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
51pub enum SelectionToolbarPolicy {
52 #[default]
54 Framework,
55 Native,
57}
58
59#[derive(Clone, Copy, Debug)]
61struct PolicyState {
62 value: SelectionToolbarPolicy,
63 locked: bool,
64}
65
66static POLICY: Mutex<PolicyState> = Mutex::new(PolicyState {
67 value: SelectionToolbarPolicy::Framework,
68 locked: false,
69});
70
71pub fn set_selection_toolbar_policy(policy: SelectionToolbarPolicy) {
74 let mut state = POLICY.lock().unwrap_or_else(|e| e.into_inner());
75 if state.locked {
76 #[cfg(debug_assertions)]
77 eprintln!(
78 "frust-core: the selection-toolbar policy is locked to {:?} by the platform shell; \
79 ignoring an app's set_selection_toolbar_policy({policy:?}) call (see \
80 frust_core::lock_selection_toolbar_policy)",
81 state.value
82 );
83 return;
84 }
85 state.value = policy;
86}
87
88pub fn selection_toolbar_policy() -> SelectionToolbarPolicy {
90 POLICY.lock().unwrap_or_else(|e| e.into_inner()).value
91}
92
93pub fn lock_selection_toolbar_policy(policy: SelectionToolbarPolicy) -> bool {
96 let mut state = POLICY.lock().unwrap_or_else(|e| e.into_inner());
97 if state.locked {
98 return false;
99 }
100 state.value = policy;
101 state.locked = true;
102 true
103}
104
105pub type SelectionToolbarBuilder =
108 Arc<dyn Fn(&SelectionToolbarRequest, Size) -> AnyView<()> + Send + Sync>;
109
110static BUILDER: Mutex<Option<SelectionToolbarBuilder>> = Mutex::new(None);
113
114pub fn set_selection_toolbar_builder(builder: SelectionToolbarBuilder) {
116 *BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(builder);
117}
118
119pub fn install_selection_toolbar_builder_if_unset(builder: SelectionToolbarBuilder) -> bool {
122 let mut slot = BUILDER.lock().unwrap_or_else(|e| e.into_inner());
123 if slot.is_some() {
124 return false;
125 }
126 *slot = Some(builder);
127 true
128}
129
130pub fn selection_toolbar_builder() -> Option<SelectionToolbarBuilder> {
132 BUILDER
133 .lock()
134 .unwrap_or_else(|e| e.into_inner())
135 .as_ref()
136 .map(Arc::clone)
137}
138
139pub(crate) struct SelectionToolbarPass(PassBracket<Option<SelectionToolbarRequest>>);
141
142impl SelectionToolbarPass {
143 pub(crate) fn enter() -> Self {
144 SelectionToolbarPass(PassBracket::enter(&PUBLISHED, &PASS_OPEN))
145 }
146
147 pub(crate) fn take(&self) -> Option<SelectionToolbarRequest> {
148 self.0.take()
149 }
150}
151
152pub(crate) fn publish(request: SelectionToolbarRequest) {
154 PUBLISHED.with(|slot| slot.set(Some(request)));
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use std::sync::Mutex as StdMutex;
161
162 static TEST_LOCK: StdMutex<()> = StdMutex::new(());
168
169 fn toolbar_request() -> SelectionToolbarRequest {
170 SelectionToolbarRequest {
171 anchor: Rect::new(10.0, 20.0, 110.0, 40.0),
172 actions: SelectionToolbarActions {
173 copy: true,
174 cut: true,
175 paste: false,
176 select_all: true,
177 },
178 present_menu: true,
179 }
180 }
181
182 #[test]
183 fn policy_defaults_to_framework_and_round_trips() {
184 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
185 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
186 assert_eq!(
187 selection_toolbar_policy(),
188 SelectionToolbarPolicy::Framework
189 );
190 assert_eq!(
191 SelectionToolbarPolicy::default(),
192 SelectionToolbarPolicy::Framework,
193 "the default route is the framework-drawn one"
194 );
195
196 set_selection_toolbar_policy(SelectionToolbarPolicy::Native);
197 assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
198
199 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
201 }
202
203 fn unlock_selection_toolbar_policy_for_test() {
211 POLICY.lock().unwrap_or_else(|e| e.into_inner()).locked = false;
212 }
213
214 #[test]
215 fn set_policy_is_unaffected_when_nothing_has_locked_it() {
216 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
217 set_selection_toolbar_policy(SelectionToolbarPolicy::Native);
222 assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
223 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
224 assert_eq!(
225 selection_toolbar_policy(),
226 SelectionToolbarPolicy::Framework
227 );
228 }
229
230 #[test]
231 fn locking_the_policy_sets_the_declared_value() {
232 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
233 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
234
235 assert!(
236 lock_selection_toolbar_policy(SelectionToolbarPolicy::Native),
237 "the first claim always takes the lock"
238 );
239 assert_eq!(
240 selection_toolbar_policy(),
241 SelectionToolbarPolicy::Native,
242 "claiming the lock sets the value it declares"
243 );
244
245 unlock_selection_toolbar_policy_for_test();
246 }
247
248 #[test]
249 fn a_locked_policy_refuses_a_later_set_and_a_later_claim() {
250 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
251 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
252 assert!(lock_selection_toolbar_policy(
253 SelectionToolbarPolicy::Native
254 ));
255
256 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
258 assert_eq!(
259 selection_toolbar_policy(),
260 SelectionToolbarPolicy::Native,
261 "a locked slot must not move for an app's later set_selection_toolbar_policy call"
262 );
263
264 assert!(
266 !lock_selection_toolbar_policy(SelectionToolbarPolicy::Framework),
267 "a second lock claim must not displace the first"
268 );
269 assert_eq!(selection_toolbar_policy(), SelectionToolbarPolicy::Native);
270
271 unlock_selection_toolbar_policy_for_test();
273 set_selection_toolbar_policy(SelectionToolbarPolicy::Framework);
274 }
275
276 #[test]
277 fn install_if_unset_yields_to_an_installed_builder_but_set_replaces_it() {
278 let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
279 *BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = None;
281 assert!(
282 selection_toolbar_builder().is_none(),
283 "nothing is installed at rest"
284 );
285
286 let first: SelectionToolbarBuilder = Arc::new(|_req, _size| crate::view::any(FirstView));
287 assert!(
288 install_selection_toolbar_builder_if_unset(Arc::clone(&first)),
289 "the first cooperative install takes the empty slot"
290 );
291
292 let second: SelectionToolbarBuilder = Arc::new(|_req, _size| crate::view::any(SecondView));
293 assert!(
294 !install_selection_toolbar_builder_if_unset(Arc::clone(&second)),
295 "a second cooperative install must not displace the first"
296 );
297 assert!(
298 Arc::ptr_eq(
299 &selection_toolbar_builder().expect("a builder is installed"),
300 &first
301 ),
302 "the slot still holds the first builder"
303 );
304
305 set_selection_toolbar_builder(Arc::clone(&second));
306 assert!(
307 Arc::ptr_eq(
308 &selection_toolbar_builder().expect("a builder is installed"),
309 &second
310 ),
311 "an explicit set replaces whatever stood"
312 );
313
314 *BUILDER.lock().unwrap_or_else(|e| e.into_inner()) = None;
315 }
316
317 #[test]
318 fn publish_is_pass_scoped_and_last_writer_wins() {
319 let pass = SelectionToolbarPass::enter();
320 assert_eq!(pass.take(), None, "a fresh pass starts empty");
321
322 publish(toolbar_request());
323 let mut second = toolbar_request();
324 second.actions.paste = true;
325 publish(second);
326 assert_eq!(
327 pass.take(),
328 Some(second),
329 "the last publish of the pass is the one resolved"
330 );
331 assert_eq!(pass.take(), None, "and the drain empties the slot");
332 drop(pass);
333
334 publish(toolbar_request());
337 let next = SelectionToolbarPass::enter();
338 assert_eq!(
339 next.take(),
340 None,
341 "a stray publish does not survive into the next pass"
342 );
343 }
344
345 struct FirstView;
349 struct SecondView;
351
352 macro_rules! stub_view {
353 ($name:ident) => {
354 impl crate::view::View<()> for $name {
355 type Element = StubWidget;
356 fn build(&self, _ctx: &mut crate::view::BuildCtx<'_>) -> StubWidget {
357 StubWidget
358 }
359 fn rebuild(
360 &self,
361 _prev: &Self,
362 _element: &mut StubWidget,
363 _ctx: &mut crate::view::BuildCtx<'_>,
364 ) -> crate::view::ChangeFlags {
365 crate::view::ChangeFlags::NONE
366 }
367 }
368 };
369 }
370 stub_view!(FirstView);
371 stub_view!(SecondView);
372
373 struct StubWidget;
375 impl crate::widget::Widget for StubWidget {
376 fn layout(
377 &mut self,
378 _ctx: &mut crate::widget::LayoutCtx,
379 _bc: &crate::layout::BoxConstraints,
380 ) -> Size {
381 Size::ZERO
382 }
383 fn paint(
384 &mut self,
385 _ctx: &mut crate::widget::PaintCtx,
386 _scene: &mut dyn crate::widget::PaintScene,
387 ) {
388 }
389 }
390}