zng_app/event/
events.rs

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
use hashbrown::HashSet;
use zng_app_context::app_local;
use zng_time::INSTANT_APP;
use zng_txt::Txt;

use crate::update::{UpdatesTrace, UPDATES};

use super::*;

app_local! {
    pub(crate) static EVENTS_SV: EventsService = const { EventsService::new() };
}

pub(crate) struct EventsService {
    updates: Mutex<Vec<EventUpdate>>, // not locked, used to make service Sync.
    commands: CommandSet,
    register_commands: Vec<Command>,
    l10n: EventsL10n,
}
enum EventsL10n {
    Pending(Vec<([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>)>),
    Init(Box<dyn Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync>),
}
impl EventsService {
    const fn new() -> Self {
        Self {
            updates: Mutex::new(vec![]),
            commands: HashSet::with_hasher(BuildFxHasher),
            register_commands: vec![],
            l10n: EventsL10n::Pending(vec![]),
        }
    }

    pub(super) fn register_command(&mut self, command: Command) {
        if self.register_commands.is_empty() {
            UPDATES.update(None);
        }
        self.register_commands.push(command);
    }

    pub(super) fn sender<A>(&mut self, event: Event<A>) -> EventSender<A>
    where
        A: EventArgs + Send,
    {
        EventSender {
            sender: UPDATES.sender(),
            event,
        }
    }

    pub(crate) fn has_pending_updates(&mut self) -> bool {
        !self.updates.get_mut().is_empty()
    }
}

/// Const rustc-hash hasher.
#[derive(Clone, Default)]
pub struct BuildFxHasher;
impl std::hash::BuildHasher for BuildFxHasher {
    type Hasher = rustc_hash::FxHasher;

    fn build_hasher(&self) -> Self::Hasher {
        rustc_hash::FxHasher::default()
    }
}

/// Registered commands set.
pub type CommandSet = HashSet<Command, BuildFxHasher>;

/// App events and commands service.
pub struct EVENTS;
impl EVENTS {
    /// Commands that had handles generated in this app.
    ///
    /// When [`Command::subscribe`] is called for the first time in an app, the command gets added
    /// to this list after the current update, if the command is app scoped it remains on the list for
    /// the lifetime of the app, if it is window or widget scoped it only remains while there are handles.
    ///
    /// [`Command::subscribe`]: crate::event::Command::subscribe
    pub fn commands(&self) -> CommandSet {
        EVENTS_SV.read().commands.clone()
    }

    /// Schedules the raw event update.
    pub fn notify(&self, update: EventUpdate) {
        UpdatesTrace::log_event(update.event);
        EVENTS_SV.write().updates.get_mut().push(update);
        UPDATES.send_awake();
    }

    #[must_use]
    pub(crate) fn apply_updates(&self) -> Vec<EventUpdate> {
        let _s = tracing::trace_span!("EVENTS").entered();

        let mut ev = EVENTS_SV.write();
        ev.commands.retain(|c| c.update_state());

        {
            let ev = &mut *ev;
            for cmd in ev.register_commands.drain(..) {
                if cmd.update_state() && !ev.commands.insert(cmd) {
                    tracing::error!("command `{cmd:?}` is already registered")
                }
            }
        }

        let mut updates: Vec<_> = ev.updates.get_mut().drain(..).collect();
        drop(ev);

        if !updates.is_empty() {
            let _t = INSTANT_APP.pause_for_update();

            for u in &mut updates {
                let ev = u.event;
                ev.on_update(u);
            }
        }
        updates
    }
}

/// EVENTS L10N integration.
#[expect(non_camel_case_types)]
pub struct EVENTS_L10N;
impl EVENTS_L10N {
    pub(crate) fn init_meta_l10n(&self, file: [&'static str; 3], cmd: Command, meta_name: &'static str, txt: CommandMetaVar<Txt>) {
        {
            let sv = EVENTS_SV.read();
            if let EventsL10n::Init(f) = &sv.l10n {
                f(file, cmd, meta_name, txt);
                return;
            }
        }

        let mut sv = EVENTS_SV.write();
        match &mut sv.l10n {
            EventsL10n::Pending(a) => a.push((file, cmd, meta_name, txt)),
            EventsL10n::Init(f) => f(file, cmd, meta_name, txt),
        }
    }

    /// Register a closure that is called to localize command metadata.
    ///
    /// The closure arguments are:
    ///
    /// * `file` is the crate package name, version and the file from command declaration `@l10n: "file"`
    ///    value or is empty if `@l10n` was set to something else.
    /// * `cmd` is the command, the command event name should be used as key.
    /// * `meta` is the metadata name, for example `"name"`, should be used as attribute.
    /// * `txt` is text variable that must be set with the translation.
    pub fn init_l10n(&self, localize: impl Fn([&'static str; 3], Command, &'static str, CommandMetaVar<Txt>) + Send + Sync + 'static) {
        let mut sv = EVENTS_SV.write();
        match &mut sv.l10n {
            EventsL10n::Pending(a) => {
                for (f, k, a, t) in a.drain(..) {
                    localize(f, k, a, t);
                }
            }
            EventsL10n::Init(_) => panic!("EVENTS_L10N already has a localizer"),
        }
        sv.l10n = EventsL10n::Init(Box::new(localize));
    }
}