tilo_app/framework/
bar_actions.rs1use 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
32const PREFIX: &str = "invoke:";
35
36const KEPT: usize = 256;
40
41pub struct BarActions<R: Resource> {
43 dispatcher: Dispatcher<R>,
44 shown: RefCell<HashMap<String, Invoke<R>>>,
46}
47
48impl<R: Resource> BarActions<R> {
49 #[must_use]
51 pub fn new(dispatcher: Dispatcher<R>) -> Self {
52 Self {
53 dispatcher,
54 shown: RefCell::new(HashMap::new()),
55 }
56 }
57
58 #[must_use]
60 pub fn dispatcher(&self) -> &Dispatcher<R> {
61 &self.dispatcher
62 }
63
64 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 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 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
148const COMMAND_PREFIX: &str = "command:";
150
151thread_local! {
152 static NEXT_COMMAND: Cell<u64> = const { Cell::new(0) };
155}
156
157pub struct BarCommands<C> {
159 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 #[must_use]
175 pub fn new() -> Self {
176 Self::default()
177 }
178
179 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 #[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}