zng_app/event/
events.rs

1use std::collections::HashSet;
2use zng_app_context::app_local;
3use zng_time::INSTANT_APP;
4use zng_txt::Txt;
5
6use crate::update::{UPDATES, UpdatesTrace};
7
8use super::*;
9
10app_local! {
11    pub(crate) static EVENTS_SV: EventsService = const { EventsService::new() };
12}
13
14pub(crate) struct EventsService {
15    updates: Mutex<Vec<EventUpdate>>, // not locked, used to make service Sync.
16    commands: CommandSet,
17    register_commands: Vec<Command>,
18    l10n: EventsL10n,
19}
20enum EventsL10n {
21    Pending(Vec<([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>)>),
22    Init(Box<dyn Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync>),
23}
24impl EventsService {
25    const fn new() -> Self {
26        Self {
27            updates: Mutex::new(vec![]),
28            commands: HashSet::with_hasher(BuildFxHasher),
29            register_commands: vec![],
30            l10n: EventsL10n::Pending(vec![]),
31        }
32    }
33
34    pub(super) fn register_command(&mut self, command: Command) {
35        if self.register_commands.is_empty() {
36            UPDATES.update(None);
37        }
38        self.register_commands.push(command);
39    }
40
41    pub(super) fn sender<A>(&mut self, event: Event<A>) -> EventSender<A>
42    where
43        A: EventArgs + Send,
44    {
45        EventSender {
46            sender: UPDATES.sender(),
47            event,
48        }
49    }
50
51    pub(crate) fn has_pending_updates(&mut self) -> bool {
52        !self.updates.get_mut().is_empty()
53    }
54}
55
56/// Const rustc-hash hasher.
57#[derive(Clone, Default)]
58pub struct BuildFxHasher;
59impl std::hash::BuildHasher for BuildFxHasher {
60    type Hasher = rustc_hash::FxHasher;
61
62    fn build_hasher(&self) -> Self::Hasher {
63        rustc_hash::FxHasher::default()
64    }
65}
66
67/// Registered commands set.
68pub type CommandSet = HashSet<Command, BuildFxHasher>;
69
70/// App events and commands service.
71pub struct EVENTS;
72impl EVENTS {
73    /// Commands that had handles generated in this app.
74    ///
75    /// When [`Command::subscribe`] is called for the first time in an app, the command gets added
76    /// to this list after the current update, if the command is app scoped it remains on the list for
77    /// the lifetime of the app, if it is window or widget scoped it only remains while there are handles.
78    ///
79    /// [`Command::subscribe`]: crate::event::Command::subscribe
80    pub fn commands(&self) -> CommandSet {
81        EVENTS_SV.read().commands.clone()
82    }
83
84    /// Schedules the raw event update.
85    pub fn notify(&self, update: EventUpdate) {
86        UpdatesTrace::log_event(update.event);
87        EVENTS_SV.write().updates.get_mut().push(update);
88        UPDATES.send_awake();
89    }
90
91    #[must_use]
92    pub(crate) fn apply_updates(&self) -> Vec<EventUpdate> {
93        let _s = tracing::trace_span!("EVENTS").entered();
94
95        let mut ev = EVENTS_SV.write();
96        ev.commands.retain(|c| c.update_state());
97
98        {
99            let ev = &mut *ev;
100            for cmd in ev.register_commands.drain(..) {
101                if cmd.update_state() && !ev.commands.insert(cmd) {
102                    tracing::error!("command `{cmd:?}` is already registered")
103                }
104            }
105        }
106
107        let mut updates: Vec<_> = ev.updates.get_mut().drain(..).collect();
108        drop(ev);
109
110        if !updates.is_empty() {
111            let _t = INSTANT_APP.pause_for_update();
112
113            for u in &mut updates {
114                let ev = u.event;
115                ev.on_update(u);
116            }
117        }
118        updates
119    }
120}
121
122/// EVENTS L10N integration.
123#[expect(non_camel_case_types)]
124pub struct EVENTS_L10N;
125impl EVENTS_L10N {
126    pub(crate) fn init_meta_l10n(&self, file: [&'static str; 3], cmd: Command, meta_name: &'static str, txt: CommandMetaVar<Txt>) {
127        {
128            let sv = EVENTS_SV.read();
129            if let EventsL10n::Init(f) = &sv.l10n {
130                f(file, cmd, meta_name, txt);
131                return;
132            }
133        }
134
135        let mut sv = EVENTS_SV.write();
136        match &mut sv.l10n {
137            EventsL10n::Pending(a) => a.push((file, cmd, meta_name, txt)),
138            EventsL10n::Init(f) => f(file, cmd, meta_name, txt),
139        }
140    }
141
142    /// Register a closure that is called to localize command metadata.
143    ///
144    /// The closure arguments are:
145    ///
146    /// * `file` is the crate package name, version and the file from command declaration `@l10n: "file"`
147    ///   value or is empty if `@l10n` was set to something else.
148    /// * `cmd` is the command, the command event name should be used as key.
149    /// * `meta` is the metadata name, for example `"name"`, should be used as attribute.
150    /// * `txt` is text variable that must be set with the translation.
151    pub fn init_l10n(&self, localize: impl Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync + 'static) {
152        let mut sv = EVENTS_SV.write();
153        match &mut sv.l10n {
154            EventsL10n::Pending(a) => {
155                for (f, k, a, t) in a.drain(..) {
156                    localize(f, k, a, t);
157                }
158            }
159            EventsL10n::Init(_) => panic!("EVENTS_L10N already has a localizer"),
160        }
161        sv.l10n = EventsL10n::Init(Box::new(localize));
162    }
163}