1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
//! Widget-owned units of behavior bound to named intents.
//!
//! An [`Action`] lives on a widget alongside its event handlers. When
//! an [`Intent`] is dispatched, the framework walks the focus /
//! source-widget chain and, at each level, invokes any [`Action`]
//! whose `intent` name matches the intent's name. The handler's
//! [`IntentResponse`] controls whether the intent is consumed or
//! continues up the chain.
//!
//! A widget may declare multiple actions for different intent names;
//! at a single level, if two actions match the same name, the first
//! (by declaration order) wins.
use crate::intent::{Intent, IntentResponse};
use crate::signal::Prop;
use crate::widget::EventContext;
/// Closure signature for an action handler.
pub type ActionHandler = Box<dyn FnMut(&Intent, &mut EventContext) -> IntentResponse + 'static>;
/// A widget-owned handler bound to a named intent.
pub struct Action {
/// The intent name this action responds to. Matches against
/// [`Intent::name`] during dispatch.
pub intent: &'static str,
/// Invoked when a matching intent is dispatched to this widget.
pub handler: ActionHandler,
/// Reactive "is this action currently applicable?" predicate.
/// `None` means always enabled.
///
/// Disabled semantics at dispatch time: the intent propagates past
/// this action as if no match existed here, **unless** the firing
/// [`Shortcut`](crate::shortcut::Shortcut) has
/// `propagate_when_disabled == false`, in which case the intent
/// is consumed (dormant) at this level.
pub enabled_when: Option<Prop<bool>>,
}
impl std::fmt::Debug for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Action")
.field("intent", &self.intent)
.field("handler", &"<closure>")
.field("enabled_when", &self.enabled_when.is_some())
.finish()
}
}
impl Action {
/// Start building an [`Action`] bound to the given intent name.
#[allow(clippy::new_ret_no_self)]
pub fn new(intent: &'static str) -> ActionBuilder {
ActionBuilder {
intent,
handler: None,
enabled_when: None,
}
}
/// Resolve the current enabled state. `true` when no predicate is
/// set; otherwise reads the signal.
pub fn is_enabled(&self) -> bool {
self.enabled_when.as_ref().map(|s| s.get()).unwrap_or(true)
}
}
/// Fluent builder for [`Action`]. Use [`ActionBuilder::on_invoke`] for
/// the common case (handler consumes the intent) or
/// [`ActionBuilder::on_invoke_with_response`] when the handler needs
/// to decide whether to propagate.
pub struct ActionBuilder {
intent: &'static str,
handler: Option<ActionHandler>,
enabled_when: Option<Prop<bool>>,
}
impl ActionBuilder {
/// Reactive enabled-predicate. When the signal holds `false` the
/// action is skipped during dispatch.
pub fn enabled_when(mut self, signal: impl Into<Prop<bool>>) -> Self {
self.enabled_when = Some(signal.into());
self
}
/// Register a handler that consumes the intent. The handler's
/// return value is ignored; the framework treats every invocation
/// as [`IntentResponse::Handled`].
pub fn on_invoke(mut self, mut f: impl FnMut(&Intent, &mut EventContext) + 'static) -> Action {
self.handler = Some(Box::new(move |intent, ctx| {
f(intent, ctx);
IntentResponse::Handled
}));
self.finish()
}
/// Register a handler whose return value decides whether the
/// intent propagates to ancestor widgets. Use when a widget wants
/// to observe an intent (e.g., update a draft status) while still
/// letting an ancestor perform the primary action.
pub fn on_invoke_with_response(
mut self,
f: impl FnMut(&Intent, &mut EventContext) -> IntentResponse + 'static,
) -> Action {
self.handler = Some(Box::new(f));
self.finish()
}
fn finish(self) -> Action {
Action {
intent: self.intent,
handler: self
.handler
.expect("ActionBuilder requires on_invoke or on_invoke_with_response"),
enabled_when: self.enabled_when,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn action_builder_on_invoke_defaults_to_handled() {
let fired = Rc::new(Cell::new(false));
let fired_flag = fired.clone();
let mut action = Action::new("app.save").on_invoke(move |_intent, _ctx| {
fired_flag.set(true);
});
let mut ctx = EventContext::new();
let intent = Intent::new("app.save");
let response = (action.handler)(&intent, &mut ctx);
assert_eq!(response, IntentResponse::Handled);
assert!(fired.get());
assert_eq!(action.intent, "app.save");
}
#[test]
fn action_handler_receives_typed_payload() {
let seen = Rc::new(Cell::new(0_i64));
let seen_flag = seen.clone();
let mut action = Action::new("tab.switch").on_invoke(move |intent, _ctx| {
if let Some(&n) = intent.payload::<i64>() {
seen_flag.set(n);
}
});
let mut ctx = EventContext::new();
let intent = Intent::with_payload("tab.switch", 5_i64);
(action.handler)(&intent, &mut ctx);
assert_eq!(seen.get(), 5);
}
#[test]
fn action_with_response_can_propagate() {
let mut action = Action::new("log.observe")
.on_invoke_with_response(|_intent, _ctx| IntentResponse::Propagated);
let mut ctx = EventContext::new();
let intent = Intent::new("log.observe");
let response = (action.handler)(&intent, &mut ctx);
assert_eq!(response, IntentResponse::Propagated);
}
#[test]
fn enabled_when_defaults_true() {
let action = Action::new("edit.delete").on_invoke(|_, _| {});
assert!(action.is_enabled());
}
#[test]
fn enabled_when_follows_signal() {
let enabled = crate::signal::Signal::new(false);
let action = Action::new("edit.delete")
.enabled_when(enabled.clone())
.on_invoke(|_, _| {});
assert!(!action.is_enabled());
enabled.set(true);
assert!(action.is_enabled());
}
}