Skip to main content

slt/context/widgets_interactive/
events.rs

1use super::*;
2
3impl Context {
4    /// Render a horizontal divider line.
5    ///
6    /// The line is drawn with the theme's border color and expands to fill the
7    /// container width.
8    pub fn separator(&mut self) -> &mut Self {
9        self.commands.push(Command::Text {
10            content: "─".repeat(200),
11            cursor_offset: None,
12            style: Style::new().fg(self.theme.border).dim(),
13            grow: 0,
14            align: Align::Start,
15            wrap: false,
16            truncate: false,
17            margin: Margin::default(),
18            constraints: Constraints::default(),
19        });
20        self.last_text_idx = Some(self.commands.len() - 1);
21        self
22    }
23
24    /// Render a horizontal separator line with a custom color.
25    pub fn separator_colored(&mut self, color: Color) -> &mut Self {
26        self.commands.push(Command::Text {
27            content: "─".repeat(200),
28            cursor_offset: None,
29            style: Style::new().fg(color),
30            grow: 0,
31            align: Align::Start,
32            wrap: false,
33            truncate: false,
34            margin: Margin::default(),
35            constraints: Constraints::default(),
36        });
37        self.last_text_idx = Some(self.commands.len() - 1);
38        self
39    }
40
41    /// Render a help bar showing keybinding hints.
42    ///
43    /// `bindings` is a slice of `(key, action)` pairs. Keys are rendered in the
44    /// theme's primary color; actions in the dim text color. Pairs are separated
45    /// by a `·` character.
46    pub fn help(&mut self, bindings: &[(&str, &str)]) -> Response {
47        if bindings.is_empty() {
48            return Response::none();
49        }
50
51        self.interaction_count += 1;
52        self.commands.push(Command::BeginContainer {
53            direction: Direction::Row,
54            gap: 2,
55            align: Align::Start,
56            align_self: None,
57            justify: Justify::Start,
58            border: None,
59            border_sides: BorderSides::all(),
60            border_style: Style::new().fg(self.theme.border),
61            bg_color: None,
62            padding: Padding::default(),
63            margin: Margin::default(),
64            constraints: Constraints::default(),
65            title: None,
66            grow: 0,
67            group_name: None,
68        });
69        for (idx, (key, action)) in bindings.iter().enumerate() {
70            if idx > 0 {
71                self.styled("·", Style::new().fg(self.theme.text_dim));
72            }
73            self.styled(*key, Style::new().bold().fg(self.theme.primary));
74            self.styled(*action, Style::new().fg(self.theme.text_dim));
75        }
76        self.commands.push(Command::EndContainer);
77        self.last_text_idx = None;
78
79        Response::none()
80    }
81
82    /// Render a help bar with custom key/description colors.
83    pub fn help_colored(
84        &mut self,
85        bindings: &[(&str, &str)],
86        key_color: Color,
87        text_color: Color,
88    ) -> Response {
89        if bindings.is_empty() {
90            return Response::none();
91        }
92
93        self.interaction_count += 1;
94        self.commands.push(Command::BeginContainer {
95            direction: Direction::Row,
96            gap: 2,
97            align: Align::Start,
98            align_self: None,
99            justify: Justify::Start,
100            border: None,
101            border_sides: BorderSides::all(),
102            border_style: Style::new().fg(self.theme.border),
103            bg_color: None,
104            padding: Padding::default(),
105            margin: Margin::default(),
106            constraints: Constraints::default(),
107            title: None,
108            grow: 0,
109            group_name: None,
110        });
111        for (idx, (key, action)) in bindings.iter().enumerate() {
112            if idx > 0 {
113                self.styled("·", Style::new().fg(text_color));
114            }
115            self.styled(*key, Style::new().bold().fg(key_color));
116            self.styled(*action, Style::new().fg(text_color));
117        }
118        self.commands.push(Command::EndContainer);
119        self.last_text_idx = None;
120
121        Response::none()
122    }
123
124    // ── events ───────────────────────────────────────────────────────
125
126    /// Check if a character key was pressed this frame.
127    ///
128    /// Returns `true` if the key event has not been consumed by another widget.
129    pub fn key(&self, c: char) -> bool {
130        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
131            return false;
132        }
133        self.events.iter().enumerate().any(|(i, e)| {
134            !self.consumed[i]
135                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
136        })
137    }
138
139    /// Check if a specific key code was pressed this frame.
140    ///
141    /// Returns `true` if the key event has not been consumed by another widget.
142    /// Blocked when a modal/overlay is active and the caller is outside the overlay.
143    /// Use [`raw_key_code`](Self::raw_key_code) for global shortcuts that must work
144    /// regardless of modal/overlay state.
145    pub fn key_code(&self, code: KeyCode) -> bool {
146        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
147            return false;
148        }
149        self.events.iter().enumerate().any(|(i, e)| {
150            !self.consumed[i]
151                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
152        })
153    }
154
155    /// Check if a specific key code was pressed this frame, ignoring modal/overlay state.
156    ///
157    /// Unlike [`key_code`](Self::key_code), this method bypasses the modal/overlay guard
158    /// so it works even when a modal or overlay is active. Use this for global shortcuts
159    /// (e.g. Esc to close a modal, Ctrl+Q to quit) that must always be reachable.
160    ///
161    /// Returns `true` if the key event has not been consumed by another widget.
162    pub fn raw_key_code(&self, code: KeyCode) -> bool {
163        self.events.iter().enumerate().any(|(i, e)| {
164            !self.consumed[i]
165                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
166        })
167    }
168
169    /// Check if a character key was released this frame.
170    ///
171    /// Returns `true` if the key release event has not been consumed by another widget.
172    pub fn key_release(&self, c: char) -> bool {
173        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
174            return false;
175        }
176        self.events.iter().enumerate().any(|(i, e)| {
177            !self.consumed[i]
178                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
179        })
180    }
181
182    /// Check if a specific key code was released this frame.
183    ///
184    /// Returns `true` if the key release event has not been consumed by another widget.
185    pub fn key_code_release(&self, code: KeyCode) -> bool {
186        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
187            return false;
188        }
189        self.events.iter().enumerate().any(|(i, e)| {
190            !self.consumed[i]
191                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
192        })
193    }
194
195    /// Check for a character key press and consume the event, preventing other
196    /// handlers from seeing it.
197    ///
198    /// Returns `true` if the key was found unconsumed and is now consumed.
199    /// Unlike [`key()`](Self::key) which peeks without consuming, this claims
200    /// exclusive ownership of the event.
201    ///
202    /// Call **after** widgets if you want widgets to have priority over your
203    /// handler, or **before** widgets to intercept first.
204    pub fn consume_key(&mut self, c: char) -> bool {
205        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
206            return false;
207        }
208        for (i, event) in self.events.iter().enumerate() {
209            if self.consumed[i] {
210                continue;
211            }
212            if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
213            {
214                self.consumed[i] = true;
215                return true;
216            }
217        }
218        false
219    }
220
221    /// Check for a special key press and consume the event, preventing other
222    /// handlers from seeing it.
223    ///
224    /// Returns `true` if the key was found unconsumed and is now consumed.
225    /// Unlike [`key_code()`](Self::key_code) which peeks without consuming,
226    /// this claims exclusive ownership of the event.
227    ///
228    /// Call **after** widgets if you want widgets to have priority over your
229    /// handler, or **before** widgets to intercept first.
230    pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
231        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
232            return false;
233        }
234        for (i, event) in self.events.iter().enumerate() {
235            if self.consumed[i] {
236                continue;
237            }
238            if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code) {
239                self.consumed[i] = true;
240                return true;
241            }
242        }
243        false
244    }
245
246    /// Check if a character key with specific modifiers was pressed this frame.
247    ///
248    /// Returns `true` if the key event has not been consumed by another widget.
249    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
250        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
251            return false;
252        }
253        self.events.iter().enumerate().any(|(i, e)| {
254            !self.consumed[i]
255                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
256        })
257    }
258
259    /// Like [`key_mod`](Self::key_mod) but bypasses the modal/overlay guard.
260    pub fn raw_key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
261        self.events.iter().enumerate().any(|(i, e)| {
262            !self.consumed[i]
263                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
264        })
265    }
266
267    /// Return the position of a left mouse button down event this frame, if any.
268    ///
269    /// Returns `None` if no unconsumed mouse-down event occurred.
270    pub fn mouse_down(&self) -> Option<(u32, u32)> {
271        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
272            return None;
273        }
274        self.events.iter().enumerate().find_map(|(i, event)| {
275            if self.consumed[i] {
276                return None;
277            }
278            if let Event::Mouse(mouse) = event {
279                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
280                    return Some((mouse.x, mouse.y));
281                }
282            }
283            None
284        })
285    }
286
287    /// Return the current mouse cursor position, if known.
288    ///
289    /// The position is updated on every mouse move or click event. Returns
290    /// `None` until the first mouse event is received.
291    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
292        self.mouse_pos
293    }
294
295    /// Return the first unconsumed paste event text, if any.
296    pub fn paste(&self) -> Option<&str> {
297        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
298            return None;
299        }
300        self.events.iter().enumerate().find_map(|(i, event)| {
301            if self.consumed[i] {
302                return None;
303            }
304            if let Event::Paste(ref text) = event {
305                return Some(text.as_str());
306            }
307            None
308        })
309    }
310
311    /// Check if an unconsumed scroll-up event occurred this frame.
312    pub fn scroll_up(&self) -> bool {
313        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
314            return false;
315        }
316        self.events.iter().enumerate().any(|(i, event)| {
317            !self.consumed[i]
318                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
319        })
320    }
321
322    /// Check if an unconsumed scroll-down event occurred this frame.
323    pub fn scroll_down(&self) -> bool {
324        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
325            return false;
326        }
327        self.events.iter().enumerate().any(|(i, event)| {
328            !self.consumed[i]
329                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
330        })
331    }
332
333    /// Check if an unconsumed scroll-left event occurred this frame.
334    pub fn scroll_left(&self) -> bool {
335        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
336            return false;
337        }
338        self.events.iter().enumerate().any(|(i, event)| {
339            !self.consumed[i]
340                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollLeft))
341        })
342    }
343
344    /// Check if an unconsumed scroll-right event occurred this frame.
345    pub fn scroll_right(&self) -> bool {
346        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
347            return false;
348        }
349        self.events.iter().enumerate().any(|(i, event)| {
350            !self.consumed[i]
351                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollRight))
352        })
353    }
354
355    /// Signal the run loop to exit after this frame.
356    pub fn quit(&mut self) {
357        self.should_quit = true;
358    }
359
360    /// Copy text to the system clipboard via OSC 52.
361    ///
362    /// Works transparently over SSH connections. The text is queued and
363    /// written to the terminal after the current frame renders.
364    ///
365    /// Requires a terminal that supports OSC 52 (most modern terminals:
366    /// Ghostty, kitty, WezTerm, iTerm2, Windows Terminal).
367    pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
368        self.clipboard_text = Some(text.into());
369    }
370
371    /// Get the current theme.
372    pub fn theme(&self) -> &Theme {
373        &self.theme
374    }
375
376    /// Change the theme for subsequent rendering.
377    ///
378    /// All widgets rendered after this call will use the new theme's colors.
379    pub fn set_theme(&mut self, theme: Theme) {
380        self.theme = theme;
381    }
382
383    /// Check if dark mode is active.
384    pub fn is_dark_mode(&self) -> bool {
385        self.dark_mode
386    }
387
388    /// Set dark mode. When true, dark_* style variants are applied.
389    pub fn set_dark_mode(&mut self, dark: bool) {
390        self.dark_mode = dark;
391    }
392
393    // ── info ─────────────────────────────────────────────────────────
394
395    /// Get the terminal width in cells.
396    pub fn width(&self) -> u32 {
397        self.area_width
398    }
399
400    /// Get the current terminal width breakpoint.
401    ///
402    /// Returns a [`Breakpoint`] based on the terminal width:
403    /// - `Xs`: < 40 columns
404    /// - `Sm`: 40-79 columns
405    /// - `Md`: 80-119 columns
406    /// - `Lg`: 120-159 columns
407    /// - `Xl`: >= 160 columns
408    ///
409    /// Use this for responsive layouts that adapt to terminal size:
410    /// ```no_run
411    /// # use slt::{Breakpoint, Context};
412    /// # slt::run(|ui: &mut Context| {
413    /// match ui.breakpoint() {
414    ///     Breakpoint::Xs | Breakpoint::Sm => {
415    ///         ui.col(|ui| { ui.text("Stacked layout"); });
416    ///     }
417    ///     _ => {
418    ///         ui.row(|ui| { ui.text("Side-by-side layout"); });
419    ///     }
420    /// }
421    /// # });
422    /// ```
423    pub fn breakpoint(&self) -> Breakpoint {
424        let w = self.area_width;
425        if w < 40 {
426            Breakpoint::Xs
427        } else if w < 80 {
428            Breakpoint::Sm
429        } else if w < 120 {
430            Breakpoint::Md
431        } else if w < 160 {
432            Breakpoint::Lg
433        } else {
434            Breakpoint::Xl
435        }
436    }
437
438    /// Get the terminal height in cells.
439    pub fn height(&self) -> u32 {
440        self.area_height
441    }
442
443    /// Get the current tick count (increments each frame).
444    ///
445    /// Useful for animations and time-based logic. The tick starts at 0 and
446    /// increases by 1 on every rendered frame.
447    pub fn tick(&self) -> u64 {
448        self.tick
449    }
450
451    /// Return whether the layout debugger is enabled.
452    ///
453    /// The debugger is toggled with F12 at runtime.
454    pub fn debug_enabled(&self) -> bool {
455        self.debug
456    }
457}