1use std::cell::{Cell, RefCell};
16use std::collections::HashMap;
17use std::rc::Rc;
18
19use day_spec::{ToolbarItem, ToolbarPatch, ToolbarValue};
20
21use crate::tree::{RNode, with_tree};
22
23type ValueAction = Rc<dyn Fn(&ToolbarValue)>;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum Chrome {
33 Window(RNode),
35 Page(RNode),
38}
39
40struct PageFrame {
43 page: RNode,
44 active: Option<Rc<dyn Fn() -> bool>>,
45 column: day_spec::ToolbarColumn,
46 window: RNode,
51}
52
53struct Contribution {
55 chrome: Chrome,
56 seq: u64,
59 items: Vec<ToolbarItem>,
60 window: RNode,
62 document_window: Option<DocumentWindow>,
63 active: Option<Rc<dyn Fn() -> bool>>,
68}
69
70#[derive(Clone)]
73pub struct DocumentWindow(Rc<Cell<RNode>>);
74impl DocumentWindow {
75 pub fn new(root: RNode) -> Self {
76 Self(Rc::new(Cell::new(root)))
77 }
78 pub fn set(&self, root: RNode) {
79 if self.0.replace(root) != root {
80 schedule_recompose();
81 }
82 }
83}
84impl Contribution {
85 fn window(&self) -> RNode {
86 self.document_window
87 .as_ref()
88 .map_or(self.window, |w| w.0.get())
89 }
90}
91
92day_reactive::tls_slots! {
93 toolbar;
94 static VALUE_ACTIONS: RefCell<HashMap<u64, ValueAction>> = RefCell::new(HashMap::new());
98 static CONTRIBUTIONS: RefCell<HashMap<u64, Contribution>> = RefCell::new(HashMap::new());
100 static MODELS: RefCell<Vec<(Chrome, Vec<ToolbarItem>)>> = const { RefCell::new(Vec::new()) };
104 static SLOT_IDS: RefCell<HashMap<(RNode, String), u64>> = RefCell::new(HashMap::new());
107 static SLOT_TARGETS: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new());
109 static DELIVERED: RefCell<HashMap<RNode, Vec<ToolbarItem>>> = RefCell::new(HashMap::new());
114 static RECOMPOSE_PENDING: Cell<bool> = const { Cell::new(false) };
116 static RECOMPOSING: Cell<bool> = const { Cell::new(false) };
117 static RECOMPOSE_AGAIN: Cell<bool> = const { Cell::new(false) };
118 static NEXT_TOKEN: Cell<u64> = const { Cell::new(1) };
120 static BUILDING: Cell<Option<RNode>> = const { Cell::new(None) };
122 static PAGE_STACK: RefCell<Vec<PageFrame>> = RefCell::new(Vec::new());
126}
127
128pub(crate) fn with_window<R>(root: RNode, f: impl FnOnce() -> R) -> R {
131 struct Restore(Option<RNode>);
135 impl Drop for Restore {
136 fn drop(&mut self) {
137 BUILDING.with(|b| b.set(self.0));
138 }
139 }
140 let _restore = Restore(BUILDING.with(|b| b.replace(Some(root))));
141 f()
142}
143
144pub fn with_page<R>(page: RNode, f: impl FnOnce() -> R) -> R {
147 with_page_in(
148 page,
149 None,
150 day_spec::ToolbarColumn::Detail,
151 window_being_built(),
152 f,
153 )
154}
155
156pub fn with_page_in<R>(
159 page: RNode,
160 active: Option<Rc<dyn Fn() -> bool>>,
161 column: day_spec::ToolbarColumn,
162 window: RNode,
163 f: impl FnOnce() -> R,
164) -> R {
165 struct Restore;
166 impl Drop for Restore {
167 fn drop(&mut self) {
168 PAGE_STACK.with(|s| {
169 s.borrow_mut().pop();
170 });
171 }
172 }
173 PAGE_STACK.with(|s| {
174 s.borrow_mut().push(PageFrame {
175 page,
176 active,
177 column,
178 window,
179 })
180 });
181 let _restore = Restore;
182 f()
183}
184
185pub fn with_page_gated<R>(
189 page: RNode,
190 active: Option<Rc<dyn Fn() -> bool>>,
191 column: day_spec::ToolbarColumn,
192 f: impl FnOnce() -> R,
193) -> R {
194 with_page_in(page, active, column, window_being_built(), f)
195}
196
197pub fn current_page_window() -> RNode {
199 PAGE_STACK
200 .with(|s| s.borrow().last().map(|f| f.window))
201 .unwrap_or_else(window_being_built)
202}
203
204pub fn current_page_gate() -> Option<Rc<dyn Fn() -> bool>> {
206 PAGE_STACK.with(|s| s.borrow().last().and_then(|f| f.active.clone()))
207}
208
209pub fn current_page_column() -> day_spec::ToolbarColumn {
211 PAGE_STACK.with(|s| {
212 s.borrow()
213 .last()
214 .map(|f| f.column)
215 .unwrap_or(day_spec::ToolbarColumn::Window)
216 })
217}
218
219pub fn window_being_built() -> RNode {
223 BUILDING
224 .with(|b| b.get())
225 .unwrap_or_else(|| with_tree(|t| t.root_node()))
226}
227
228pub fn current_chrome() -> Chrome {
235 match PAGE_STACK.with(|s| s.borrow().last().map(|f| f.page)) {
236 Some(page) => Chrome::Page(page),
237 None => Chrome::Window(window_being_built()),
238 }
239}
240
241pub fn register_toolbar_value(f: Rc<dyn Fn(&ToolbarValue)>) -> u64 {
244 let id = crate::menu::next_action_id();
245 VALUE_ACTIONS.with(|m| m.borrow_mut().insert(id, f));
246 id
247}
248
249pub fn dispatch_toolbar_value(action: u64, value: &ToolbarValue) {
253 mirror_value(action, value);
254 let target = resolve_slot(action);
255 let f = VALUE_ACTIONS.with(|m| m.borrow().get(&target).cloned());
256 if let Some(f) = f {
257 day_reactive::batch(|| f(value));
258 }
259}
260
261pub fn synthesize_toolbar_value(action: u64, value: &ToolbarValue) {
267 if let ToolbarValue::Text(text) = value {
268 let target = MODELS.with(|m| {
269 m.borrow().iter().find_map(|(c, items)| match c {
270 Chrome::Window(root) => items
271 .iter()
272 .find(|i| i.action == action)
273 .map(|i| (*root, i.id.clone())),
274 Chrome::Page(_) => None,
275 })
276 });
277 if let Some((root, item)) = target {
278 let patch = ToolbarPatch::Text {
279 item,
280 text: text.clone(),
281 };
282 if with_tree(|t| t.patch_window_toolbar(root, patch.clone())) {
283 deliver_value(root, &patch);
284 }
285 }
286 }
287 dispatch_toolbar_value(action, value);
288}
289
290pub(crate) fn resolve_slot(id: u64) -> u64 {
293 SLOT_TARGETS.with(|m| m.borrow().get(&id).copied().unwrap_or(id))
294}
295
296pub fn register_contribution(chrome: Chrome, items: Vec<ToolbarItem>) -> u64 {
299 register_contribution_gated(chrome, items, None)
300}
301
302pub fn register_contribution_gated(
306 chrome: Chrome,
307 items: Vec<ToolbarItem>,
308 active: Option<Rc<dyn Fn() -> bool>>,
309) -> u64 {
310 let token = NEXT_TOKEN.with(|n| {
311 let t = n.get();
312 n.set(t + 1);
313 t
314 });
315 let window = current_page_window();
316 CONTRIBUTIONS.with(|m| {
317 m.borrow_mut().insert(
318 token,
319 Contribution {
320 chrome,
321 seq: token,
322 items,
323 window,
324 document_window: day_reactive::Scope::current().use_context::<DocumentWindow>(),
325 active,
326 },
327 )
328 });
329 let _ = chrome;
330 schedule_recompose();
331 token
332}
333
334fn merged_window(root: RNode) -> Vec<ToolbarItem> {
337 let mut live: Vec<(u64, Vec<ToolbarItem>)> = CONTRIBUTIONS.with(|m| {
338 m.borrow()
339 .values()
340 .filter(|c| c.window() == root)
341 .filter(|c| {
342 c.active
343 .as_ref()
344 .is_none_or(|f| day_reactive::untrack(|| f()))
345 })
346 .map(|c| (c.seq, c.items.clone()))
347 .collect()
348 });
349 live.sort_by_key(|(seq, _)| *seq);
350 let mut out: Vec<ToolbarItem> = live.into_iter().flat_map(|(_, items)| items).collect();
351 out.sort_by_key(|i| placement_rank(i.placement));
352 out
353}
354
355pub fn update_contribution(token: u64, items: Vec<ToolbarItem>) {
357 let old = CONTRIBUTIONS.with(|m| {
358 let mut m = m.borrow_mut();
359 let c = m.get_mut(&token)?;
360 Some(std::mem::replace(&mut c.items, items))
361 });
362 if let Some(old) = old {
363 forget_closures(&old);
364 schedule_recompose();
365 }
366}
367
368pub fn relabel_contribution(token: u64, texts: &[(String, Option<String>)]) {
372 let Some(mut items) = CONTRIBUTIONS.with(|m| m.borrow().get(&token).map(|c| c.items.clone()))
373 else {
374 return;
375 };
376 for (item, (label, tooltip)) in items.iter_mut().zip(texts) {
377 item.label = label.clone();
378 item.tooltip = tooltip.clone();
379 }
380 update_contribution(token, items);
381}
382
383pub fn unregister_contribution(token: u64) {
386 let old = CONTRIBUTIONS.with(|m| m.borrow_mut().remove(&token).map(|c| c.items));
387 if let Some(old) = old {
388 forget_closures(&old);
389 schedule_recompose();
390 }
391}
392
393fn forget_closures(items: &[ToolbarItem]) {
400 let live: std::collections::HashSet<u64> = CONTRIBUTIONS.with(|m| {
401 m.borrow()
402 .values()
403 .flat_map(|c| c.items.iter().map(|i| i.action))
404 .collect()
405 });
406 for item in items
407 .iter()
408 .filter(|i| i.action != 0 && !live.contains(&i.action))
409 {
410 VALUE_ACTIONS.with(|m| m.borrow_mut().remove(&item.action));
411 if matches!(
412 item.kind,
413 day_spec::ToolbarItemKind::Button | day_spec::ToolbarItemKind::Menu { .. }
414 ) {
415 crate::menu::forget_action(item.action);
416 }
417 }
418}
419
420fn placement_rank(p: day_spec::ToolbarPlacement) -> u8 {
424 use day_spec::ToolbarPlacement as P;
425 match p {
426 P::Navigation => 0,
427 P::Principal => 1,
428 P::Automatic => 2,
429 P::Primary => 3,
430 P::Secondary | P::Bottom => 4,
431 }
432}
433
434fn mirror_value(action: u64, value: &ToolbarValue) {
443 let Some((chrome, id)) = MODELS.with(|m| {
447 let m = m.borrow();
448 let found = |window: bool| {
449 m.iter().find_map(|(c, items)| {
450 (matches!(c, Chrome::Window(_)) == window)
451 .then(|| items.iter().find(|i| i.action == action))
452 .flatten()
453 .map(|i| (*c, i.id.clone()))
454 })
455 };
456 found(true).or_else(|| found(false))
457 }) else {
458 return;
459 };
460 let patch = match value {
461 ToolbarValue::Text(text) => ToolbarPatch::Text {
462 item: id,
463 text: text.clone(),
464 },
465 ToolbarValue::On(on) => ToolbarPatch::On { item: id, on: *on },
466 ToolbarValue::Selected(index) => ToolbarPatch::Selected {
467 item: id,
468 index: *index,
469 },
470 ToolbarValue::Submit(_) => return,
472 };
473 CONTRIBUTIONS.with(|m| {
474 for c in m.borrow_mut().values_mut().filter(|c| match chrome {
475 Chrome::Window(root) => c.window() == root,
476 Chrome::Page(_) => c.chrome == chrome,
477 }) {
478 apply_to_model(&mut c.items, &patch);
479 }
480 });
481 MODELS.with(|m| {
482 if let Some((_, items)) = m.borrow_mut().iter_mut().find(|(c, _)| *c == chrome) {
483 apply_to_model(items, &patch);
484 }
485 });
486 if let Chrome::Window(root) = chrome {
487 deliver_value(root, &patch);
488 }
489}
490
491fn schedule_recompose() {
501 if RECOMPOSE_PENDING.with(|p| p.replace(true)) {
502 return;
503 }
504 day_reactive::at_turn_end(recompose_windows);
505}
506
507fn recompose_windows() {
516 recompose_serially(recompose_windows_inner);
517}
518
519fn recompose_serially(mut apply: impl FnMut()) {
524 if RECOMPOSING.with(|busy| busy.replace(true)) {
525 RECOMPOSE_AGAIN.with(|again| again.set(true));
526 return;
527 }
528 struct Reset;
529 impl Drop for Reset {
530 fn drop(&mut self) {
531 RECOMPOSING.with(|busy| busy.set(false));
532 RECOMPOSE_AGAIN.with(|again| again.set(false));
533 }
534 }
535 let _reset = Reset;
536 loop {
537 RECOMPOSE_AGAIN.with(|again| again.set(false));
538 apply();
539 if !RECOMPOSE_AGAIN.with(|again| again.get()) {
540 break;
541 }
542 }
543}
544
545fn recompose_windows_inner() {
546 RECOMPOSE_PENDING.with(|p| p.set(false));
547 let roots: Vec<RNode> = {
548 let mut v: Vec<RNode> =
549 CONTRIBUTIONS.with(|m| m.borrow().values().map(|c| c.window()).collect());
550 v.extend(MODELS.with(|m| {
552 m.borrow()
553 .iter()
554 .filter_map(|(c, _)| match c {
555 Chrome::Window(r) => Some(*r),
556 Chrome::Page(_) => None,
557 })
558 .collect::<Vec<_>>()
559 }));
560 v.sort();
561 v.dedup();
562 v
563 };
564 for root in roots {
565 let items = slot_model(root, unique_ids(merged_window(root)));
566 MODELS.with(|m| {
567 let mut m = m.borrow_mut();
568 match m.iter_mut().find(|(c, _)| *c == Chrome::Window(root)) {
569 Some(entry) => entry.1 = items.clone(),
570 None => m.push((Chrome::Window(root), items.clone())),
571 }
572 });
573 let prev = DELIVERED.with(|d| d.borrow().get(&root).cloned().unwrap_or_default());
574 let (ops, patches) = diff_bar(&prev, &items);
575 if ops.is_empty() && patches.is_empty() {
576 continue;
577 }
578 debug_assert!(edit_is_ordered(&ops), "toolbar edit out of order: {ops:?}");
579 let delivered = ops.is_empty() || with_tree(|t| t.edit_window_toolbar(root, ops));
582 if !delivered {
583 continue;
584 }
585 for patch in patches {
586 with_tree(|t| t.patch_window_toolbar(root, patch));
587 }
588 DELIVERED.with(|d| d.borrow_mut().insert(root, items));
589 }
590}
591
592fn edit_is_ordered(ops: &[day_spec::ToolbarOp]) -> bool {
595 use day_spec::ToolbarOp as O;
596 let first_insert = ops
597 .iter()
598 .position(|o| matches!(o, O::Insert { .. }))
599 .unwrap_or(ops.len());
600 let inserts: Vec<usize> = ops[first_insert..]
601 .iter()
602 .filter_map(|o| match o {
603 O::Insert { index, .. } => Some(*index),
604 O::Remove { .. } => None,
605 })
606 .collect();
607 inserts.len() == ops.len() - first_insert && inserts.windows(2).all(|w| w[0] < w[1])
608}
609
610fn deliver_value(root: RNode, patch: &ToolbarPatch) {
613 DELIVERED.with(|d| {
614 if let Some(items) = d.borrow_mut().get_mut(&root) {
615 patch.apply_to(items);
616 }
617 });
618}
619
620fn unique_ids(items: Vec<ToolbarItem>) -> Vec<ToolbarItem> {
627 let mut seen = std::collections::HashSet::new();
628 items
629 .into_iter()
630 .filter(|i| {
631 let fresh = seen.insert(i.id.clone());
632 if !fresh {
633 log::warn!(
634 "toolbar item id {:?} appears twice in one window's bar; only the first is \
635 shown (docs/toolbars.md: ids are unique within a bar)",
636 i.id
637 );
638 }
639 fresh
640 })
641 .collect()
642}
643
644fn diff_bar(
654 prev: &[ToolbarItem],
655 next: &[ToolbarItem],
656) -> (Vec<day_spec::ToolbarOp>, Vec<ToolbarPatch>) {
657 use day_spec::ToolbarOp;
658 let matched: Vec<Option<usize>> = next
660 .iter()
661 .map(|n| {
662 prev.iter()
663 .position(|p| p.id == n.id)
664 .filter(|&at| shape_of(&prev[at]) == shape_of(n))
665 })
666 .collect();
667 let kept = longest_increasing(&matched);
668
669 let mut ops: Vec<ToolbarOp> = prev
670 .iter()
671 .enumerate()
672 .filter(|(at, _)| !kept.iter().any(|&k| matched[k] == Some(*at)))
673 .map(|(_, p)| ToolbarOp::Remove { id: p.id.clone() })
674 .collect();
675 let mut patches = Vec::new();
676 for (index, item) in next.iter().enumerate() {
677 if kept.contains(&index) {
678 let before = &prev[matched[index].unwrap_or_default()];
679 patches.extend(value_patches(before, item));
680 } else {
681 ops.push(ToolbarOp::Insert {
682 index,
683 item: item.clone(),
684 });
685 }
686 }
687 (ops, patches)
688}
689
690fn longest_increasing(seq: &[Option<usize>]) -> Vec<usize> {
692 let mut tails: Vec<usize> = Vec::new();
695 let mut back: Vec<Option<usize>> = vec![None; seq.len()];
696 for (i, v) in seq.iter().enumerate() {
697 let Some(v) = *v else { continue };
698 let k = tails.partition_point(|&t| seq[t].unwrap_or_default() < v);
699 back[i] = k.checked_sub(1).map(|p| tails[p]);
700 if k == tails.len() {
701 tails.push(i);
702 } else {
703 tails[k] = i;
704 }
705 }
706 let mut out = Vec::new();
707 let mut at = tails.last().copied();
708 while let Some(i) = at {
709 out.push(i);
710 at = back[i];
711 }
712 out.reverse();
713 out
714}
715
716fn value_patches(was: &ToolbarItem, now: &ToolbarItem) -> Vec<ToolbarPatch> {
718 use day_spec::ToolbarItemKind as K;
719 let item = now.id.clone();
720 let mut out = Vec::new();
721 match (&was.kind, &now.kind) {
722 (
723 K::Search {
724 text: t0,
725 suggestions: s0,
726 placeholder: p0,
727 },
728 K::Search {
729 text: t1,
730 suggestions: s1,
731 placeholder: p1,
732 },
733 ) => {
734 if t0 != t1 {
735 out.push(ToolbarPatch::Text {
736 item: item.clone(),
737 text: t1.clone(),
738 });
739 }
740 if s0 != s1 {
741 out.push(ToolbarPatch::Suggestions {
742 item: item.clone(),
743 list: s1.clone(),
744 });
745 }
746 if p0 != p1 {
747 out.push(ToolbarPatch::Placeholder {
748 item: item.clone(),
749 text: p1.clone(),
750 });
751 }
752 }
753 (K::Toggle { on: a }, K::Toggle { on: b }) if a != b => out.push(ToolbarPatch::On {
754 item: item.clone(),
755 on: *b,
756 }),
757 (K::Segmented { selected: a, .. }, K::Segmented { selected: b, .. }) if a != b => {
758 out.push(ToolbarPatch::Selected {
759 item: item.clone(),
760 index: *b,
761 })
762 }
763 _ => {}
764 }
765 if was.label != now.label || was.tooltip != now.tooltip {
766 out.push(ToolbarPatch::Label {
767 item: item.clone(),
768 label: now.label.clone(),
769 tooltip: now.tooltip.clone(),
770 });
771 }
772 if was.enabled != now.enabled {
773 out.push(ToolbarPatch::Enabled {
774 item,
775 on: now.enabled,
776 });
777 }
778 out
779}
780
781fn slot_model(root: RNode, mut items: Vec<ToolbarItem>) -> Vec<ToolbarItem> {
789 let mut live: Vec<u64> = Vec::new();
790 for item in &mut items {
791 let key = item.id.clone();
792 item.action = slot_for(root, &key, item.action, &mut live);
793 if let day_spec::ToolbarItemKind::Menu { items: entries } = &mut item.kind {
794 slot_menu(root, &key, entries, &mut live);
795 }
796 }
797 let keep: std::collections::HashSet<u64> = live.into_iter().collect();
800 SLOT_IDS.with(|ids| {
801 SLOT_TARGETS.with(|t| {
802 let mut t = t.borrow_mut();
803 for ((r, _), slot) in ids.borrow().iter() {
804 if *r == root && !keep.contains(slot) {
805 t.remove(slot);
806 }
807 }
808 })
809 });
810 items
811}
812
813fn slot_menu(root: RNode, parent: &str, entries: &mut [day_spec::MenuItem], live: &mut Vec<u64>) {
814 for (i, entry) in entries.iter_mut().enumerate() {
815 let key = format!("{parent}/{i}");
816 match entry {
817 day_spec::MenuItem::Action { action, .. } => {
818 *action = slot_for(root, &key, *action, live);
819 }
820 day_spec::MenuItem::Submenu { items, .. } => slot_menu(root, &key, items, live),
821 day_spec::MenuItem::Separator => {}
822 }
823 }
824}
825
826fn slot_for(root: RNode, key: &str, target: u64, live: &mut Vec<u64>) -> u64 {
829 if target == 0 {
830 return 0;
831 }
832 let slot = SLOT_IDS.with(|ids| {
833 *ids.borrow_mut()
834 .entry((root, key.to_string()))
835 .or_insert_with(crate::menu::next_action_id)
836 });
837 SLOT_TARGETS.with(|t| t.borrow_mut().insert(slot, target));
838 live.push(slot);
839 slot
840}
841
842pub fn chrome_changed() {
846 schedule_recompose();
847}
848
849pub fn patch_toolbar(patch: ToolbarPatch) {
852 let owner = MODELS.with(|m| {
853 m.borrow()
854 .iter()
855 .find(|(_, items)| items.iter().any(|i| i.id == patch.item()))
856 .map(|(c, _)| *c)
857 });
858 if let Some(chrome) = owner {
859 patch_chrome(chrome, patch);
860 }
861}
862
863pub fn patch_chrome(chrome: Chrome, patch: ToolbarPatch) {
866 let window = CONTRIBUTIONS.with(|m| {
869 let mut m = m.borrow_mut();
870 let mut window = None;
871 for c in m.values_mut() {
872 if c.chrome == chrome {
873 apply_to_model(&mut c.items, &patch);
874 window = Some(c.window());
875 }
876 }
877 window
878 });
879 let Some(root) = window else { return };
886 MODELS.with(|m| {
887 let mut m = m.borrow_mut();
888 if let Some((_, items)) = m.iter_mut().find(|(c, _)| *c == Chrome::Window(root)) {
889 apply_to_model(items, &patch);
890 }
891 });
892 if with_tree(|t| t.patch_window_toolbar(root, patch.clone())) {
893 deliver_value(root, &patch);
894 }
895}
896
897fn apply_to_model(items: &mut [ToolbarItem], patch: &ToolbarPatch) {
899 patch.apply_to(items);
900}
901
902fn shape_of(item: &ToolbarItem) -> ToolbarItem {
905 use day_spec::ToolbarItemKind as K;
906 let mut i = item.clone();
907 i.enabled = true;
908 i.label.clear();
910 i.tooltip = None;
911 match &mut i.kind {
912 K::Search {
913 text,
914 suggestions,
915 placeholder,
916 } => {
917 text.clear();
918 suggestions.clear();
919 placeholder.clear();
920 }
921 K::Toggle { on } => *on = false,
922 K::Segmented { selected, .. } => *selected = 0,
923 _ => {}
924 }
925 i
926}
927
928pub fn toolbar_model() -> Vec<ToolbarItem> {
932 MODELS.with(|m| m.borrow().iter().flat_map(|(_, i)| i.clone()).collect())
933}
934
935pub fn toggle_sidebar(host: RNode) -> bool {
941 with_tree(|t| t.toggle_sidebar(host))
942}
943
944pub(crate) fn forget_window(root: RNode) {
953 let gone: Vec<ToolbarItem> = CONTRIBUTIONS.with(|m| {
954 let mut m = m.borrow_mut();
955 let mut gone = Vec::new();
956 m.retain(|_, c| {
957 let mine = c.window() == root;
958 if mine {
959 gone.extend(c.items.clone());
960 }
961 !mine
962 });
963 gone
964 });
965 forget_closures(&gone);
966 MODELS.with(|m| {
967 m.borrow_mut()
968 .retain(|(c, _)| !matches!(c, Chrome::Window(r) if *r == root))
969 });
970 DELIVERED.with(|d| {
971 d.borrow_mut().remove(&root);
972 });
973 SLOT_IDS.with(|ids| {
974 SLOT_TARGETS.with(|t| {
975 let mut t = t.borrow_mut();
976 ids.borrow_mut().retain(|(r, _), slot| {
977 if *r == root {
978 t.remove(slot);
979 }
980 *r != root
981 });
982 })
983 });
984}
985
986pub fn reset_toolbars() {
988 MODELS.with(|m| m.borrow_mut().clear());
989 CONTRIBUTIONS.with(|m| m.borrow_mut().clear());
990 VALUE_ACTIONS.with(|m| m.borrow_mut().clear());
991 SLOT_IDS.with(|m| m.borrow_mut().clear());
992 SLOT_TARGETS.with(|m| m.borrow_mut().clear());
993 DELIVERED.with(|m| m.borrow_mut().clear());
994 RECOMPOSE_PENDING.with(|p| p.set(false));
995 PAGE_STACK.with(|s| s.borrow_mut().clear());
996 BUILDING.with(|b| b.set(None));
997}
998
999pub fn focus_toolbar_search() -> bool {
1002 let root = crate::windows::focused_root();
1003 let has = MODELS.with(|models| {
1004 models.borrow().iter().any(|(chrome, items)| {
1005 *chrome == Chrome::Window(root)
1006 && items.iter().any(|item| item.id == day_spec::SEARCH_ITEM_ID)
1007 })
1008 });
1009 if has {
1010 with_tree(|t| {
1011 t.patch_window_toolbar(
1012 root,
1013 ToolbarPatch::Focus {
1014 item: day_spec::SEARCH_ITEM_ID.to_owned(),
1015 },
1016 )
1017 });
1018 }
1019 has
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024
1025 #[test]
1026 fn native_events_recompose_after_the_delivered_baseline_is_recorded() {
1027 let passes = std::cell::Cell::new(0);
1028 let delivered = std::cell::Cell::new(false);
1029 super::recompose_serially(|| {
1030 passes.set(passes.get() + 1);
1031 if passes.get() == 1 {
1032 super::recompose_serially(|| panic!("must not diff the stale baseline"));
1034 super::recompose_serially(|| panic!("nested requests must coalesce"));
1035 delivered.set(true);
1036 } else {
1037 assert!(delivered.get());
1038 }
1039 });
1040 assert_eq!(passes.get(), 2);
1041 super::recompose_serially(|| passes.set(passes.get() + 1));
1042 assert_eq!(passes.get(), 3, "the guard releases after a settled update");
1043 }
1044 use super::*;
1045 use day_spec::{ToolbarItemKind as K, ToolbarMirror, ToolbarOp};
1046
1047 fn item(id: &str, label: &str) -> ToolbarItem {
1048 ToolbarItem {
1049 id: id.into(),
1050 kind: K::Button,
1051 label: label.into(),
1052 tooltip: None,
1053 icon: None,
1054 enabled: true,
1055 action: 0,
1056 placement: day_spec::ToolbarPlacement::Automatic,
1057 label_style: day_spec::LabelStyle::Automatic,
1058 prominent: false,
1059 column: day_spec::ToolbarColumn::Window,
1060 }
1061 }
1062
1063 fn check(prev: &[ToolbarItem], next: &[ToolbarItem]) -> Vec<ToolbarOp> {
1065 let (ops, patches) = diff_bar(prev, next);
1066 let mut mirror = ToolbarMirror::default();
1067 for (i, p) in prev.iter().enumerate() {
1068 mirror.insert(i, p.clone());
1069 }
1070 let first_insert = ops
1072 .iter()
1073 .position(|o| matches!(o, ToolbarOp::Insert { .. }))
1074 .unwrap_or(ops.len());
1075 assert!(
1076 ops[first_insert..]
1077 .iter()
1078 .all(|o| matches!(o, ToolbarOp::Insert { .. }))
1079 );
1080 for op in &ops {
1081 mirror.apply(op);
1082 }
1083 for p in &patches {
1084 mirror.patch(p);
1085 }
1086 assert_eq!(mirror.items(), next);
1087 ops
1088 }
1089
1090 #[test]
1091 fn identical_bars_need_no_edit() {
1092 let bar = vec![item("a", "A"), item("b", "B")];
1093 assert!(check(&bar, &bar).is_empty());
1094 }
1095
1096 #[test]
1097 fn inserts_and_removes_touch_only_their_items() {
1098 let prev = vec![item("a", "A"), item("b", "B"), item("search", "S")];
1099 let next = vec![item("a", "A"), item("c", "C"), item("search", "S")];
1100 let ops = check(&prev, &next);
1101 assert_eq!(ops.len(), 2);
1102 assert!(ops.iter().all(|o| match o {
1103 ToolbarOp::Remove { id } => id == "b",
1104 ToolbarOp::Insert { item, index } => item.id == "c" && *index == 1,
1105 }));
1106 }
1107
1108 #[test]
1109 fn a_reorder_moves_the_fewest_items() {
1110 let prev = vec![
1111 item("a", "A"),
1112 item("b", "B"),
1113 item("c", "C"),
1114 item("d", "D"),
1115 ];
1116 let next = vec![
1117 item("d", "D"),
1118 item("a", "A"),
1119 item("b", "B"),
1120 item("c", "C"),
1121 ];
1122 let ops = check(&prev, &next);
1123 assert_eq!(ops.len(), 2, "{ops:?}");
1125 }
1126
1127 #[test]
1128 fn a_value_change_is_a_patch_and_a_label_change_an_edit() {
1129 let mut toggled = item("t", "T");
1130 toggled.kind = K::Toggle { on: false };
1131 let mut on = toggled.clone();
1132 on.kind = K::Toggle { on: true };
1133 on.enabled = false;
1134 let (ops, patches) = diff_bar(std::slice::from_ref(&toggled), std::slice::from_ref(&on));
1135 assert!(ops.is_empty());
1136 assert_eq!(patches.len(), 2);
1137 check(&[item("x", "Old")], &[item("x", "New")]);
1138 }
1139
1140 #[test]
1141 fn longest_increasing_skips_unmatched() {
1142 assert_eq!(
1143 longest_increasing(&[Some(2), None, Some(0), Some(1), Some(3)]),
1144 vec![2, 3, 4]
1145 );
1146 assert!(longest_increasing(&[None, None]).is_empty());
1147 }
1148}