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}