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    /// Appends an entry to the vConsole log signal, trimming if over capacity.
126    ///
127    /// No-op when `Console::init` has not been called yet.
128    ///
129    /// OPT-23: when the shared `RefCell` backing store is available
130    /// (the common case after `Console::init`), this appends in place
131    /// to the `RefCell` and re-broadcasts via the signal in a single
132    /// `set` call. The previous signal-only path had to clone the
133    /// entire log vec via `Signal::get` before pushing — the new path
134    /// mutates in place and clones only the snapshot it forwards to
135    /// `set`.
136    ///
137    /// # Arguments
138    ///
139    /// - `ConsoleEntry` - The console entry to append.
140    fn append_entry(entry: ConsoleEntry) {
141        if let Some(logs_ref) = console_log_ref() {
142            let mut logs: std::cell::RefMut<'_, Vec<ConsoleEntry>> = logs_ref.borrow_mut();
143            logs.push(entry);
144            if logs.len() > MAX_CONSOLE_LOG_ENTRIES {
145                let excess: usize = logs.len() - MAX_CONSOLE_LOG_ENTRIES;
146                logs.drain(0..excess);
147            }
148            let snapshot: Vec<ConsoleEntry> = logs.clone();
149            drop(logs);
150            Self::replace_signal(snapshot);
151            return;
152        }
153        let Some(log) = Self::get_signal() else {
154            return;
155        };
156        let mut current: Vec<ConsoleEntry> = log.get();
157        current.push(entry);
158        if current.len() > MAX_CONSOLE_LOG_ENTRIES {
159            let excess: usize = current.len() - MAX_CONSOLE_LOG_ENTRIES;
160            current.drain(0..excess);
161        }
162        log.set(current);
163    }
164
165    /// OPT-23: append-only mutation API for the vConsole log signal.
166    ///
167    /// Public escape hatch for callers (and tests) that want to push
168    /// a `ConsoleEntry` without going through the `log`/`warn`/`error`
169    /// helpers. Uses the shared `RefCell` backing store for an
170    /// in-place append, then re-broadcasts via the signal so existing
171    /// reactive subscribers re-render.
172    ///
173    /// Falls back to the signal-only path when `Console::init` has
174    /// not yet installed the shared `RefCell`.
175    ///
176    /// # Arguments
177    ///
178    /// - `ConsoleEntry` - The console entry to append.
179    pub fn push(entry: ConsoleEntry) {
180        Self::append_entry(entry);
181    }
182
183    /// OPT-23: replaces the public log signal value with the given
184    /// snapshot. Used by `append_entry` after mutating the shared
185    /// `RefCell`, so subscribers receive the latest snapshot without
186    /// the `RefCell` borrow aliasing the signal listener registry.
187    fn replace_signal(next: Vec<ConsoleEntry>) {
188        if let Some(log) = Self::get_signal() {
189            log.set(next);
190        }
191    }
192}
193
194/// Implements the Display trait for LogFilter to render filter button labels.
195impl Display for LogFilter {
196    /// Formats the [`LogFilter`] via the supplied formatter.
197    ///
198    /// # Arguments
199    ///
200    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
201    ///
202    /// # Returns
203    ///
204    /// - `FmtResult` - Result of the formatting operation.
205    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
206        let label: &str = match self {
207            LogFilter::All => "All",
208            LogFilter::Log => "Log",
209            LogFilter::Warn => "Warn",
210            LogFilter::Error => "Error",
211        };
212        write!(formatter, "{}", label)
213    }
214}
215
216/// Implementation of log level badge rendering.
217impl LogLevel {
218    /// Returns the short badge label for a log level.
219    ///
220    /// # Returns
221    ///
222    /// - `&str` - The badge label string ("LOG", "WRN", "ERR").
223    pub(crate) fn badge(self) -> &'static str {
224        match self {
225            LogLevel::Log => "LOG",
226            LogLevel::Warn => "WRN",
227            LogLevel::Error => "ERR",
228        }
229    }
230}
231
232/// Implementation of log filter event handlers.
233impl LogFilter {
234    /// Creates a click event handler that sets the log filter to "All".
235    ///
236    /// # Arguments
237    ///
238    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
239    ///
240    /// # Returns
241    ///
242    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to All.
243    pub(crate) fn on_filter_all(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
244        Some(Rc::new(move |_: Event| {
245            filter_signal.set(LogFilter::All);
246        }))
247    }
248
249    /// Creates a click event handler that sets the log filter to "Log".
250    ///
251    /// # Arguments
252    ///
253    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
254    ///
255    /// # Returns
256    ///
257    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Log.
258    pub(crate) fn on_filter_log(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
259        Some(Rc::new(move |_: Event| {
260            filter_signal.set(LogFilter::Log);
261        }))
262    }
263
264    /// Creates a click event handler that sets the log filter to "Warn".
265    ///
266    /// # Arguments
267    ///
268    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
269    ///
270    /// # Returns
271    ///
272    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Warn.
273    pub(crate) fn on_filter_warn(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
274        Some(Rc::new(move |_: Event| {
275            filter_signal.set(LogFilter::Warn);
276        }))
277    }
278
279    /// Creates a click event handler that sets the log filter to "Error".
280    ///
281    /// # Arguments
282    ///
283    /// - `Signal<LogFilter>` - The signal controlling the active log filter.
284    ///
285    /// # Returns
286    ///
287    /// - `Option<Rc<dyn Fn(Event)>>` - A click handler that sets filter to Error.
288    pub(crate) fn on_filter_error(filter_signal: Signal<LogFilter>) -> Option<Rc<dyn Fn(Event)>> {
289        Some(Rc::new(move |_: Event| {
290            filter_signal.set(LogFilter::Error);
291        }))
292    }
293}