Skip to main content

tilo_app/framework/
bar_actions.rs

1//! The resource's declared actions and effects as entries of the window's
2//! bar.
3//!
4//! An app puts one of its record's verbs, field edits or effects in its
5//! window's bar ([`ui_core::window_toolbar`]) by naming it, the way a bound
6//! button names it (`bar.action(Snippets::DELETE.with(id))`,
7//! `bar.effect(&View::ZoomIn)`): the entry takes its name, icon and
8//! destructive role from the declaration, and is disabled where the
9//! participant may not take it. Where it sits (the row or the menu, a menu
10//! button's items) and whether a toggle is on stay the app's, set on the
11//! entry it gets back like on any [`ToolbarAction`].
12//!
13//! The app hands each event it takes from the bar to [`BarActions::press`]
14//! first, as it hands them to the navigation shell: a press on one of these
15//! entries is submitted through the window's dispatcher, as a bound
16//! control's is, and never reaches the app's own slot handling. No id to
17//! keep in step and no command to map it back to.
18//!
19//! What the window does that is no action of its record (a panel it opens,
20//! a view state it switches) is an entry of [`BarCommands`]: it carries the
21//! app's own command, and a press on it hands the command back.
22
23use std::cell::{Cell, RefCell};
24use std::collections::HashMap;
25
26use tilo_actions::{ActionNeed, RiskLevel};
27use tilo_resource::{Effect, Invoke, Resource};
28use ui_core::window_toolbar::{ToolbarAction, ToolbarEvent};
29
30use super::{Dispatcher, Submit};
31
32/// Starts the id of every entry a [`BarActions`] runs, so a press on one is
33/// told from the app's own slots by its id alone.
34const PREFIX: &str = "invoke:";
35
36/// The most entries kept for presses. An entry's id is what it invokes, so
37/// a bar declared anew keeps the same ids; this only bounds the ones a
38/// changing bar (a toggle's argument flipping) leaves behind.
39const KEPT: usize = 256;
40
41/// The window bar's entries that invoke the resource's actions and effects.
42pub struct BarActions<R: Resource> {
43    dispatcher: Dispatcher<R>,
44    /// What each entry declared since the bar last forgot invokes, by id.
45    shown: RefCell<HashMap<String, Invoke<R>>>,
46}
47
48impl<R: Resource> BarActions<R> {
49    /// Entries submitted through `dispatcher`: the window's own.
50    #[must_use]
51    pub fn new(dispatcher: Dispatcher<R>) -> Self {
52        Self {
53            dispatcher,
54            shown: RefCell::new(HashMap::new()),
55        }
56    }
57
58    /// The window's dispatcher, which these entries are submitted through.
59    #[must_use]
60    pub fn dispatcher(&self) -> &Dispatcher<R> {
61        &self.dispatcher
62    }
63
64    /// The entry that submits `submit` (a verb, a field edit), named and
65    /// pictured as its action is declared; a field edit, declared by no
66    /// verb, has neither and is named by the app. Disabled where the
67    /// participant may not take it.
68    pub fn action(&self, submit: impl Submit<R>) -> ToolbarAction {
69        let id = submit.action_id();
70        let declarations = tilo_resource::vocabulary::<R>().declarations();
71        let declared = declarations.iter().find(|decl| decl.id == id);
72        let (name, icon) = declared.map_or_else(Default::default, |decl| {
73            (decl.label.clone(), decl.icon.clone().unwrap_or_default())
74        });
75        let mut entry = match submit.into_invoke() {
76            Ok(invoke) => {
77                let enabled = self.dispatcher.may_invoke(&invoke);
78                self.entry(invoke, icon, name).enabled(enabled)
79            }
80            Err(error) => {
81                super::controls::unbound::<R>(&format!("`{id}`"), &error);
82                ToolbarAction::new(String::new(), icon, name).enabled(false)
83            }
84        };
85        entry.destructive = declared.is_some_and(|decl| decl.destructive);
86        entry
87    }
88
89    /// The entry that invokes `effect`, named and pictured as it is
90    /// declared, and answered by whoever answers the effect (a view effect
91    /// by the window's surface). Disabled where the participant may not
92    /// take it.
93    pub fn effect<E: Effect>(&self, effect: &E) -> ToolbarAction {
94        let declared = E::declaration(effect.id());
95        let (name, icon) = declared.as_ref().map_or_else(Default::default, |decl| {
96            (
97                decl.label.to_owned(),
98                decl.icon.unwrap_or_default().to_owned(),
99            )
100        });
101        let enabled = declared.as_ref().is_some_and(|decl| {
102            self.dispatcher.may(&ActionNeed {
103                kind: decl.action_kind,
104                field: None,
105                privileged: false,
106            })
107        });
108        let mut entry = self
109            .entry(Invoke::effect(effect), icon, name)
110            .enabled(enabled);
111        entry.destructive = declared.is_some_and(|decl| decl.risk == RiskLevel::Destructive);
112        entry
113    }
114
115    /// Submit what `event` presses when it presses one of these entries.
116    /// Returns whether it was a press on one (then it is handled, even when
117    /// the entry is gone), so the app's own slots never see it.
118    pub fn press(&self, event: &ToolbarEvent) -> bool {
119        let ToolbarEvent::Action { id } = event else {
120            return false;
121        };
122        if !id.starts_with(PREFIX) {
123            return false;
124        }
125        let invoke = self.shown.borrow().get(id).cloned();
126        if let Some(invoke) = invoke {
127            self.dispatcher.submit(invoke);
128        }
129        true
130    }
131
132    fn entry(&self, invoke: Invoke<R>, icon: String, name: String) -> ToolbarAction {
133        let id = format!(
134            "{PREFIX}{}{}:{}",
135            if invoke.is_effect() { "effect:" } else { "" },
136            invoke.id,
137            serde_json::Value::Object(invoke.arguments.clone())
138        );
139        let mut shown = self.shown.borrow_mut();
140        if shown.len() >= KEPT && !shown.contains_key(&id) {
141            shown.clear();
142        }
143        shown.insert(id.clone(), invoke);
144        ToolbarAction::new(id, icon, name)
145    }
146}
147
148/// Starts the id of every entry a [`BarCommands`] runs.
149const COMMAND_PREFIX: &str = "command:";
150
151thread_local! {
152    /// The serial of the next command entry, on any [`BarCommands`] of the
153    /// thread: an id never names two commands, even of two windows.
154    static NEXT_COMMAND: Cell<u64> = const { Cell::new(0) };
155}
156
157/// The window bar's entries that run the app's own commands `C`.
158pub struct BarCommands<C> {
159    /// The commands of the entries declared since the bar last forgot, by
160    /// serial.
161    shown: RefCell<Vec<(u64, C)>>,
162}
163
164impl<C> Default for BarCommands<C> {
165    fn default() -> Self {
166        Self {
167            shown: RefCell::new(Vec::new()),
168        }
169    }
170}
171
172impl<C: Clone + PartialEq> BarCommands<C> {
173    /// No entries yet.
174    #[must_use]
175    pub fn new() -> Self {
176        Self::default()
177    }
178
179    /// The entry, pictured by `icon` and named `name`, that hands back
180    /// `command` when pressed ([`Self::pressed`]). An equal command keeps
181    /// its entry's id, so a bar declared anew is the same bar.
182    pub fn entry(
183        &self,
184        icon: impl Into<String>,
185        name: impl Into<String>,
186        command: C,
187    ) -> ToolbarAction {
188        let mut shown = self.shown.borrow_mut();
189        let serial = if let Some((serial, _)) = shown.iter().find(|(_, shown)| *shown == command) {
190            *serial
191        } else {
192            if shown.len() >= KEPT {
193                shown.clear();
194            }
195            let serial = NEXT_COMMAND.with(|next| next.replace(next.get().wrapping_add(1)));
196            shown.push((serial, command));
197            serial
198        };
199        ToolbarAction::new(format!("{COMMAND_PREFIX}{serial}"), icon, name)
200    }
201
202    /// The command of the entry `event` presses, if it presses one of
203    /// these: the app runs it, and its own slots never see the press.
204    #[must_use]
205    pub fn pressed(&self, event: &ToolbarEvent) -> Option<C> {
206        let ToolbarEvent::Action { id } = event else {
207            return None;
208        };
209        let serial = id.strip_prefix(COMMAND_PREFIX)?.parse::<u64>().ok()?;
210        self.shown
211            .borrow()
212            .iter()
213            .find(|(shown, _)| *shown == serial)
214            .map(|(_, command)| command.clone())
215    }
216}