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.rollback.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.rollback.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.skip_interaction_slot();
52        self.commands
53            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
54                direction: Direction::Row,
55                gap: 2,
56                align: Align::Start,
57                align_self: None,
58                justify: Justify::Start,
59                border: None,
60                border_sides: BorderSides::all(),
61                border_style: Style::new().fg(self.theme.border),
62                bg_color: None,
63                padding: Padding::default(),
64                margin: Margin::default(),
65                constraints: Constraints::default(),
66                title: None,
67                grow: 0,
68                group_name: None,
69            })));
70        for (idx, (key, action)) in bindings.iter().enumerate() {
71            if idx > 0 {
72                self.styled("·", Style::new().fg(self.theme.text_dim));
73            }
74            self.styled(*key, Style::new().bold().fg(self.theme.primary));
75            self.styled(*action, Style::new().fg(self.theme.text_dim));
76        }
77        self.commands.push(Command::EndContainer);
78        self.rollback.last_text_idx = None;
79
80        Response::none()
81    }
82
83    /// Render a help bar with custom key/description colors.
84    pub fn help_colored(
85        &mut self,
86        bindings: &[(&str, &str)],
87        key_color: Color,
88        text_color: Color,
89    ) -> Response {
90        if bindings.is_empty() {
91            return Response::none();
92        }
93
94        self.skip_interaction_slot();
95        self.commands
96            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
97                direction: Direction::Row,
98                gap: 2,
99                align: Align::Start,
100                align_self: None,
101                justify: Justify::Start,
102                border: None,
103                border_sides: BorderSides::all(),
104                border_style: Style::new().fg(self.theme.border),
105                bg_color: None,
106                padding: Padding::default(),
107                margin: Margin::default(),
108                constraints: Constraints::default(),
109                title: None,
110                grow: 0,
111                group_name: None,
112            })));
113        for (idx, (key, action)) in bindings.iter().enumerate() {
114            if idx > 0 {
115                self.styled("·", Style::new().fg(text_color));
116            }
117            self.styled(*key, Style::new().bold().fg(key_color));
118            self.styled(*action, Style::new().fg(text_color));
119        }
120        self.commands.push(Command::EndContainer);
121        self.rollback.last_text_idx = None;
122
123        Response::none()
124    }
125
126    // ── events ───────────────────────────────────────────────────────
127
128    /// Check if a character key was pressed this frame.
129    ///
130    /// Returns `true` if the key event has not been consumed by another widget.
131    pub fn key(&self, c: char) -> bool {
132        if (self.rollback.modal_active || self.prev_modal_active)
133            && self.rollback.overlay_depth == 0
134        {
135            return false;
136        }
137        self.events.iter().enumerate().any(|(i, e)| {
138            !self.consumed[i]
139                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
140        })
141    }
142
143    /// Check if a specific key code was pressed this frame.
144    ///
145    /// Returns `true` if the key event has not been consumed by another widget.
146    /// Blocked when a modal/overlay is active and the caller is outside the overlay.
147    /// Use [`raw_key_code`](Self::raw_key_code) for global shortcuts that must work
148    /// regardless of modal/overlay state.
149    pub fn key_code(&self, code: KeyCode) -> bool {
150        if (self.rollback.modal_active || self.prev_modal_active)
151            && self.rollback.overlay_depth == 0
152        {
153            return false;
154        }
155        self.events.iter().enumerate().any(|(i, e)| {
156            !self.consumed[i]
157                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
158        })
159    }
160
161    /// Check if a specific key code was pressed this frame, ignoring modal/overlay state.
162    ///
163    /// Unlike [`key_code`](Self::key_code), this method bypasses the modal/overlay guard
164    /// so it works even when a modal or overlay is active. Use this for global shortcuts
165    /// (e.g. Esc to close a modal, Ctrl+Q to quit) that must always be reachable.
166    ///
167    /// Returns `true` if the key event has not been consumed by another widget.
168    pub fn raw_key_code(&self, code: KeyCode) -> bool {
169        self.events.iter().enumerate().any(|(i, e)| {
170            !self.consumed[i]
171                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
172        })
173    }
174
175    /// Check if a character key was released this frame.
176    ///
177    /// Returns `true` if the key release event has not been consumed by another widget.
178    pub fn key_release(&self, c: char) -> bool {
179        if (self.rollback.modal_active || self.prev_modal_active)
180            && self.rollback.overlay_depth == 0
181        {
182            return false;
183        }
184        self.events.iter().enumerate().any(|(i, e)| {
185            !self.consumed[i]
186                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
187        })
188    }
189
190    /// Check if a specific key code was released this frame.
191    ///
192    /// Returns `true` if the key release event has not been consumed by another widget.
193    pub fn key_code_release(&self, code: KeyCode) -> bool {
194        if (self.rollback.modal_active || self.prev_modal_active)
195            && self.rollback.overlay_depth == 0
196        {
197            return false;
198        }
199        self.events.iter().enumerate().any(|(i, e)| {
200            !self.consumed[i]
201                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
202        })
203    }
204
205    /// Check for a character key press and consume the event, preventing other
206    /// handlers from seeing it.
207    ///
208    /// Returns `true` if the key was found unconsumed and is now consumed.
209    /// Unlike [`key()`](Self::key) which peeks without consuming, this claims
210    /// exclusive ownership of the event.
211    ///
212    /// Call **after** widgets if you want widgets to have priority over your
213    /// handler, or **before** widgets to intercept first.
214    pub fn consume_key(&mut self, c: char) -> bool {
215        if (self.rollback.modal_active || self.prev_modal_active)
216            && self.rollback.overlay_depth == 0
217        {
218            return false;
219        }
220        let index = self.available_key_presses().find_map(|(i, key)| {
221            if key.code == KeyCode::Char(c) {
222                Some(i)
223            } else {
224                None
225            }
226        });
227        if let Some(index) = index {
228            self.consume_indices([index]);
229            true
230        } else {
231            false
232        }
233    }
234
235    /// Check for a special key press and consume the event, preventing other
236    /// handlers from seeing it.
237    ///
238    /// Returns `true` if the key was found unconsumed and is now consumed.
239    /// Unlike [`key_code()`](Self::key_code) which peeks without consuming,
240    /// this claims exclusive ownership of the event.
241    ///
242    /// Call **after** widgets if you want widgets to have priority over your
243    /// handler, or **before** widgets to intercept first.
244    pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
245        if (self.rollback.modal_active || self.prev_modal_active)
246            && self.rollback.overlay_depth == 0
247        {
248            return false;
249        }
250        let index =
251            self.available_key_presses().find_map(
252                |(i, key)| {
253                    if key.code == code {
254                        Some(i)
255                    } else {
256                        None
257                    }
258                },
259            );
260        if let Some(index) = index {
261            self.consume_indices([index]);
262            true
263        } else {
264            false
265        }
266    }
267
268    /// Check if a character key with specific modifiers was pressed this frame.
269    ///
270    /// Returns `true` if the key event has not been consumed by another widget.
271    pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
272        if (self.rollback.modal_active || self.prev_modal_active)
273            && self.rollback.overlay_depth == 0
274        {
275            return false;
276        }
277        self.events.iter().enumerate().any(|(i, e)| {
278            !self.consumed[i]
279                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
280        })
281    }
282
283    /// Like [`key_mod`](Self::key_mod) but bypasses the modal/overlay guard.
284    pub fn raw_key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
285        self.events.iter().enumerate().any(|(i, e)| {
286            !self.consumed[i]
287                && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
288        })
289    }
290
291    /// Return the position of a left mouse button down event this frame, if any.
292    ///
293    /// Returns `None` if no unconsumed mouse-down event occurred.
294    pub fn mouse_down(&self) -> Option<(u32, u32)> {
295        if (self.rollback.modal_active || self.prev_modal_active)
296            && self.rollback.overlay_depth == 0
297        {
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::Mouse(mouse) = event {
305                if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
306                    return Some((mouse.x, mouse.y));
307                }
308            }
309            None
310        })
311    }
312
313    /// Return the position of a left mouse button drag event this frame, if any.
314    ///
315    /// Returns `None` if no unconsumed drag event occurred. Drag events fire
316    /// while the left button is held and the cursor moves.
317    pub fn mouse_drag(&self) -> Option<(u32, u32)> {
318        if (self.rollback.modal_active || self.prev_modal_active)
319            && self.rollback.overlay_depth == 0
320        {
321            return None;
322        }
323        self.events.iter().enumerate().find_map(|(i, event)| {
324            if self.consumed[i] {
325                return None;
326            }
327            if let Event::Mouse(mouse) = event {
328                if matches!(mouse.kind, MouseKind::Drag(MouseButton::Left)) {
329                    return Some((mouse.x, mouse.y));
330                }
331            }
332            None
333        })
334    }
335
336    /// Return the position of a left mouse button release event this frame, if any.
337    ///
338    /// Returns `None` if no unconsumed mouse-up event occurred.
339    pub fn mouse_up(&self) -> Option<(u32, u32)> {
340        if (self.rollback.modal_active || self.prev_modal_active)
341            && self.rollback.overlay_depth == 0
342        {
343            return None;
344        }
345        self.events.iter().enumerate().find_map(|(i, event)| {
346            if self.consumed[i] {
347                return None;
348            }
349            if let Event::Mouse(mouse) = event {
350                if matches!(mouse.kind, MouseKind::Up(MouseButton::Left)) {
351                    return Some((mouse.x, mouse.y));
352                }
353            }
354            None
355        })
356    }
357
358    /// Return the position of a mouse button down event for the specified button.
359    ///
360    /// This is a generalized version of [`mouse_down`](Self::mouse_down) that
361    /// accepts any [`MouseButton`].
362    pub fn mouse_down_button(&self, button: MouseButton) -> Option<(u32, u32)> {
363        if (self.rollback.modal_active || self.prev_modal_active)
364            && self.rollback.overlay_depth == 0
365        {
366            return None;
367        }
368        self.events.iter().enumerate().find_map(|(i, event)| {
369            if self.consumed[i] {
370                return None;
371            }
372            if let Event::Mouse(mouse) = event {
373                if matches!(&mouse.kind, MouseKind::Down(b) if *b == button) {
374                    return Some((mouse.x, mouse.y));
375                }
376            }
377            None
378        })
379    }
380
381    /// Return the position of a mouse drag event for the specified button.
382    pub fn mouse_drag_button(&self, button: MouseButton) -> Option<(u32, u32)> {
383        if (self.rollback.modal_active || self.prev_modal_active)
384            && self.rollback.overlay_depth == 0
385        {
386            return None;
387        }
388        self.events.iter().enumerate().find_map(|(i, event)| {
389            if self.consumed[i] {
390                return None;
391            }
392            if let Event::Mouse(mouse) = event {
393                if matches!(&mouse.kind, MouseKind::Drag(b) if *b == button) {
394                    return Some((mouse.x, mouse.y));
395                }
396            }
397            None
398        })
399    }
400
401    /// Return the position of a mouse button release event for the specified button.
402    pub fn mouse_up_button(&self, button: MouseButton) -> Option<(u32, u32)> {
403        if (self.rollback.modal_active || self.prev_modal_active)
404            && self.rollback.overlay_depth == 0
405        {
406            return None;
407        }
408        self.events.iter().enumerate().find_map(|(i, event)| {
409            if self.consumed[i] {
410                return None;
411            }
412            if let Event::Mouse(mouse) = event {
413                if matches!(&mouse.kind, MouseKind::Up(b) if *b == button) {
414                    return Some((mouse.x, mouse.y));
415                }
416            }
417            None
418        })
419    }
420
421    /// Return the current mouse cursor position, if known.
422    ///
423    /// The position is updated on every mouse move or click event. Returns
424    /// `None` until the first mouse event is received.
425    pub fn mouse_pos(&self) -> Option<(u32, u32)> {
426        self.mouse_pos
427    }
428
429    /// Return the first unconsumed paste event text, if any.
430    pub fn paste(&self) -> Option<&str> {
431        if (self.rollback.modal_active || self.prev_modal_active)
432            && self.rollback.overlay_depth == 0
433        {
434            return None;
435        }
436        self.events.iter().enumerate().find_map(|(i, event)| {
437            if self.consumed[i] {
438                return None;
439            }
440            if let Event::Paste(ref text) = event {
441                return Some(text.as_str());
442            }
443            None
444        })
445    }
446
447    /// Check if an unconsumed scroll-up event occurred this frame.
448    pub fn scroll_up(&self) -> bool {
449        if (self.rollback.modal_active || self.prev_modal_active)
450            && self.rollback.overlay_depth == 0
451        {
452            return false;
453        }
454        self.events.iter().enumerate().any(|(i, event)| {
455            !self.consumed[i]
456                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
457        })
458    }
459
460    /// Check if an unconsumed scroll-down event occurred this frame.
461    pub fn scroll_down(&self) -> bool {
462        if (self.rollback.modal_active || self.prev_modal_active)
463            && self.rollback.overlay_depth == 0
464        {
465            return false;
466        }
467        self.events.iter().enumerate().any(|(i, event)| {
468            !self.consumed[i]
469                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
470        })
471    }
472
473    /// Check if an unconsumed scroll-left event occurred this frame.
474    pub fn scroll_left(&self) -> bool {
475        if (self.rollback.modal_active || self.prev_modal_active)
476            && self.rollback.overlay_depth == 0
477        {
478            return false;
479        }
480        self.events.iter().enumerate().any(|(i, event)| {
481            !self.consumed[i]
482                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollLeft))
483        })
484    }
485
486    /// Check if an unconsumed scroll-right event occurred this frame.
487    pub fn scroll_right(&self) -> bool {
488        if (self.rollback.modal_active || self.prev_modal_active)
489            && self.rollback.overlay_depth == 0
490        {
491            return false;
492        }
493        self.events.iter().enumerate().any(|(i, event)| {
494            !self.consumed[i]
495                && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollRight))
496        })
497    }
498
499    /// Iterate over unconsumed events this frame, respecting the modal guard.
500    ///
501    /// Returns an empty iterator when a modal is active and the caller is not
502    /// inside an overlay. Use [`raw_events`](Self::raw_events) to bypass the
503    /// modal guard (e.g., for global hotkeys).
504    ///
505    /// # Example
506    ///
507    /// ```no_run
508    /// # slt::run(|ui: &mut slt::Context| {
509    /// for event in ui.events() {
510    ///     if let slt::Event::Mouse(mouse) = event {
511    ///         if matches!(mouse.kind, slt::MouseKind::Down(slt::MouseButton::Right)) {
512    ///             // handle right-click
513    ///         }
514    ///     }
515    /// }
516    /// # });
517    /// ```
518    pub fn events(&self) -> impl Iterator<Item = &Event> {
519        let blocked = (self.rollback.modal_active || self.prev_modal_active)
520            && self.rollback.overlay_depth == 0;
521        self.events.iter().enumerate().filter_map(move |(i, e)| {
522            if blocked || self.consumed[i] {
523                None
524            } else {
525                Some(e)
526            }
527        })
528    }
529
530    /// Iterate over all unconsumed events, bypassing the modal guard.
531    ///
532    /// Use this for global shortcuts that must work even when a modal or
533    /// overlay is active. Prefer [`events`](Self::events) for normal use.
534    pub fn raw_events(&self) -> impl Iterator<Item = &Event> + '_ {
535        self.events
536            .iter()
537            .enumerate()
538            .filter_map(|(i, e)| if self.consumed[i] { None } else { Some(e) })
539    }
540
541    /// Signal the run loop to exit after this frame.
542    pub fn quit(&mut self) {
543        self.should_quit = true;
544    }
545
546    /// Copy text to the system clipboard via OSC 52.
547    ///
548    /// Works transparently over SSH connections. The text is queued and
549    /// written to the terminal after the current frame renders.
550    ///
551    /// Requires a terminal that supports OSC 52 (most modern terminals:
552    /// Ghostty, kitty, WezTerm, iTerm2, Windows Terminal).
553    pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
554        self.clipboard_text = Some(text.into());
555    }
556
557    /// Get the current theme.
558    pub fn theme(&self) -> &Theme {
559        &self.theme
560    }
561
562    /// Resolve a [`ThemeColor`] token against the current theme.
563    pub fn color(&self, token: ThemeColor) -> Color {
564        self.theme.resolve(token)
565    }
566
567    /// Get the current spacing scale from the theme.
568    pub fn spacing(&self) -> Spacing {
569        self.theme.spacing
570    }
571
572    /// Change the theme for subsequent rendering.
573    ///
574    /// All widgets rendered after this call will use the new theme's colors.
575    pub fn set_theme(&mut self, theme: Theme) {
576        self.theme = theme;
577    }
578
579    /// Check if dark mode is active.
580    pub fn is_dark_mode(&self) -> bool {
581        self.rollback.dark_mode
582    }
583
584    /// Set dark mode. When true, dark_* style variants are applied.
585    pub fn set_dark_mode(&mut self, dark: bool) {
586        self.rollback.dark_mode = dark;
587    }
588
589    // ── info ─────────────────────────────────────────────────────────
590
591    /// Get the terminal width in cells.
592    pub fn width(&self) -> u32 {
593        self.area_width
594    }
595
596    /// Get the current terminal width breakpoint.
597    ///
598    /// Returns a [`Breakpoint`] based on the terminal width:
599    /// - `Xs`: < 40 columns
600    /// - `Sm`: 40-79 columns
601    /// - `Md`: 80-119 columns
602    /// - `Lg`: 120-159 columns
603    /// - `Xl`: >= 160 columns
604    ///
605    /// Use this for responsive layouts that adapt to terminal size:
606    /// ```no_run
607    /// # use slt::{Breakpoint, Context};
608    /// # slt::run(|ui: &mut Context| {
609    /// match ui.breakpoint() {
610    ///     Breakpoint::Xs | Breakpoint::Sm => {
611    ///         ui.col(|ui| { ui.text("Stacked layout"); });
612    ///     }
613    ///     _ => {
614    ///         ui.row(|ui| { ui.text("Side-by-side layout"); });
615    ///     }
616    /// }
617    /// # });
618    /// ```
619    pub fn breakpoint(&self) -> Breakpoint {
620        let w = self.area_width;
621        if w < 40 {
622            Breakpoint::Xs
623        } else if w < 80 {
624            Breakpoint::Sm
625        } else if w < 120 {
626            Breakpoint::Md
627        } else if w < 160 {
628            Breakpoint::Lg
629        } else {
630            Breakpoint::Xl
631        }
632    }
633
634    /// Get the terminal height in cells.
635    pub fn height(&self) -> u32 {
636        self.area_height
637    }
638
639    /// Get the current tick count (increments each frame).
640    ///
641    /// Useful for animations and time-based logic. The tick starts at 0 and
642    /// increases by 1 on every rendered frame.
643    pub fn tick(&self) -> u64 {
644        self.tick
645    }
646
647    /// Return whether the layout debugger is enabled.
648    ///
649    /// The debugger is toggled with F12 at runtime.
650    pub fn debug_enabled(&self) -> bool {
651        self.debug
652    }
653}