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}