Skip to main content

euv_ui/component/vconsole/hook/
impl.rs

1use super::*;
2
3/// Implements the Console struct providing web console API methods.
4///
5/// Each method outputs to both the browser developer console and the
6/// vConsole panel signal, with appropriate log level classification.
7/// Methods are associated functions that internally access the global
8/// Console instance, so callers never need to hold a reference.
9impl Console {
10    /// Initializes the global Console log signal.
11    ///
12    /// Must be called once during application startup before any `Console::log`,
13    /// `Console::warn`, `Console::error`, or `Console::push` calls.
14    ///
15    /// OPT-23: also populates the shared `RefCell` backing store that
16    /// `Console::push` uses for in-place appends. The `RefCell` and the
17    /// `Signal` always share the same `Vec` snapshot — every `push`
18    /// writes to the `RefCell` first, then re-broadcasts via the signal.
19    pub fn init() {
20        let logs_ref: Rc<RefCell<Vec<ConsoleEntry>>> = Rc::new(RefCell::new(Vec::new()));
21        install_console_log_ref(logs_ref);
22        let signal: Signal<Vec<ConsoleEntry>> = Signal::create(Vec::new());
23        CONSOLE_LOG_SIGNAL.set(signal);
24    }
25
26    /// Logs an informational message (equivalent to console.log).
27    ///
28    /// The vConsole panel entry is appended only when `Console::init` has
29    /// been called; the browser console output always happens.
30    ///
31    /// # Arguments
32    ///
33    /// - `M: AsRef<str>` - The message to log.
34    pub fn log<M>(message: M)
35    where
36        M: AsRef<str>,
37    {
38        let message_ref: &str = message.as_ref();
39        console::log_1(&message_ref.into());
40        Self::append_entry(ConsoleEntry::new(LogLevel::Log, message_ref.to_string()));
41    }
42
43    /// Logs a warning message (equivalent to console.warn).
44    ///
45    /// The vConsole panel entry is appended only when `Console::init` has
46    /// been called; the browser console output always happens.
47    ///
48    /// # Arguments
49    ///
50    /// - `M: AsRef<str>` - The warning message to log.
51    pub fn warn<M>(message: M)
52    where
53        M: AsRef<str>,
54    {
55        let message_ref: &str = message.as_ref();
56        console::warn_1(&message_ref.into());
57        Self::append_entry(ConsoleEntry::new(LogLevel::Warn, message_ref.to_string()));
58    }
59
60    /// Logs an error message (equivalent to console.error).
61    ///
62    /// The vConsole panel entry is appended only when `Console::init` has
63    /// been called; the browser console output always happens.
64    ///
65    /// # Arguments
66    ///
67    /// - `M: AsRef<str>` - The error message to log.
68    pub fn error<M>(message: M)
69    where
70        M: AsRef<str>,
71    {
72        let message_ref: &str = message.as_ref();
73        console::error_1(&message_ref.into());
74        Self::append_entry(ConsoleEntry::new(LogLevel::Error, message_ref.to_string()));
75    }
76
77    /// Clears all log entries from the vConsole panel signal.
78    ///
79    /// No-op when `Console::init` has not been called yet.
80    ///
81    /// OPT-23: when the shared `RefCell` backing store is installed,
82    /// clears it in place before re-broadcasting an empty vec, so the
83    /// two storage sites stay in sync.
84    pub fn clear() {
85        if let Some(logs_ref) = console_log_ref() {
86            logs_ref.borrow_mut().clear();
87        }
88        let Some(log) = Self::get_signal() else {
89            return;
90        };
91        log.set(Vec::new());
92    }
93
94    /// Returns the global vConsole log signal, if initialized.
95    ///
96    /// # Returns
97    ///
98    /// - `Option<Signal<Vec<ConsoleEntry>>>` - The console log signal, or
99    ///   `None` when `Console::init` has not been called yet.
100    pub(crate) fn get_signal() -> Option<Signal<Vec<ConsoleEntry>>> {
101        CONSOLE_LOG_SIGNAL.loaded()
102    }
103
104    /// Creates a click event handler that opens the vConsole fab panel.
105    ///
106    /// Pushes an overlay state and sets the panel visibility signal to true.
107    ///
108    /// # Arguments
109    ///
110    /// - `Signal<bool>` - The signal controlling panel visibility.
111    ///
112    /// # Returns
113    ///
114    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that opens the panel.
115    pub(crate) fn fab_on_click(panel_open: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
116        Some(Rc::new(move |_: Event| {
117            let closer: Rc<dyn Fn()> = Rc::new(move || {
118                panel_open.set(false);
119            });
120            Router::overlay_stack_push(closer);
121            panel_open.set(true);
122        }))
123    }
124
125    /// Filters and reverses console log entries based on the current filter signal value.
126    ///
127    /// # Arguments
128    ///
129    /// - `Signal<Vec<ConsoleEntry>>` - The console log signal.
130    /// - `Signal<LogFilter>` - The current filter level signal.
131    ///
132    /// # Returns
133    ///
134    /// - `Vec<(usize, ConsoleEntry)>` - The filtered and reversed entries with original indices.
135    pub(crate) fn filter_entries(
136        logs: Signal<Vec<ConsoleEntry>>,
137        filter: Signal<LogFilter>,
138    ) -> Vec<(usize, ConsoleEntry)> {
139        let log_list: Vec<ConsoleEntry> = logs.get();
140        let filter_value: LogFilter = filter.get();
141        let mut result: Vec<(usize, ConsoleEntry)> = log_list
142            .iter()
143            .enumerate()
144            .filter(|(_, entry): &(usize, &ConsoleEntry)| match filter_value {
145                LogFilter::All => true,
146                LogFilter::Log => entry.get_level() == LogLevel::Log,
147                LogFilter::Warn => entry.get_level() == LogLevel::Warn,
148                LogFilter::Error => entry.get_level() == LogLevel::Error,
149            })
150            .map(|(index, entry): (usize, &ConsoleEntry)| (index, entry.clone()))
151            .collect();
152        result.reverse();
153        result
154    }
155
156    /// Appends an entry to the vConsole log signal, trimming if over capacity.
157    ///
158    /// No-op when `Console::init` has not been called yet.
159    ///
160    /// OPT-23: when the shared `RefCell` backing store is available
161    /// (the common case after `Console::init`), this appends in place
162    /// to the `RefCell` and re-broadcasts via the signal in a single
163    /// `set` call. The previous signal-only path had to clone the
164    /// entire log vec via `Signal::get` before pushing — the new path
165    /// mutates in place and clones only the snapshot it forwards to
166    /// `set`.
167    ///
168    /// # Arguments
169    ///
170    /// - `ConsoleEntry` - The console entry to append.
171    fn append_entry(entry: ConsoleEntry) {
172        if let Some(logs_ref) = console_log_ref() {
173            let mut logs: std::cell::RefMut<'_, Vec<ConsoleEntry>> = logs_ref.borrow_mut();
174            logs.push(entry);
175            if logs.len() > MAX_CONSOLE_LOG_ENTRIES {
176                let excess: usize = logs.len() - MAX_CONSOLE_LOG_ENTRIES;
177                logs.drain(0..excess);
178            }
179            let snapshot: Vec<ConsoleEntry> = logs.clone();
180            drop(logs);
181            Self::replace_signal(snapshot);
182            return;
183        }
184        let Some(log) = Self::get_signal() else {
185            return;
186        };
187        let mut current: Vec<ConsoleEntry> = log.get();
188        current.push(entry);
189        if current.len() > MAX_CONSOLE_LOG_ENTRIES {
190            let excess: usize = current.len() - MAX_CONSOLE_LOG_ENTRIES;
191            current.drain(0..excess);
192        }
193        log.set(current);
194    }
195
196    /// OPT-23: append-only mutation API for the vConsole log signal.
197    ///
198    /// Public escape hatch for callers (and tests) that want to push
199    /// a `ConsoleEntry` without going through the `log`/`warn`/`error`
200    /// helpers. Uses the shared `RefCell` backing store for an
201    /// in-place append, then re-broadcasts via the signal so existing
202    /// reactive subscribers re-render.
203    ///
204    /// Falls back to the signal-only path when `Console::init` has
205    /// not yet installed the shared `RefCell`.
206    ///
207    /// # Arguments
208    ///
209    /// - `ConsoleEntry` - The console entry to append.
210    pub fn push(entry: ConsoleEntry) {
211        Self::append_entry(entry);
212    }
213
214    /// OPT-23: replaces the public log signal value with the given
215    /// snapshot. Used by `append_entry` after mutating the shared
216    /// `RefCell`, so subscribers receive the latest snapshot without
217    /// the `RefCell` borrow aliasing the signal listener registry.
218    fn replace_signal(next: Vec<ConsoleEntry>) {
219        if let Some(log) = Self::get_signal() {
220            log.set(next);
221        }
222    }
223}
224
225/// Implements the Display trait for LogFilter to render filter button labels.
226impl Display for LogFilter {
227    /// Formats the [`LogFilter`] via the supplied formatter.
228    ///
229    /// # Arguments
230    ///
231    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
232    ///
233    /// # Returns
234    ///
235    /// - `FmtResult` - Result of the formatting operation.
236    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
237        let label: &str = match self {
238            LogFilter::All => "All",
239            LogFilter::Log => "Log",
240            LogFilter::Warn => "Warn",
241            LogFilter::Error => "Error",
242        };
243        write!(formatter, "{}", label)
244    }
245}
246
247/// Implementation of log level badge rendering.
248impl LogLevel {
249    /// Returns the short badge label for a log level.
250    ///
251    /// # Returns
252    ///
253    /// - `&str` - The badge label string ("LOG", "WRN", "ERR").
254    pub(crate) fn badge(self) -> &'static str {
255        match self {
256            LogLevel::Log => "LOG",
257            LogLevel::Warn => "WRN",
258            LogLevel::Error => "ERR",
259        }
260    }
261}
262
263/// Implementation of log filter event handlers.
264impl LogFilter {
265    /// Creates a click event handler that sets the log filter to "All".
266    ///
267    /// # Arguments
268    ///
269    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
270    ///
271    /// # Returns
272    ///
273    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to All.
274    pub(crate) fn on_filter_all(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
275        Some(Rc::new(move |_: Event| {
276            filter_signal.set(LogFilter::All);
277        }))
278    }
279
280    /// Creates a click event handler that sets the log filter to "Log".
281    ///
282    /// # Arguments
283    ///
284    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
285    ///
286    /// # Returns
287    ///
288    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Log.
289    pub(crate) fn on_filter_log(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
290        Some(Rc::new(move |_: Event| {
291            filter_signal.set(LogFilter::Log);
292        }))
293    }
294
295    /// Creates a click event handler that sets the log filter to "Warn".
296    ///
297    /// # Arguments
298    ///
299    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
300    ///
301    /// # Returns
302    ///
303    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Warn.
304    pub(crate) fn on_filter_warn(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
305        Some(Rc::new(move |_: Event| {
306            filter_signal.set(LogFilter::Warn);
307        }))
308    }
309
310    /// Creates a click event handler that sets the log filter to "Error".
311    ///
312    /// # Arguments
313    ///
314    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
315    ///
316    /// # Returns
317    ///
318    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Error.
319    pub(crate) fn on_filter_error(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
320        Some(Rc::new(move |_: Event| {
321            filter_signal.set(LogFilter::Error);
322        }))
323    }
324}