Skip to main content

slt/context/widgets_interactive/
events.rs

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