1use std::rc::Rc;
14
15use frust_core::event::PointerSource;
16use frust_core::{
17 BoxConstraints, BuildCtx, ChangeFlags, ChildPod, Chord, EventCtx, EventResult, InputEvent,
18 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, ShortcutDecl, ShortcutId, ShortcutScope,
19 SliverWidget, View, Widget,
20};
21use kurbo::Size;
22
23use crate::authoring::{ErasedCallback, erase_callback};
24
25struct Binding<State> {
27 chord: Chord,
28 label: String,
29 scope: ShortcutScope,
30 handler: Rc<dyn Fn(&mut State)>,
31}
32
33pub struct ShortcutsView<State: 'static, V> {
35 child: V,
36 bindings: Vec<Binding<State>>,
37}
38
39pub fn shortcuts<State: 'static, V: View<State>>(child: V) -> ShortcutsView<State, V> {
52 ShortcutsView {
53 child,
54 bindings: Vec::new(),
55 }
56}
57
58impl<State: 'static, V> ShortcutsView<State, V> {
59 pub fn on(
67 self,
68 chord: &str,
69 label: impl Into<String>,
70 handler: impl Fn(&mut State) + 'static,
71 ) -> Self {
72 self.bind(parse(chord), label, ShortcutScope::Local, handler)
73 }
74
75 pub fn global(
82 self,
83 chord: &str,
84 label: impl Into<String>,
85 handler: impl Fn(&mut State) + 'static,
86 ) -> Self {
87 self.bind(parse(chord), label, ShortcutScope::AppGlobal, handler)
88 }
89
90 pub fn bind(
92 mut self,
93 chord: Chord,
94 label: impl Into<String>,
95 scope: ShortcutScope,
96 handler: impl Fn(&mut State) + 'static,
97 ) -> Self {
98 self.bindings.push(Binding {
99 chord,
100 label: label.into(),
101 scope,
102 handler: Rc::new(handler),
103 });
104 self
105 }
106}
107
108fn parse(spec: &str) -> Chord {
109 Chord::parse(spec).unwrap_or_else(|err| panic!("invalid shortcut chord `{spec}`: {err}"))
110}
111
112struct Entry {
114 decl: ShortcutDecl,
115 handler: ErasedCallback,
116}
117
118pub struct ShortcutsWidget<W> {
120 child: W,
121 entries: Vec<Entry>,
122}
123
124impl<State: 'static> Binding<State> {
125 fn entry(&self, id: ShortcutId) -> Entry {
126 Entry {
127 decl: ShortcutDecl {
128 id,
129 chord: self.chord.clone(),
130 label: self.label.clone(),
131 scope: self.scope,
132 },
133 handler: erase_callback(&self.handler),
134 }
135 }
136}
137
138impl<State: 'static, V: View<State>> View<State> for ShortcutsView<State, V> {
139 type Element = ShortcutsWidget<V::Element>;
140
141 fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
142 let entries = self.bindings.iter();
143 ShortcutsWidget {
144 child: self.child.build(ctx),
145 entries: entries.map(|b| b.entry(ShortcutId::next())).collect(),
146 }
147 }
148
149 fn rebuild(
151 &self,
152 prev: &Self,
153 element: &mut Self::Element,
154 ctx: &mut BuildCtx<'_>,
155 ) -> ChangeFlags {
156 let mut flags = self.child.rebuild(&prev.child, &mut element.child, ctx);
157 let before: Vec<ShortcutDecl> = element.entries.iter().map(|e| e.decl.clone()).collect();
158 element.entries.truncate(self.bindings.len());
159 for (i, binding) in self.bindings.iter().enumerate() {
160 match element.entries.get_mut(i) {
161 Some(entry) => *entry = binding.entry(entry.decl.id),
162 None => element.entries.push(binding.entry(ShortcutId::next())),
163 }
164 }
165 if element.entries.iter().map(|e| &e.decl).ne(before.iter()) {
166 flags |= ChangeFlags::PAINT;
167 }
168 flags
169 }
170
171 fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
172 self.child.teardown(&mut element.child, ctx);
173 }
174
175 fn message(&self, element: &mut Self::Element, state: &mut State) {
176 self.child.message(&mut element.child, state);
177 }
178}
179
180impl<W: Widget> Widget for ShortcutsWidget<W> {
181 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
182 self.child.layout(ctx, bc)
183 }
184
185 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
186 for entry in &self.entries {
187 ctx.register_shortcut(entry.decl.clone());
188 }
189 self.child.paint(ctx, scene);
190 }
191
192 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
193 if let InputEvent::Shortcut(id) = event
194 && let Some(entry) = self.entries.iter_mut().find(|e| e.decl.id == *id)
195 {
196 (entry.handler)(ctx);
197 ctx.request_redraw();
198 return EventResult::Handled;
199 }
200 self.child.event(ctx, event)
201 }
202
203 fn semantics(&self, ctx: &mut SemanticsCtx) {
204 self.child.semantics(ctx);
205 }
206
207 fn type_name(&self) -> &'static str {
209 self.child.type_name()
210 }
211
212 fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
213 self.child.visit_children(visitor);
214 }
215
216 fn is_focusable(&self) -> bool {
217 self.child.is_focusable()
218 }
219
220 fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
221 self.child.visit_focus_children(visitor);
222 }
223
224 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
225 self.child.as_sliver()
226 }
227}
228
229pub(crate) fn activation(ctx: &EventCtx, event: &InputEvent) -> bool {
231 ctx.has_focus() && matches!(event, InputEvent::Key(key) if key.is_activation())
232}
233
234pub(crate) fn press_claims_focus(ctx: &EventCtx) -> bool {
237 ctx.pointer_id().source == PointerSource::Mouse
238}
239
240#[cfg(test)]
241mod tests {
242 use std::any::Any;
243 use std::cell::Cell;
244
245 use frust_core::{
246 DiscardScene, FrameTime, Key, KeyEvent, Modifiers, NamedKey, RenderRoot, ShortcutHint,
247 };
248 use frust_text::TextContext;
249
250 use super::*;
251 use crate::{FlexView, IndexedStack, button, column, either, text};
252
253 #[derive(Default)]
254 struct App {
255 opened: u32,
256 closed: u32,
257 }
258
259 fn key(key: Key, modifiers: Modifiers) -> InputEvent {
260 InputEvent::Key(KeyEvent {
261 key,
262 modifiers,
263 repeat: false,
264 })
265 }
266
267 fn ctrl(c: &str) -> InputEvent {
268 let modifiers = Modifiers {
269 ctrl: true,
270 ..Modifiers::default()
271 };
272 key(Key::Character(c.to_string()), modifiers)
273 }
274
275 fn frame<V: View<App>>(root: &mut RenderRoot<App, V>, logic: &mut impl FnMut(&mut App) -> V) {
276 let mut state = App::default();
277 root.rebuild(logic, &mut state);
278 root.layout_with_text(
279 Size::new(300.0, 300.0),
280 &mut TextContext::new() as &mut dyn Any,
281 );
282 root.paint(&mut DiscardScene, FrameTime::ZERO);
283 }
284
285 fn labels(root: &RenderRoot<App, impl View<App>>) -> Vec<String> {
286 root.shortcut_hints(|_| true)
287 .into_iter()
288 .map(|hint| hint.label)
289 .collect()
290 }
291
292 fn bound() -> ShortcutsView<App, crate::TextView> {
293 shortcuts(text("body"))
294 .on("Ctrl+K", "Open", |app: &mut App| app.opened += 1)
295 .global("Ctrl+J", "Close", |app: &mut App| app.closed += 1)
296 }
297
298 #[test]
299 fn a_shortcut_delivery_runs_only_its_own_handler() {
300 let mut counter = 0u64;
301 let view = bound();
302 let mut widget = view.build(&mut BuildCtx::new(&mut counter));
303 let (open, close) = (widget.entries[0].decl.id, widget.entries[1].decl.id);
304 let mut app = App::default();
305 let deliver = |widget: &mut ShortcutsWidget<_>, app: &mut App, id| {
306 let state: &mut dyn Any = app;
307 let mut ctx = EventCtx::new(state, kurbo::Point::ZERO, Size::new(10.0, 10.0));
308 widget.event(&mut ctx, &InputEvent::Shortcut(id))
309 };
310 assert_eq!(deliver(&mut widget, &mut app, open), EventResult::Handled);
311 assert_eq!(deliver(&mut widget, &mut app, close), EventResult::Handled);
312 assert_eq!((app.opened, app.closed), (1, 1));
313 let foreign = ShortcutId::next();
314 assert_eq!(
315 deliver(&mut widget, &mut app, foreign),
316 EventResult::Ignored
317 );
318 assert_eq!((app.opened, app.closed), (1, 1), "another id is not ours");
319 }
320
321 #[test]
322 fn a_pressed_chord_fires_its_handler_and_hints_list_the_bindings() {
323 let mut root: RenderRoot<App, FlexView<App>> = RenderRoot::new();
324 let mut logic = |_: &mut App| column().child(bound());
325 frame(&mut root, &mut logic);
326 let hints = root.shortcut_hints(|_| true);
327 assert_eq!(
328 hints[0],
329 ShortcutHint {
330 chord: Chord::parse("Ctrl+K").unwrap(),
331 label: "Open".to_string(),
332 scope: ShortcutScope::Local,
333 }
334 );
335 assert_eq!(hints[1].scope, ShortcutScope::AppGlobal);
336 assert_eq!(
337 hints[1].chord.display(),
338 Chord::parse("Ctrl+J").unwrap().display()
339 );
340
341 let mut app = App::default();
342 assert!(root.event(&mut app, &ctrl("k")).handled);
343 assert!(root.event(&mut app, &ctrl("j")).handled);
344 assert!(
345 !root.event(&mut app, &ctrl("x")).handled,
346 "an unbound chord"
347 );
348 assert_eq!((app.opened, app.closed), (1, 1));
349 }
350
351 #[test]
352 fn an_unmounted_wrapper_declares_nothing() {
353 let shown = Cell::new(true);
354 let mut root = RenderRoot::new();
355 let mut logic = |_: &mut App| either(shown.get(), bound, || text("gone"));
356 frame(&mut root, &mut logic);
357 assert_eq!(labels(&root), ["Open", "Close"]);
358
359 shown.set(false);
360 frame(&mut root, &mut logic);
361 assert!(labels(&root).is_empty(), "nothing declared after unmount");
362 let mut app = App::default();
363 root.event(&mut app, &ctrl("k"));
364 assert_eq!(app.opened, 0);
365 }
366
367 #[test]
368 fn a_nested_binding_wins_its_tier_and_rebuilds_keep_ids() {
369 let mut root: RenderRoot<App, ShortcutsView<App, ShortcutsView<App, crate::TextView>>> =
370 RenderRoot::new();
371 let inner_label = Cell::new("Inner");
372 let mut logic = |_: &mut App| {
373 let inner =
374 shortcuts(text("x")).on("Ctrl+K", inner_label.get(), |a: &mut App| a.closed += 1);
375 shortcuts(inner).on("Ctrl+K", "Outer", |a: &mut App| a.opened += 1)
376 };
377 frame(&mut root, &mut logic);
378 let mut app = App::default();
379 root.event(&mut app, &ctrl("k"));
380 assert_eq!(
381 (app.opened, app.closed),
382 (0, 1),
383 "the inner declaration is newer"
384 );
385
386 let id = |root: &RenderRoot<App, _>| {
387 let pod = root.tree().pod(root.root_id().unwrap()).unwrap();
388 let widget = (pod.widget() as &dyn Any)
389 .downcast_ref::<ShortcutsWidget<ShortcutsWidget<crate::TextWidget>>>();
390 widget.unwrap().child.entries[0].decl.id
391 };
392 let before = id(&root);
393 inner_label.set("Renamed");
394 frame(&mut root, &mut logic);
395 assert_eq!(id(&root), before, "a rebuild keeps the binding's id");
396 assert_eq!(labels(&root), ["Outer", "Renamed"]);
397 }
398
399 #[test]
400 fn rebuild_reports_paint_only_when_a_declaration_changes() {
401 let mut counter = 0u64;
402 let mut ctx = BuildCtx::new(&mut counter);
403 let prev = bound();
404 let mut widget = prev.build(&mut ctx);
405 let same = bound();
406 assert_eq!(
407 same.rebuild(&prev, &mut widget, &mut ctx),
408 ChangeFlags::NONE
409 );
410 let more = bound().on("Ctrl+L", "Third", |_: &mut App| {});
411 assert!(more.rebuild(&same, &mut widget, &mut ctx).needs_paint());
412 assert_eq!(widget.entries.len(), 3);
413 let fewer = shortcuts(text("body")).on("Ctrl+K", "Open", |_: &mut App| {});
414 assert!(fewer.rebuild(&more, &mut widget, &mut ctx).needs_paint());
415 assert_eq!(widget.entries.len(), 1);
416 }
417
418 #[test]
419 fn focus_queries_forward_to_the_child() {
420 let mut counter = 0u64;
421 let mut ctx = BuildCtx::new(&mut counter);
422 let wrapped = shortcuts(button::<App, _>("Go", |_| {})).on("Ctrl+K", "k", |_| {});
423 assert!(wrapped.build(&mut ctx).is_focusable());
424 assert!(!bound().build(&mut ctx).is_focusable());
425 let stack = IndexedStack(Some(0), (text("a"), text("b")));
426 let widget = shortcuts::<App, _>(stack).build(&mut ctx);
427 let (mut all, mut focus) = (0, 0);
428 widget.visit_children(&mut |_| all += 1);
429 widget.visit_focus_children(&mut |_| focus += 1);
430 assert_eq!(
431 (all, focus),
432 (2, 1),
433 "the stack's narrowing survives the wrapper"
434 );
435 }
436
437 #[test]
438 fn tab_reaches_a_wrapped_control_and_its_keys_pass_through() {
439 let mut root: RenderRoot<App, FlexView<App>> = RenderRoot::new();
440 let mut logic = |_: &mut App| {
441 let go = button::<App, _>("Go", |app: &mut App| app.opened += 1);
442 column().child(shortcuts(go).on("Ctrl+K", "k", |app: &mut App| app.closed += 1))
443 };
444 frame(&mut root, &mut logic);
445 let mut app = App::default();
446 root.event(
447 &mut app,
448 &key(Key::Named(NamedKey::Tab), Modifiers::default()),
449 );
450 assert!(
451 root.is_focus_active(),
452 "the wrapper's pod is the button's stop"
453 );
454 root.event(
455 &mut app,
456 &key(Key::Character(" ".into()), Modifiers::default()),
457 );
458 root.event(&mut app, &ctrl("k"));
459 assert_eq!((app.opened, app.closed), (1, 1));
460 }
461
462 #[test]
463 #[should_panic(expected = "invalid shortcut chord `F25`")]
464 fn an_unparsable_chord_panics_naming_it() {
465 let _ = shortcuts::<App, _>(text("x")).on("F25", "Rename", |_| {});
466 }
467}