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}