zng_app/event/
events.rs

1use std::collections::HashSet;
2use zng_app_context::app_local;
3use zng_txt::Txt;
4use zng_var::VARS;
5
6use crate::update::UPDATES;
7
8use super::*;
9
10app_local! {
11    pub(crate) static EVENTS_SV: EventsService = const { EventsService::new() };
12}
13
14pub(crate) struct EventsService {
15    commands: CommandSet,
16    l10n: EventsL10n,
17}
18enum EventsL10n {
19    Pending(Vec<([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>)>),
20    Init(Box<dyn Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync>),
21}
22impl EventsService {
23    const fn new() -> Self {
24        Self {
25            commands: HashSet::with_hasher(BuildFxHasher),
26            l10n: EventsL10n::Pending(vec![]),
27        }
28    }
29}
30
31/// Const rustc-hash hasher.
32#[derive(Clone, Default)]
33pub struct BuildFxHasher;
34impl std::hash::BuildHasher for BuildFxHasher {
35    type Hasher = rustc_hash::FxHasher;
36
37    fn build_hasher(&self) -> Self::Hasher {
38        rustc_hash::FxHasher::default()
39    }
40}
41
42/// Registered commands set.
43pub type CommandSet = HashSet<Command, BuildFxHasher>;
44
45/// App events and commands service.
46pub struct EVENTS;
47impl EVENTS {
48    /// Commands that had handles generated in this app.
49    ///
50    /// When [`Command::subscribe`] is called for the first time in an app, the command gets added
51    /// to this list after the current update, if the command is app scoped it remains on the list for
52    /// the lifetime of the app, if it is window or widget scoped it only remains while there are handles.
53    ///
54    /// [`Command::subscribe`]: crate::event::Command::subscribe
55    pub fn commands(&self) -> CommandSet {
56        EVENTS_SV.read().commands.clone()
57    }
58
59    pub(super) fn register_command(&self, cmd: Command) {
60        tracing::trace!("register {cmd:?}");
61        UPDATES.once_update("register_command", move || {
62            let mut ev = EVENTS_SV.write();
63            if !ev.commands.insert(cmd) {
64                tracing::error!("command `{cmd:?}` is already registered");
65            }
66        });
67    }
68    pub(super) fn unregister_command(&self, cmd: Command) {
69        tracing::trace!("unregister {cmd:?}");
70        UPDATES.once_update("unregister_command", move || {
71            EVENTS_SV.write().commands.remove(&cmd);
72        });
73    }
74
75    /// Schedule a custom closure to run as an event notify callback.
76    ///
77    /// The closure `n` will run after the current update, any event it notifies will update on the next cycle.
78    ///
79    /// Note that this is just an alias for [`VARS::modify`], events are just an specialized variable.
80    pub fn notify(&self, debug_name: &'static str, n: impl FnOnce() + Send + 'static) {
81        VARS.modify(debug_name, n);
82    }
83}
84
85/// EVENTS L10N integration.
86#[expect(non_camel_case_types)]
87pub struct EVENTS_L10N;
88impl EVENTS_L10N {
89    pub(crate) fn init_meta_l10n(&self, file: [&'static str; 3], cmd: Command, meta_name: &'static str, txt: CommandMetaVar<Txt>) {
90        {
91            let sv = EVENTS_SV.read();
92            if let EventsL10n::Init(f) = &sv.l10n {
93                f(file, cmd, meta_name, txt);
94                return;
95            }
96        }
97
98        let mut sv = EVENTS_SV.write();
99        match &mut sv.l10n {
100            EventsL10n::Pending(a) => a.push((file, cmd, meta_name, txt)),
101            EventsL10n::Init(f) => f(file, cmd, meta_name, txt),
102        }
103    }
104
105    /// Register a closure that is called to localize command metadata.
106    ///
107    /// The closure arguments are:
108    ///
109    /// * `file` is the crate package name, version and the file from command declaration `@l10n: "file"`
110    ///   value or is empty if `@l10n` was set to something else.
111    /// * `cmd` is the command, the command event name should be used as key.
112    /// * `meta` is the metadata name, for example `"name"`, should be used as attribute.
113    /// * `txt` is text variable that must be set with the translation.
114    pub fn init_l10n(&self, localize: impl Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync + 'static) {
115        let mut sv = EVENTS_SV.write();
116        match &mut sv.l10n {
117            EventsL10n::Pending(a) => {
118                for (f, k, a, t) in a.drain(..) {
119                    localize(f, k, a, t);
120                }
121            }
122            EventsL10n::Init(_) => panic!("EVENTS_L10N already has a localizer"),
123        }
124        sv.l10n = EventsL10n::Init(Box::new(localize));
125    }
126}