1use crate::chart::{build_histogram_config, render_chart, Candle, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
3use crate::halfblock::HalfBlockImage;
4use crate::layout::{Command, Direction};
5use crate::rect::Rect;
6use crate::style::{
7 Align, Border, BorderSides, Breakpoint, Color, Constraints, ContainerStyle, Justify, Margin,
8 Modifiers, Padding, Style, Theme, WidgetColors,
9};
10use crate::widgets::{
11 ApprovalAction, ButtonVariant, CalendarState, CommandPaletteState, ContextItem,
12 FilePickerState, FormField, FormState, ListState, MultiSelectState, RadioState, ScreenState,
13 ScrollState, SelectState, SpinnerState, StreamingTextState, TableState, TabsState,
14 TextInputState, TextareaState, ToastLevel, ToastState, ToolApprovalState, TreeState,
15};
16use crate::FrameState;
17use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
18
19#[allow(dead_code)]
20fn slt_assert(condition: bool, msg: &str) {
21 if !condition {
22 panic!("[SLT] {}", msg);
23 }
24}
25
26#[cfg(debug_assertions)]
27#[allow(dead_code, clippy::print_stderr)]
28fn slt_warn(msg: &str) {
29 eprintln!("\x1b[33m[SLT warning]\x1b[0m {}", msg);
30}
31
32#[cfg(not(debug_assertions))]
33#[allow(dead_code)]
34fn slt_warn(_msg: &str) {}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq)]
38pub struct State<T> {
39 idx: usize,
40 _marker: std::marker::PhantomData<T>,
41}
42
43impl<T: 'static> State<T> {
44 pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
46 ui.hook_states[self.idx]
47 .downcast_ref::<T>()
48 .unwrap_or_else(|| {
49 panic!(
50 "use_state type mismatch at hook index {} — expected {}",
51 self.idx,
52 std::any::type_name::<T>()
53 )
54 })
55 }
56
57 pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
59 ui.hook_states[self.idx]
60 .downcast_mut::<T>()
61 .unwrap_or_else(|| {
62 panic!(
63 "use_state type mismatch at hook index {} — expected {}",
64 self.idx,
65 std::any::type_name::<T>()
66 )
67 })
68 }
69}
70
71#[derive(Debug, Clone, Default)]
90#[must_use = "Response contains interaction state — check .clicked, .hovered, or .changed"]
91pub struct Response {
92 pub clicked: bool,
94 pub hovered: bool,
96 pub changed: bool,
98 pub focused: bool,
100 pub rect: Rect,
102}
103
104impl Response {
105 pub fn none() -> Self {
107 Self::default()
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum BarDirection {
114 Horizontal,
116 Vertical,
118}
119
120#[derive(Debug, Clone)]
122pub struct Bar {
123 pub label: String,
125 pub value: f64,
127 pub color: Option<Color>,
129 pub text_value: Option<String>,
131 pub value_style: Option<Style>,
133}
134
135impl Bar {
136 pub fn new(label: impl Into<String>, value: f64) -> Self {
138 Self {
139 label: label.into(),
140 value,
141 color: None,
142 text_value: None,
143 value_style: None,
144 }
145 }
146
147 pub fn color(mut self, color: Color) -> Self {
149 self.color = Some(color);
150 self
151 }
152
153 pub fn text_value(mut self, text: impl Into<String>) -> Self {
155 self.text_value = Some(text.into());
156 self
157 }
158
159 pub fn value_style(mut self, style: Style) -> Self {
161 self.value_style = Some(style);
162 self
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
168pub struct BarChartConfig {
169 pub direction: BarDirection,
171 pub bar_width: u16,
173 pub bar_gap: u16,
175 pub group_gap: u16,
177 pub max_value: Option<f64>,
179}
180
181impl Default for BarChartConfig {
182 fn default() -> Self {
183 Self {
184 direction: BarDirection::Horizontal,
185 bar_width: 1,
186 bar_gap: 0,
187 group_gap: 2,
188 max_value: None,
189 }
190 }
191}
192
193impl BarChartConfig {
194 pub fn direction(&mut self, direction: BarDirection) -> &mut Self {
196 self.direction = direction;
197 self
198 }
199
200 pub fn bar_width(&mut self, bar_width: u16) -> &mut Self {
202 self.bar_width = bar_width.max(1);
203 self
204 }
205
206 pub fn bar_gap(&mut self, bar_gap: u16) -> &mut Self {
208 self.bar_gap = bar_gap;
209 self
210 }
211
212 pub fn group_gap(&mut self, group_gap: u16) -> &mut Self {
214 self.group_gap = group_gap;
215 self
216 }
217
218 pub fn max_value(&mut self, max_value: f64) -> &mut Self {
220 self.max_value = Some(max_value);
221 self
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct BarGroup {
228 pub label: String,
230 pub bars: Vec<Bar>,
232}
233
234impl BarGroup {
235 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
237 Self {
238 label: label.into(),
239 bars,
240 }
241 }
242}
243
244pub trait Widget {
306 type Response;
309
310 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
316}
317
318pub struct Context {
334 pub(crate) commands: Vec<Command>,
336 pub(crate) events: Vec<Event>,
337 pub(crate) consumed: Vec<bool>,
338 pub(crate) should_quit: bool,
339 pub(crate) area_width: u32,
340 pub(crate) area_height: u32,
341 pub(crate) tick: u64,
342 pub(crate) focus_index: usize,
343 pub(crate) focus_count: usize,
344 pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
345 pub(crate) hook_cursor: usize,
346 prev_focus_count: usize,
347 pub(crate) modal_focus_start: usize,
348 pub(crate) modal_focus_count: usize,
349 prev_modal_focus_start: usize,
350 prev_modal_focus_count: usize,
351 scroll_count: usize,
352 prev_scroll_infos: Vec<(u32, u32)>,
353 prev_scroll_rects: Vec<Rect>,
354 interaction_count: usize,
355 pub(crate) prev_hit_map: Vec<Rect>,
356 pub(crate) group_stack: Vec<String>,
357 pub(crate) prev_group_rects: Vec<(String, Rect)>,
358 group_count: usize,
359 prev_focus_groups: Vec<Option<String>>,
360 _prev_focus_rects: Vec<(usize, Rect)>,
361 mouse_pos: Option<(u32, u32)>,
362 click_pos: Option<(u32, u32)>,
363 last_text_idx: Option<usize>,
364 overlay_depth: usize,
365 pub(crate) modal_active: bool,
366 prev_modal_active: bool,
367 pub(crate) clipboard_text: Option<String>,
368 debug: bool,
369 theme: Theme,
370 pub(crate) dark_mode: bool,
371 pub(crate) is_real_terminal: bool,
372 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
373 pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
374 pub(crate) pending_tooltips: Vec<PendingTooltip>,
375 pub(crate) text_color_stack: Vec<Option<Color>>,
376}
377
378type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
379
380pub(crate) struct PendingTooltip {
381 pub anchor_rect: Rect,
382 pub lines: Vec<String>,
383}
384
385struct ContextSnapshot {
386 cmd_count: usize,
387 last_text_idx: Option<usize>,
388 focus_count: usize,
389 interaction_count: usize,
390 scroll_count: usize,
391 group_count: usize,
392 group_stack_len: usize,
393 overlay_depth: usize,
394 modal_active: bool,
395 modal_focus_start: usize,
396 modal_focus_count: usize,
397 hook_cursor: usize,
398 hook_states_len: usize,
399 dark_mode: bool,
400 deferred_draws_len: usize,
401 notification_queue_len: usize,
402 pending_tooltips_len: usize,
403 text_color_stack_len: usize,
404}
405
406impl ContextSnapshot {
407 fn capture(ctx: &Context) -> Self {
408 Self {
409 cmd_count: ctx.commands.len(),
410 last_text_idx: ctx.last_text_idx,
411 focus_count: ctx.focus_count,
412 interaction_count: ctx.interaction_count,
413 scroll_count: ctx.scroll_count,
414 group_count: ctx.group_count,
415 group_stack_len: ctx.group_stack.len(),
416 overlay_depth: ctx.overlay_depth,
417 modal_active: ctx.modal_active,
418 modal_focus_start: ctx.modal_focus_start,
419 modal_focus_count: ctx.modal_focus_count,
420 hook_cursor: ctx.hook_cursor,
421 hook_states_len: ctx.hook_states.len(),
422 dark_mode: ctx.dark_mode,
423 deferred_draws_len: ctx.deferred_draws.len(),
424 notification_queue_len: ctx.notification_queue.len(),
425 pending_tooltips_len: ctx.pending_tooltips.len(),
426 text_color_stack_len: ctx.text_color_stack.len(),
427 }
428 }
429
430 fn restore(&self, ctx: &mut Context) {
431 ctx.commands.truncate(self.cmd_count);
432 ctx.last_text_idx = self.last_text_idx;
433 ctx.focus_count = self.focus_count;
434 ctx.interaction_count = self.interaction_count;
435 ctx.scroll_count = self.scroll_count;
436 ctx.group_count = self.group_count;
437 ctx.group_stack.truncate(self.group_stack_len);
438 ctx.overlay_depth = self.overlay_depth;
439 ctx.modal_active = self.modal_active;
440 ctx.modal_focus_start = self.modal_focus_start;
441 ctx.modal_focus_count = self.modal_focus_count;
442 ctx.hook_cursor = self.hook_cursor;
443 ctx.hook_states.truncate(self.hook_states_len);
444 ctx.dark_mode = self.dark_mode;
445 ctx.deferred_draws.truncate(self.deferred_draws_len);
446 ctx.notification_queue.truncate(self.notification_queue_len);
447 ctx.pending_tooltips.truncate(self.pending_tooltips_len);
448 ctx.text_color_stack.truncate(self.text_color_stack_len);
449 }
450}
451
452#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
473pub struct ContainerBuilder<'a> {
474 ctx: &'a mut Context,
475 gap: u32,
476 row_gap: Option<u32>,
477 col_gap: Option<u32>,
478 align: Align,
479 align_self_value: Option<Align>,
480 justify: Justify,
481 border: Option<Border>,
482 border_sides: BorderSides,
483 border_style: Style,
484 bg: Option<Color>,
485 text_color: Option<Color>,
486 dark_bg: Option<Color>,
487 dark_border_style: Option<Style>,
488 group_hover_bg: Option<Color>,
489 group_hover_border_style: Option<Style>,
490 group_name: Option<String>,
491 padding: Padding,
492 margin: Margin,
493 constraints: Constraints,
494 title: Option<(String, Style)>,
495 grow: u16,
496 scroll_offset: Option<u32>,
497}
498
499#[derive(Debug, Clone, Copy)]
506struct CanvasPixel {
507 bits: u32,
508 color: Color,
509}
510
511#[derive(Debug, Clone)]
513struct CanvasLabel {
514 x: usize,
515 y: usize,
516 text: String,
517 color: Color,
518}
519
520#[derive(Debug, Clone)]
522struct CanvasLayer {
523 grid: Vec<Vec<CanvasPixel>>,
524 labels: Vec<CanvasLabel>,
525}
526
527pub struct CanvasContext {
529 layers: Vec<CanvasLayer>,
530 cols: usize,
531 rows: usize,
532 px_w: usize,
533 px_h: usize,
534 current_color: Color,
535}
536
537impl CanvasContext {
538 fn new(cols: usize, rows: usize) -> Self {
539 Self {
540 layers: vec![Self::new_layer(cols, rows)],
541 cols,
542 rows,
543 px_w: cols * 2,
544 px_h: rows * 4,
545 current_color: Color::Reset,
546 }
547 }
548
549 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
550 CanvasLayer {
551 grid: vec![
552 vec![
553 CanvasPixel {
554 bits: 0,
555 color: Color::Reset,
556 };
557 cols
558 ];
559 rows
560 ],
561 labels: Vec::new(),
562 }
563 }
564
565 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
566 self.layers.last_mut()
567 }
568
569 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
570 if x >= self.px_w || y >= self.px_h {
571 return;
572 }
573
574 let char_col = x / 2;
575 let char_row = y / 4;
576 let sub_col = x % 2;
577 let sub_row = y % 4;
578 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
579 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
580
581 let bit = if sub_col == 0 {
582 LEFT_BITS[sub_row]
583 } else {
584 RIGHT_BITS[sub_row]
585 };
586
587 if let Some(layer) = self.current_layer_mut() {
588 let cell = &mut layer.grid[char_row][char_col];
589 let new_bits = cell.bits | bit;
590 if new_bits != cell.bits {
591 cell.bits = new_bits;
592 cell.color = color;
593 }
594 }
595 }
596
597 fn dot_isize(&mut self, x: isize, y: isize) {
598 if x >= 0 && y >= 0 {
599 self.dot(x as usize, y as usize);
600 }
601 }
602
603 pub fn width(&self) -> usize {
605 self.px_w
606 }
607
608 pub fn height(&self) -> usize {
610 self.px_h
611 }
612
613 pub fn dot(&mut self, x: usize, y: usize) {
615 self.dot_with_color(x, y, self.current_color);
616 }
617
618 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
620 let (mut x, mut y) = (x0 as isize, y0 as isize);
621 let (x1, y1) = (x1 as isize, y1 as isize);
622 let dx = (x1 - x).abs();
623 let dy = -(y1 - y).abs();
624 let sx = if x < x1 { 1 } else { -1 };
625 let sy = if y < y1 { 1 } else { -1 };
626 let mut err = dx + dy;
627
628 loop {
629 self.dot_isize(x, y);
630 if x == x1 && y == y1 {
631 break;
632 }
633 let e2 = 2 * err;
634 if e2 >= dy {
635 err += dy;
636 x += sx;
637 }
638 if e2 <= dx {
639 err += dx;
640 y += sy;
641 }
642 }
643 }
644
645 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
647 if w == 0 || h == 0 {
648 return;
649 }
650
651 self.line(x, y, x + w.saturating_sub(1), y);
652 self.line(
653 x + w.saturating_sub(1),
654 y,
655 x + w.saturating_sub(1),
656 y + h.saturating_sub(1),
657 );
658 self.line(
659 x + w.saturating_sub(1),
660 y + h.saturating_sub(1),
661 x,
662 y + h.saturating_sub(1),
663 );
664 self.line(x, y + h.saturating_sub(1), x, y);
665 }
666
667 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
669 let mut x = r as isize;
670 let mut y: isize = 0;
671 let mut err: isize = 1 - x;
672 let (cx, cy) = (cx as isize, cy as isize);
673
674 while x >= y {
675 for &(dx, dy) in &[
676 (x, y),
677 (y, x),
678 (-x, y),
679 (-y, x),
680 (x, -y),
681 (y, -x),
682 (-x, -y),
683 (-y, -x),
684 ] {
685 let px = cx + dx;
686 let py = cy + dy;
687 self.dot_isize(px, py);
688 }
689
690 y += 1;
691 if err < 0 {
692 err += 2 * y + 1;
693 } else {
694 x -= 1;
695 err += 2 * (y - x) + 1;
696 }
697 }
698 }
699
700 pub fn set_color(&mut self, color: Color) {
702 self.current_color = color;
703 }
704
705 pub fn color(&self) -> Color {
707 self.current_color
708 }
709
710 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
712 if w == 0 || h == 0 {
713 return;
714 }
715
716 let x_end = x.saturating_add(w).min(self.px_w);
717 let y_end = y.saturating_add(h).min(self.px_h);
718 if x >= x_end || y >= y_end {
719 return;
720 }
721
722 for yy in y..y_end {
723 self.line(x, yy, x_end.saturating_sub(1), yy);
724 }
725 }
726
727 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
729 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
730 for y in (cy - r)..=(cy + r) {
731 let dy = y - cy;
732 let span_sq = (r * r - dy * dy).max(0);
733 let dx = (span_sq as f64).sqrt() as isize;
734 for x in (cx - dx)..=(cx + dx) {
735 self.dot_isize(x, y);
736 }
737 }
738 }
739
740 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
742 self.line(x0, y0, x1, y1);
743 self.line(x1, y1, x2, y2);
744 self.line(x2, y2, x0, y0);
745 }
746
747 pub fn filled_triangle(
749 &mut self,
750 x0: usize,
751 y0: usize,
752 x1: usize,
753 y1: usize,
754 x2: usize,
755 y2: usize,
756 ) {
757 let vertices = [
758 (x0 as isize, y0 as isize),
759 (x1 as isize, y1 as isize),
760 (x2 as isize, y2 as isize),
761 ];
762 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
763 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
764
765 for y in min_y..=max_y {
766 let mut intersections: Vec<f64> = Vec::new();
767
768 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
769 let (x_a, y_a) = vertices[edge.0];
770 let (x_b, y_b) = vertices[edge.1];
771 if y_a == y_b {
772 continue;
773 }
774
775 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
776 (x_a, y_a, x_b, y_b)
777 } else {
778 (x_b, y_b, x_a, y_a)
779 };
780
781 if y < y_start || y >= y_end {
782 continue;
783 }
784
785 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
786 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
787 }
788
789 intersections.sort_by(|a, b| a.total_cmp(b));
790 let mut i = 0usize;
791 while i + 1 < intersections.len() {
792 let x_start = intersections[i].ceil() as isize;
793 let x_end = intersections[i + 1].floor() as isize;
794 for x in x_start..=x_end {
795 self.dot_isize(x, y);
796 }
797 i += 2;
798 }
799 }
800
801 self.triangle(x0, y0, x1, y1, x2, y2);
802 }
803
804 pub fn points(&mut self, pts: &[(usize, usize)]) {
806 for &(x, y) in pts {
807 self.dot(x, y);
808 }
809 }
810
811 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
813 for window in pts.windows(2) {
814 if let [(x0, y0), (x1, y1)] = window {
815 self.line(*x0, *y0, *x1, *y1);
816 }
817 }
818 }
819
820 pub fn print(&mut self, x: usize, y: usize, text: &str) {
823 if text.is_empty() {
824 return;
825 }
826
827 let color = self.current_color;
828 if let Some(layer) = self.current_layer_mut() {
829 layer.labels.push(CanvasLabel {
830 x,
831 y,
832 text: text.to_string(),
833 color,
834 });
835 }
836 }
837
838 pub fn layer(&mut self) {
840 self.layers.push(Self::new_layer(self.cols, self.rows));
841 }
842
843 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
844 let mut final_grid = vec![
845 vec![
846 CanvasPixel {
847 bits: 0,
848 color: Color::Reset,
849 };
850 self.cols
851 ];
852 self.rows
853 ];
854 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
855 vec![vec![None; self.cols]; self.rows];
856
857 for layer in &self.layers {
858 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
859 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
860 let src = layer.grid[row][col];
861 if src.bits == 0 {
862 continue;
863 }
864
865 let merged = dst.bits | src.bits;
866 if merged != dst.bits {
867 dst.bits = merged;
868 dst.color = src.color;
869 }
870 }
871 }
872
873 for label in &layer.labels {
874 let row = label.y / 4;
875 if row >= self.rows {
876 continue;
877 }
878 let start_col = label.x / 2;
879 for (offset, ch) in label.text.chars().enumerate() {
880 let col = start_col + offset;
881 if col >= self.cols {
882 break;
883 }
884 labels_overlay[row][col] = Some((ch, label.color));
885 }
886 }
887 }
888
889 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
890 for row in 0..self.rows {
891 let mut segments: Vec<(String, Color)> = Vec::new();
892 let mut current_color: Option<Color> = None;
893 let mut current_text = String::new();
894
895 for col in 0..self.cols {
896 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
897 (label_ch, label_color)
898 } else {
899 let bits = final_grid[row][col].bits;
900 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
901 (ch, final_grid[row][col].color)
902 };
903
904 match current_color {
905 Some(c) if c == color => {
906 current_text.push(ch);
907 }
908 Some(c) => {
909 segments.push((std::mem::take(&mut current_text), c));
910 current_text.push(ch);
911 current_color = Some(color);
912 }
913 None => {
914 current_text.push(ch);
915 current_color = Some(color);
916 }
917 }
918 }
919
920 if let Some(color) = current_color {
921 segments.push((current_text, color));
922 }
923 lines.push(segments);
924 }
925
926 lines
927 }
928}
929
930macro_rules! define_breakpoint_methods {
931 (
932 base = $base:ident,
933 arg = $arg:ident : $arg_ty:ty,
934 xs = $xs_fn:ident => [$( $xs_doc:literal ),* $(,)?],
935 sm = $sm_fn:ident => [$( $sm_doc:literal ),* $(,)?],
936 md = $md_fn:ident => [$( $md_doc:literal ),* $(,)?],
937 lg = $lg_fn:ident => [$( $lg_doc:literal ),* $(,)?],
938 xl = $xl_fn:ident => [$( $xl_doc:literal ),* $(,)?],
939 at = $at_fn:ident => [$( $at_doc:literal ),* $(,)?]
940 ) => {
941 $(#[doc = $xs_doc])*
942 pub fn $xs_fn(self, $arg: $arg_ty) -> Self {
943 if self.ctx.breakpoint() == Breakpoint::Xs {
944 self.$base($arg)
945 } else {
946 self
947 }
948 }
949
950 $(#[doc = $sm_doc])*
951 pub fn $sm_fn(self, $arg: $arg_ty) -> Self {
952 if self.ctx.breakpoint() == Breakpoint::Sm {
953 self.$base($arg)
954 } else {
955 self
956 }
957 }
958
959 $(#[doc = $md_doc])*
960 pub fn $md_fn(self, $arg: $arg_ty) -> Self {
961 if self.ctx.breakpoint() == Breakpoint::Md {
962 self.$base($arg)
963 } else {
964 self
965 }
966 }
967
968 $(#[doc = $lg_doc])*
969 pub fn $lg_fn(self, $arg: $arg_ty) -> Self {
970 if self.ctx.breakpoint() == Breakpoint::Lg {
971 self.$base($arg)
972 } else {
973 self
974 }
975 }
976
977 $(#[doc = $xl_doc])*
978 pub fn $xl_fn(self, $arg: $arg_ty) -> Self {
979 if self.ctx.breakpoint() == Breakpoint::Xl {
980 self.$base($arg)
981 } else {
982 self
983 }
984 }
985
986 $(#[doc = $at_doc])*
987 pub fn $at_fn(self, bp: Breakpoint, $arg: $arg_ty) -> Self {
988 if self.ctx.breakpoint() == bp {
989 self.$base($arg)
990 } else {
991 self
992 }
993 }
994 };
995}
996
997impl<'a> ContainerBuilder<'a> {
998 pub fn apply(mut self, style: &ContainerStyle) -> Self {
1003 if let Some(v) = style.border {
1004 self.border = Some(v);
1005 }
1006 if let Some(v) = style.border_sides {
1007 self.border_sides = v;
1008 }
1009 if let Some(v) = style.border_style {
1010 self.border_style = v;
1011 }
1012 if let Some(v) = style.bg {
1013 self.bg = Some(v);
1014 }
1015 if let Some(v) = style.dark_bg {
1016 self.dark_bg = Some(v);
1017 }
1018 if let Some(v) = style.dark_border_style {
1019 self.dark_border_style = Some(v);
1020 }
1021 if let Some(v) = style.padding {
1022 self.padding = v;
1023 }
1024 if let Some(v) = style.margin {
1025 self.margin = v;
1026 }
1027 if let Some(v) = style.gap {
1028 self.gap = v;
1029 }
1030 if let Some(v) = style.row_gap {
1031 self.row_gap = Some(v);
1032 }
1033 if let Some(v) = style.col_gap {
1034 self.col_gap = Some(v);
1035 }
1036 if let Some(v) = style.grow {
1037 self.grow = v;
1038 }
1039 if let Some(v) = style.align {
1040 self.align = v;
1041 }
1042 if let Some(v) = style.align_self {
1043 self.align_self_value = Some(v);
1044 }
1045 if let Some(v) = style.justify {
1046 self.justify = v;
1047 }
1048 if let Some(v) = style.text_color {
1049 self.text_color = Some(v);
1050 }
1051 if let Some(w) = style.w {
1052 self.constraints.min_width = Some(w);
1053 self.constraints.max_width = Some(w);
1054 }
1055 if let Some(h) = style.h {
1056 self.constraints.min_height = Some(h);
1057 self.constraints.max_height = Some(h);
1058 }
1059 if let Some(v) = style.min_w {
1060 self.constraints.min_width = Some(v);
1061 }
1062 if let Some(v) = style.max_w {
1063 self.constraints.max_width = Some(v);
1064 }
1065 if let Some(v) = style.min_h {
1066 self.constraints.min_height = Some(v);
1067 }
1068 if let Some(v) = style.max_h {
1069 self.constraints.max_height = Some(v);
1070 }
1071 if let Some(v) = style.w_pct {
1072 self.constraints.width_pct = Some(v);
1073 }
1074 if let Some(v) = style.h_pct {
1075 self.constraints.height_pct = Some(v);
1076 }
1077 self
1078 }
1079
1080 pub fn border(mut self, border: Border) -> Self {
1082 self.border = Some(border);
1083 self
1084 }
1085
1086 pub fn border_top(mut self, show: bool) -> Self {
1088 self.border_sides.top = show;
1089 self
1090 }
1091
1092 pub fn border_right(mut self, show: bool) -> Self {
1094 self.border_sides.right = show;
1095 self
1096 }
1097
1098 pub fn border_bottom(mut self, show: bool) -> Self {
1100 self.border_sides.bottom = show;
1101 self
1102 }
1103
1104 pub fn border_left(mut self, show: bool) -> Self {
1106 self.border_sides.left = show;
1107 self
1108 }
1109
1110 pub fn border_sides(mut self, sides: BorderSides) -> Self {
1112 self.border_sides = sides;
1113 self
1114 }
1115
1116 pub fn border_x(self) -> Self {
1118 self.border_sides(BorderSides {
1119 top: false,
1120 right: true,
1121 bottom: false,
1122 left: true,
1123 })
1124 }
1125
1126 pub fn border_y(self) -> Self {
1128 self.border_sides(BorderSides {
1129 top: true,
1130 right: false,
1131 bottom: true,
1132 left: false,
1133 })
1134 }
1135
1136 pub fn rounded(self) -> Self {
1138 self.border(Border::Rounded)
1139 }
1140
1141 pub fn border_style(mut self, style: Style) -> Self {
1143 self.border_style = style;
1144 self
1145 }
1146
1147 pub fn border_fg(mut self, color: Color) -> Self {
1149 self.border_style = self.border_style.fg(color);
1150 self
1151 }
1152
1153 pub fn dark_border_style(mut self, style: Style) -> Self {
1155 self.dark_border_style = Some(style);
1156 self
1157 }
1158
1159 pub fn bg(mut self, color: Color) -> Self {
1161 self.bg = Some(color);
1162 self
1163 }
1164
1165 pub fn text_color(mut self, color: Color) -> Self {
1168 self.text_color = Some(color);
1169 self
1170 }
1171
1172 pub fn dark_bg(mut self, color: Color) -> Self {
1174 self.dark_bg = Some(color);
1175 self
1176 }
1177
1178 pub fn group_hover_bg(mut self, color: Color) -> Self {
1180 self.group_hover_bg = Some(color);
1181 self
1182 }
1183
1184 pub fn group_hover_border_style(mut self, style: Style) -> Self {
1186 self.group_hover_border_style = Some(style);
1187 self
1188 }
1189
1190 pub fn p(self, value: u32) -> Self {
1194 self.pad(value)
1195 }
1196
1197 pub fn pad(mut self, value: u32) -> Self {
1199 self.padding = Padding::all(value);
1200 self
1201 }
1202
1203 pub fn px(mut self, value: u32) -> Self {
1205 self.padding.left = value;
1206 self.padding.right = value;
1207 self
1208 }
1209
1210 pub fn py(mut self, value: u32) -> Self {
1212 self.padding.top = value;
1213 self.padding.bottom = value;
1214 self
1215 }
1216
1217 pub fn pt(mut self, value: u32) -> Self {
1219 self.padding.top = value;
1220 self
1221 }
1222
1223 pub fn pr(mut self, value: u32) -> Self {
1225 self.padding.right = value;
1226 self
1227 }
1228
1229 pub fn pb(mut self, value: u32) -> Self {
1231 self.padding.bottom = value;
1232 self
1233 }
1234
1235 pub fn pl(mut self, value: u32) -> Self {
1237 self.padding.left = value;
1238 self
1239 }
1240
1241 pub fn padding(mut self, padding: Padding) -> Self {
1243 self.padding = padding;
1244 self
1245 }
1246
1247 pub fn m(mut self, value: u32) -> Self {
1251 self.margin = Margin::all(value);
1252 self
1253 }
1254
1255 pub fn mx(mut self, value: u32) -> Self {
1257 self.margin.left = value;
1258 self.margin.right = value;
1259 self
1260 }
1261
1262 pub fn my(mut self, value: u32) -> Self {
1264 self.margin.top = value;
1265 self.margin.bottom = value;
1266 self
1267 }
1268
1269 pub fn mt(mut self, value: u32) -> Self {
1271 self.margin.top = value;
1272 self
1273 }
1274
1275 pub fn mr(mut self, value: u32) -> Self {
1277 self.margin.right = value;
1278 self
1279 }
1280
1281 pub fn mb(mut self, value: u32) -> Self {
1283 self.margin.bottom = value;
1284 self
1285 }
1286
1287 pub fn ml(mut self, value: u32) -> Self {
1289 self.margin.left = value;
1290 self
1291 }
1292
1293 pub fn margin(mut self, margin: Margin) -> Self {
1295 self.margin = margin;
1296 self
1297 }
1298
1299 pub fn w(mut self, value: u32) -> Self {
1303 self.constraints.min_width = Some(value);
1304 self.constraints.max_width = Some(value);
1305 self
1306 }
1307
1308 define_breakpoint_methods!(
1309 base = w,
1310 arg = value: u32,
1311 xs = xs_w => [
1312 "Width applied only at Xs breakpoint (< 40 cols).",
1313 "",
1314 "# Example",
1315 "```ignore",
1316 "ui.container().w(20).md_w(40).lg_w(60).col(|ui| { ... });",
1317 "```"
1318 ],
1319 sm = sm_w => ["Width applied only at Sm breakpoint (40-79 cols)."],
1320 md = md_w => ["Width applied only at Md breakpoint (80-119 cols)."],
1321 lg = lg_w => ["Width applied only at Lg breakpoint (120-159 cols)."],
1322 xl = xl_w => ["Width applied only at Xl breakpoint (>= 160 cols)."],
1323 at = w_at => ["Width applied only at the given breakpoint."]
1324 );
1325
1326 pub fn h(mut self, value: u32) -> Self {
1328 self.constraints.min_height = Some(value);
1329 self.constraints.max_height = Some(value);
1330 self
1331 }
1332
1333 define_breakpoint_methods!(
1334 base = h,
1335 arg = value: u32,
1336 xs = xs_h => ["Height applied only at Xs breakpoint (< 40 cols)."],
1337 sm = sm_h => ["Height applied only at Sm breakpoint (40-79 cols)."],
1338 md = md_h => ["Height applied only at Md breakpoint (80-119 cols)."],
1339 lg = lg_h => ["Height applied only at Lg breakpoint (120-159 cols)."],
1340 xl = xl_h => ["Height applied only at Xl breakpoint (>= 160 cols)."],
1341 at = h_at => ["Height applied only at the given breakpoint."]
1342 );
1343
1344 pub fn min_w(mut self, value: u32) -> Self {
1346 self.constraints.min_width = Some(value);
1347 self
1348 }
1349
1350 define_breakpoint_methods!(
1351 base = min_w,
1352 arg = value: u32,
1353 xs = xs_min_w => ["Minimum width applied only at Xs breakpoint (< 40 cols)."],
1354 sm = sm_min_w => ["Minimum width applied only at Sm breakpoint (40-79 cols)."],
1355 md = md_min_w => ["Minimum width applied only at Md breakpoint (80-119 cols)."],
1356 lg = lg_min_w => ["Minimum width applied only at Lg breakpoint (120-159 cols)."],
1357 xl = xl_min_w => ["Minimum width applied only at Xl breakpoint (>= 160 cols)."],
1358 at = min_w_at => ["Minimum width applied only at the given breakpoint."]
1359 );
1360
1361 pub fn max_w(mut self, value: u32) -> Self {
1363 self.constraints.max_width = Some(value);
1364 self
1365 }
1366
1367 define_breakpoint_methods!(
1368 base = max_w,
1369 arg = value: u32,
1370 xs = xs_max_w => ["Maximum width applied only at Xs breakpoint (< 40 cols)."],
1371 sm = sm_max_w => ["Maximum width applied only at Sm breakpoint (40-79 cols)."],
1372 md = md_max_w => ["Maximum width applied only at Md breakpoint (80-119 cols)."],
1373 lg = lg_max_w => ["Maximum width applied only at Lg breakpoint (120-159 cols)."],
1374 xl = xl_max_w => ["Maximum width applied only at Xl breakpoint (>= 160 cols)."],
1375 at = max_w_at => ["Maximum width applied only at the given breakpoint."]
1376 );
1377
1378 pub fn min_h(mut self, value: u32) -> Self {
1380 self.constraints.min_height = Some(value);
1381 self
1382 }
1383
1384 pub fn max_h(mut self, value: u32) -> Self {
1386 self.constraints.max_height = Some(value);
1387 self
1388 }
1389
1390 pub fn min_width(mut self, value: u32) -> Self {
1392 self.constraints.min_width = Some(value);
1393 self
1394 }
1395
1396 pub fn max_width(mut self, value: u32) -> Self {
1398 self.constraints.max_width = Some(value);
1399 self
1400 }
1401
1402 pub fn min_height(mut self, value: u32) -> Self {
1404 self.constraints.min_height = Some(value);
1405 self
1406 }
1407
1408 pub fn max_height(mut self, value: u32) -> Self {
1410 self.constraints.max_height = Some(value);
1411 self
1412 }
1413
1414 pub fn w_pct(mut self, pct: u8) -> Self {
1416 self.constraints.width_pct = Some(pct.min(100));
1417 self
1418 }
1419
1420 pub fn h_pct(mut self, pct: u8) -> Self {
1422 self.constraints.height_pct = Some(pct.min(100));
1423 self
1424 }
1425
1426 pub fn constraints(mut self, constraints: Constraints) -> Self {
1428 self.constraints = constraints;
1429 self
1430 }
1431
1432 pub fn gap(mut self, gap: u32) -> Self {
1436 self.gap = gap;
1437 self
1438 }
1439
1440 pub fn row_gap(mut self, value: u32) -> Self {
1443 self.row_gap = Some(value);
1444 self
1445 }
1446
1447 pub fn col_gap(mut self, value: u32) -> Self {
1450 self.col_gap = Some(value);
1451 self
1452 }
1453
1454 define_breakpoint_methods!(
1455 base = gap,
1456 arg = value: u32,
1457 xs = xs_gap => ["Gap applied only at Xs breakpoint (< 40 cols)."],
1458 sm = sm_gap => ["Gap applied only at Sm breakpoint (40-79 cols)."],
1459 md = md_gap => [
1460 "Gap applied only at Md breakpoint (80-119 cols).",
1461 "",
1462 "# Example",
1463 "```ignore",
1464 "ui.container().gap(0).md_gap(2).col(|ui| { ... });",
1465 "```"
1466 ],
1467 lg = lg_gap => ["Gap applied only at Lg breakpoint (120-159 cols)."],
1468 xl = xl_gap => ["Gap applied only at Xl breakpoint (>= 160 cols)."],
1469 at = gap_at => ["Gap applied only at the given breakpoint."]
1470 );
1471
1472 pub fn grow(mut self, grow: u16) -> Self {
1474 self.grow = grow;
1475 self
1476 }
1477
1478 define_breakpoint_methods!(
1479 base = grow,
1480 arg = value: u16,
1481 xs = xs_grow => ["Grow factor applied only at Xs breakpoint (< 40 cols)."],
1482 sm = sm_grow => ["Grow factor applied only at Sm breakpoint (40-79 cols)."],
1483 md = md_grow => ["Grow factor applied only at Md breakpoint (80-119 cols)."],
1484 lg = lg_grow => ["Grow factor applied only at Lg breakpoint (120-159 cols)."],
1485 xl = xl_grow => ["Grow factor applied only at Xl breakpoint (>= 160 cols)."],
1486 at = grow_at => ["Grow factor applied only at the given breakpoint."]
1487 );
1488
1489 define_breakpoint_methods!(
1490 base = p,
1491 arg = value: u32,
1492 xs = xs_p => ["Uniform padding applied only at Xs breakpoint (< 40 cols)."],
1493 sm = sm_p => ["Uniform padding applied only at Sm breakpoint (40-79 cols)."],
1494 md = md_p => ["Uniform padding applied only at Md breakpoint (80-119 cols)."],
1495 lg = lg_p => ["Uniform padding applied only at Lg breakpoint (120-159 cols)."],
1496 xl = xl_p => ["Uniform padding applied only at Xl breakpoint (>= 160 cols)."],
1497 at = p_at => ["Padding applied only at the given breakpoint."]
1498 );
1499
1500 pub fn align(mut self, align: Align) -> Self {
1504 self.align = align;
1505 self
1506 }
1507
1508 pub fn center(self) -> Self {
1510 self.align(Align::Center)
1511 }
1512
1513 pub fn justify(mut self, justify: Justify) -> Self {
1515 self.justify = justify;
1516 self
1517 }
1518
1519 pub fn space_between(self) -> Self {
1521 self.justify(Justify::SpaceBetween)
1522 }
1523
1524 pub fn space_around(self) -> Self {
1526 self.justify(Justify::SpaceAround)
1527 }
1528
1529 pub fn space_evenly(self) -> Self {
1531 self.justify(Justify::SpaceEvenly)
1532 }
1533
1534 pub fn flex_center(self) -> Self {
1536 self.justify(Justify::Center).align(Align::Center)
1537 }
1538
1539 pub fn align_self(mut self, align: Align) -> Self {
1542 self.align_self_value = Some(align);
1543 self
1544 }
1545
1546 pub fn title(self, title: impl Into<String>) -> Self {
1550 self.title_styled(title, Style::new())
1551 }
1552
1553 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1555 self.title = Some((title.into(), style));
1556 self
1557 }
1558
1559 pub fn scroll_offset(mut self, offset: u32) -> Self {
1563 self.scroll_offset = Some(offset);
1564 self
1565 }
1566
1567 fn group_name(mut self, name: String) -> Self {
1568 self.group_name = Some(name);
1569 self
1570 }
1571
1572 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1577 self.finish(Direction::Column, f)
1578 }
1579
1580 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1585 self.finish(Direction::Row, f)
1586 }
1587
1588 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1593 self.gap = 0;
1594 self.finish(Direction::Row, f)
1595 }
1596
1597 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1612 let draw_id = self.ctx.deferred_draws.len();
1613 self.ctx.deferred_draws.push(Some(Box::new(f)));
1614 self.ctx.interaction_count += 1;
1615 self.ctx.commands.push(Command::RawDraw {
1616 draw_id,
1617 constraints: self.constraints,
1618 grow: self.grow,
1619 margin: self.margin,
1620 });
1621 }
1622
1623 fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1624 let interaction_id = self.ctx.next_interaction_id();
1625 let resolved_gap = match direction {
1626 Direction::Column => self.row_gap.unwrap_or(self.gap),
1627 Direction::Row => self.col_gap.unwrap_or(self.gap),
1628 };
1629
1630 let in_hovered_group = self
1631 .group_name
1632 .as_ref()
1633 .map(|name| self.ctx.is_group_hovered(name))
1634 .unwrap_or(false)
1635 || self
1636 .ctx
1637 .group_stack
1638 .last()
1639 .map(|name| self.ctx.is_group_hovered(name))
1640 .unwrap_or(false);
1641 let in_focused_group = self
1642 .group_name
1643 .as_ref()
1644 .map(|name| self.ctx.is_group_focused(name))
1645 .unwrap_or(false)
1646 || self
1647 .ctx
1648 .group_stack
1649 .last()
1650 .map(|name| self.ctx.is_group_focused(name))
1651 .unwrap_or(false);
1652
1653 let resolved_bg = if self.ctx.dark_mode {
1654 self.dark_bg.or(self.bg)
1655 } else {
1656 self.bg
1657 };
1658 let resolved_border_style = if self.ctx.dark_mode {
1659 self.dark_border_style.unwrap_or(self.border_style)
1660 } else {
1661 self.border_style
1662 };
1663 let bg_color = if in_hovered_group || in_focused_group {
1664 self.group_hover_bg.or(resolved_bg)
1665 } else {
1666 resolved_bg
1667 };
1668 let border_style = if in_hovered_group || in_focused_group {
1669 self.group_hover_border_style
1670 .unwrap_or(resolved_border_style)
1671 } else {
1672 resolved_border_style
1673 };
1674 let group_name = self.group_name.take();
1675 let is_group_container = group_name.is_some();
1676
1677 if let Some(scroll_offset) = self.scroll_offset {
1678 self.ctx.commands.push(Command::BeginScrollable {
1679 grow: self.grow,
1680 border: self.border,
1681 border_sides: self.border_sides,
1682 border_style,
1683 padding: self.padding,
1684 margin: self.margin,
1685 constraints: self.constraints,
1686 title: self.title,
1687 scroll_offset,
1688 });
1689 } else {
1690 self.ctx.commands.push(Command::BeginContainer {
1691 direction,
1692 gap: resolved_gap,
1693 align: self.align,
1694 align_self: self.align_self_value,
1695 justify: self.justify,
1696 border: self.border,
1697 border_sides: self.border_sides,
1698 border_style,
1699 bg_color,
1700 padding: self.padding,
1701 margin: self.margin,
1702 constraints: self.constraints,
1703 title: self.title,
1704 grow: self.grow,
1705 group_name,
1706 });
1707 }
1708 self.ctx.text_color_stack.push(self.text_color);
1709 f(self.ctx);
1710 self.ctx.text_color_stack.pop();
1711 self.ctx.commands.push(Command::EndContainer);
1712 self.ctx.last_text_idx = None;
1713
1714 if is_group_container {
1715 self.ctx.group_stack.pop();
1716 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1717 }
1718
1719 self.ctx.response_for(interaction_id)
1720 }
1721}
1722
1723impl Context {
1724 pub(crate) fn new(
1725 events: Vec<Event>,
1726 width: u32,
1727 height: u32,
1728 state: &mut FrameState,
1729 theme: Theme,
1730 ) -> Self {
1731 let consumed = vec![false; events.len()];
1732
1733 let mut mouse_pos = state.last_mouse_pos;
1734 let mut click_pos = None;
1735 for event in &events {
1736 if let Event::Mouse(mouse) = event {
1737 mouse_pos = Some((mouse.x, mouse.y));
1738 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1739 click_pos = Some((mouse.x, mouse.y));
1740 }
1741 }
1742 }
1743
1744 let mut focus_index = state.focus_index;
1745 if let Some((mx, my)) = click_pos {
1746 let mut best: Option<(usize, u64)> = None;
1747 for &(fid, rect) in &state.prev_focus_rects {
1748 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1749 let area = rect.width as u64 * rect.height as u64;
1750 if best.map_or(true, |(_, ba)| area < ba) {
1751 best = Some((fid, area));
1752 }
1753 }
1754 }
1755 if let Some((fid, _)) = best {
1756 focus_index = fid;
1757 }
1758 }
1759
1760 Self {
1761 commands: Vec::new(),
1762 events,
1763 consumed,
1764 should_quit: false,
1765 area_width: width,
1766 area_height: height,
1767 tick: state.tick,
1768 focus_index,
1769 focus_count: 0,
1770 hook_states: std::mem::take(&mut state.hook_states),
1771 hook_cursor: 0,
1772 prev_focus_count: state.prev_focus_count,
1773 modal_focus_start: 0,
1774 modal_focus_count: 0,
1775 prev_modal_focus_start: state.prev_modal_focus_start,
1776 prev_modal_focus_count: state.prev_modal_focus_count,
1777 scroll_count: 0,
1778 prev_scroll_infos: std::mem::take(&mut state.prev_scroll_infos),
1779 prev_scroll_rects: std::mem::take(&mut state.prev_scroll_rects),
1780 interaction_count: 0,
1781 prev_hit_map: std::mem::take(&mut state.prev_hit_map),
1782 group_stack: Vec::new(),
1783 prev_group_rects: std::mem::take(&mut state.prev_group_rects),
1784 group_count: 0,
1785 prev_focus_groups: std::mem::take(&mut state.prev_focus_groups),
1786 _prev_focus_rects: std::mem::take(&mut state.prev_focus_rects),
1787 mouse_pos,
1788 click_pos,
1789 last_text_idx: None,
1790 overlay_depth: 0,
1791 modal_active: false,
1792 prev_modal_active: state.prev_modal_active,
1793 clipboard_text: None,
1794 debug: state.debug_mode,
1795 theme,
1796 dark_mode: theme.is_dark,
1797 is_real_terminal: false,
1798 deferred_draws: Vec::new(),
1799 notification_queue: std::mem::take(&mut state.notification_queue),
1800 pending_tooltips: Vec::new(),
1801 text_color_stack: Vec::new(),
1802 }
1803 }
1804
1805 pub(crate) fn process_focus_keys(&mut self) {
1806 for (i, event) in self.events.iter().enumerate() {
1807 if let Event::Key(key) = event {
1808 if key.kind != KeyEventKind::Press {
1809 continue;
1810 }
1811 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1812 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
1813 let mut modal_local =
1814 self.focus_index.saturating_sub(self.prev_modal_focus_start);
1815 modal_local %= self.prev_modal_focus_count;
1816 let next = (modal_local + 1) % self.prev_modal_focus_count;
1817 self.focus_index = self.prev_modal_focus_start + next;
1818 } else if self.prev_focus_count > 0 {
1819 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1820 }
1821 self.consumed[i] = true;
1822 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1823 || key.code == KeyCode::BackTab
1824 {
1825 if self.prev_modal_active && self.prev_modal_focus_count > 0 {
1826 let mut modal_local =
1827 self.focus_index.saturating_sub(self.prev_modal_focus_start);
1828 modal_local %= self.prev_modal_focus_count;
1829 let prev = if modal_local == 0 {
1830 self.prev_modal_focus_count - 1
1831 } else {
1832 modal_local - 1
1833 };
1834 self.focus_index = self.prev_modal_focus_start + prev;
1835 } else if self.prev_focus_count > 0 {
1836 self.focus_index = if self.focus_index == 0 {
1837 self.prev_focus_count - 1
1838 } else {
1839 self.focus_index - 1
1840 };
1841 }
1842 self.consumed[i] = true;
1843 }
1844 }
1845 }
1846 }
1847
1848 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1852 w.ui(self)
1853 }
1854
1855 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1870 self.error_boundary_with(f, |ui, msg| {
1871 ui.styled(
1872 format!("⚠ Error: {msg}"),
1873 Style::new().fg(ui.theme.error).bold(),
1874 );
1875 });
1876 }
1877
1878 pub fn error_boundary_with(
1898 &mut self,
1899 f: impl FnOnce(&mut Context),
1900 fallback: impl FnOnce(&mut Context, String),
1901 ) {
1902 let snapshot = ContextSnapshot::capture(self);
1903
1904 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1905 f(self);
1906 }));
1907
1908 match result {
1909 Ok(()) => {}
1910 Err(panic_info) => {
1911 if self.is_real_terminal {
1912 #[cfg(feature = "crossterm")]
1913 {
1914 let _ = crossterm::terminal::enable_raw_mode();
1915 let _ = crossterm::execute!(
1916 std::io::stdout(),
1917 crossterm::terminal::EnterAlternateScreen
1918 );
1919 }
1920
1921 #[cfg(not(feature = "crossterm"))]
1922 {}
1923 }
1924
1925 snapshot.restore(self);
1926
1927 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1928 (*s).to_string()
1929 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1930 s.clone()
1931 } else {
1932 "widget panicked".to_string()
1933 };
1934
1935 fallback(self, msg);
1936 }
1937 }
1938 }
1939
1940 pub(crate) fn next_interaction_id(&mut self) -> usize {
1942 let id = self.interaction_count;
1943 self.interaction_count += 1;
1944 self.commands.push(Command::InteractionMarker(id));
1945 id
1946 }
1947
1948 pub fn interaction(&mut self) -> Response {
1954 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1955 return Response::none();
1956 }
1957 let id = self.interaction_count;
1958 self.interaction_count += 1;
1959 self.response_for(id)
1960 }
1961
1962 pub fn register_focusable(&mut self) -> bool {
1967 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1968 return false;
1969 }
1970 let id = self.focus_count;
1971 self.focus_count += 1;
1972 self.commands.push(Command::FocusMarker(id));
1973 if self.prev_modal_active
1974 && self.prev_modal_focus_count > 0
1975 && self.modal_active
1976 && self.overlay_depth > 0
1977 {
1978 let mut modal_local_id = id.saturating_sub(self.modal_focus_start);
1979 modal_local_id %= self.prev_modal_focus_count;
1980 let mut modal_focus_idx = self.focus_index.saturating_sub(self.prev_modal_focus_start);
1981 modal_focus_idx %= self.prev_modal_focus_count;
1982 return modal_local_id == modal_focus_idx;
1983 }
1984 if self.prev_focus_count == 0 {
1985 return true;
1986 }
1987 self.focus_index % self.prev_focus_count == id
1988 }
1989
1990 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
2008 let idx = self.hook_cursor;
2009 self.hook_cursor += 1;
2010
2011 if idx >= self.hook_states.len() {
2012 self.hook_states.push(Box::new(init()));
2013 }
2014
2015 State {
2016 idx,
2017 _marker: std::marker::PhantomData,
2018 }
2019 }
2020
2021 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
2029 &mut self,
2030 deps: &D,
2031 compute: impl FnOnce(&D) -> T,
2032 ) -> &T {
2033 let idx = self.hook_cursor;
2034 self.hook_cursor += 1;
2035
2036 let should_recompute = if idx >= self.hook_states.len() {
2037 true
2038 } else {
2039 let (stored_deps, _) = self.hook_states[idx]
2040 .downcast_ref::<(D, T)>()
2041 .unwrap_or_else(|| {
2042 panic!(
2043 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
2044 idx,
2045 std::any::type_name::<(D, T)>()
2046 )
2047 });
2048 stored_deps != deps
2049 };
2050
2051 if should_recompute {
2052 let value = compute(deps);
2053 let slot = Box::new((deps.clone(), value));
2054 if idx < self.hook_states.len() {
2055 self.hook_states[idx] = slot;
2056 } else {
2057 self.hook_states.push(slot);
2058 }
2059 }
2060
2061 let (_, value) = self.hook_states[idx]
2062 .downcast_ref::<(D, T)>()
2063 .unwrap_or_else(|| {
2064 panic!(
2065 "Hook type mismatch at index {}: expected {}. Hooks must be called in the same order every frame.",
2066 idx,
2067 std::any::type_name::<(D, T)>()
2068 )
2069 });
2070 value
2071 }
2072
2073 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
2075 if self.theme.is_dark {
2076 dark
2077 } else {
2078 light
2079 }
2080 }
2081
2082 pub fn notify(&mut self, message: &str, level: ToastLevel) {
2092 let tick = self.tick;
2093 self.notification_queue
2094 .push((message.to_string(), level, tick));
2095 }
2096
2097 pub(crate) fn render_notifications(&mut self) {
2098 self.notification_queue
2099 .retain(|(_, _, created)| self.tick.saturating_sub(*created) < 180);
2100 if self.notification_queue.is_empty() {
2101 return;
2102 }
2103
2104 let items: Vec<(String, Color)> = self
2105 .notification_queue
2106 .iter()
2107 .rev()
2108 .map(|(message, level, _)| {
2109 let color = match level {
2110 ToastLevel::Info => self.theme.primary,
2111 ToastLevel::Success => self.theme.success,
2112 ToastLevel::Warning => self.theme.warning,
2113 ToastLevel::Error => self.theme.error,
2114 };
2115 (message.clone(), color)
2116 })
2117 .collect();
2118
2119 let _ = self.overlay(|ui| {
2120 let _ = ui.row(|ui| {
2121 ui.spacer();
2122 let _ = ui.col(|ui| {
2123 for (message, color) in &items {
2124 let mut line = String::with_capacity(2 + message.len());
2125 line.push_str("● ");
2126 line.push_str(message);
2127 ui.styled(line, Style::new().fg(*color));
2128 }
2129 });
2130 });
2131 });
2132 }
2133}
2134
2135mod widgets_display;
2136mod widgets_input;
2137mod widgets_interactive;
2138mod widgets_viz;
2139
2140#[inline]
2141fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2142 if char_index == 0 {
2143 return 0;
2144 }
2145 value
2146 .char_indices()
2147 .nth(char_index)
2148 .map_or(value.len(), |(idx, _)| idx)
2149}
2150
2151fn format_token_count(count: usize) -> String {
2152 if count >= 1_000_000 {
2153 format!("{:.1}M", count as f64 / 1_000_000.0)
2154 } else if count >= 1_000 {
2155 format!("{:.1}k", count as f64 / 1_000.0)
2156 } else {
2157 count.to_string()
2158 }
2159}
2160
2161fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2162 let sep_width = UnicodeWidthStr::width(separator);
2163 let total_cells_width: usize = widths.iter().map(|w| *w as usize).sum();
2164 let mut row = String::with_capacity(
2165 total_cells_width + sep_width.saturating_mul(widths.len().saturating_sub(1)),
2166 );
2167 for (i, width) in widths.iter().enumerate() {
2168 if i > 0 {
2169 row.push_str(separator);
2170 }
2171 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2172 let cell_width = UnicodeWidthStr::width(cell) as u32;
2173 let padding = (*width).saturating_sub(cell_width) as usize;
2174 row.push_str(cell);
2175 row.extend(std::iter::repeat(' ').take(padding));
2176 }
2177 row
2178}
2179
2180fn table_visible_len(state: &TableState) -> usize {
2181 if state.page_size == 0 {
2182 return state.visible_indices().len();
2183 }
2184
2185 let start = state
2186 .page
2187 .saturating_mul(state.page_size)
2188 .min(state.visible_indices().len());
2189 let end = (start + state.page_size).min(state.visible_indices().len());
2190 end.saturating_sub(start)
2191}
2192
2193pub(crate) fn handle_vertical_nav(
2194 selected: &mut usize,
2195 max_index: usize,
2196 key_code: KeyCode,
2197) -> bool {
2198 match key_code {
2199 KeyCode::Up | KeyCode::Char('k') => {
2200 if *selected > 0 {
2201 *selected -= 1;
2202 true
2203 } else {
2204 false
2205 }
2206 }
2207 KeyCode::Down | KeyCode::Char('j') => {
2208 if *selected < max_index {
2209 *selected += 1;
2210 true
2211 } else {
2212 false
2213 }
2214 }
2215 _ => false,
2216 }
2217}
2218
2219fn format_compact_number(value: f64) -> String {
2220 if value.fract().abs() < f64::EPSILON {
2221 return format!("{value:.0}");
2222 }
2223
2224 let mut s = format!("{value:.2}");
2225 while s.contains('.') && s.ends_with('0') {
2226 s.pop();
2227 }
2228 if s.ends_with('.') {
2229 s.pop();
2230 }
2231 s
2232}
2233
2234fn center_text(text: &str, width: usize) -> String {
2235 let text_width = UnicodeWidthStr::width(text);
2236 if text_width >= width {
2237 return text.to_string();
2238 }
2239
2240 let total = width - text_width;
2241 let left = total / 2;
2242 let right = total - left;
2243 let mut centered = String::with_capacity(width);
2244 centered.extend(std::iter::repeat(' ').take(left));
2245 centered.push_str(text);
2246 centered.extend(std::iter::repeat(' ').take(right));
2247 centered
2248}
2249
2250struct TextareaVLine {
2251 logical_row: usize,
2252 char_start: usize,
2253 char_count: usize,
2254}
2255
2256fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2257 let mut out = Vec::new();
2258 for (row, line) in lines.iter().enumerate() {
2259 if line.is_empty() || wrap_width == u32::MAX {
2260 out.push(TextareaVLine {
2261 logical_row: row,
2262 char_start: 0,
2263 char_count: line.chars().count(),
2264 });
2265 continue;
2266 }
2267 let mut seg_start = 0usize;
2268 let mut seg_chars = 0usize;
2269 let mut seg_width = 0u32;
2270 for (idx, ch) in line.chars().enumerate() {
2271 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2272 if seg_width + cw > wrap_width && seg_chars > 0 {
2273 out.push(TextareaVLine {
2274 logical_row: row,
2275 char_start: seg_start,
2276 char_count: seg_chars,
2277 });
2278 seg_start = idx;
2279 seg_chars = 0;
2280 seg_width = 0;
2281 }
2282 seg_chars += 1;
2283 seg_width += cw;
2284 }
2285 out.push(TextareaVLine {
2286 logical_row: row,
2287 char_start: seg_start,
2288 char_count: seg_chars,
2289 });
2290 }
2291 out
2292}
2293
2294fn textarea_logical_to_visual(
2295 vlines: &[TextareaVLine],
2296 logical_row: usize,
2297 logical_col: usize,
2298) -> (usize, usize) {
2299 for (i, vl) in vlines.iter().enumerate() {
2300 if vl.logical_row != logical_row {
2301 continue;
2302 }
2303 let seg_end = vl.char_start + vl.char_count;
2304 if logical_col >= vl.char_start && logical_col < seg_end {
2305 return (i, logical_col - vl.char_start);
2306 }
2307 if logical_col == seg_end {
2308 let is_last_seg = vlines
2309 .get(i + 1)
2310 .map_or(true, |next| next.logical_row != logical_row);
2311 if is_last_seg {
2312 return (i, logical_col - vl.char_start);
2313 }
2314 }
2315 }
2316 (vlines.len().saturating_sub(1), 0)
2317}
2318
2319fn textarea_visual_to_logical(
2320 vlines: &[TextareaVLine],
2321 visual_row: usize,
2322 visual_col: usize,
2323) -> (usize, usize) {
2324 if let Some(vl) = vlines.get(visual_row) {
2325 let logical_col = vl.char_start + visual_col.min(vl.char_count);
2326 (vl.logical_row, logical_col)
2327 } else {
2328 (0, 0)
2329 }
2330}
2331
2332#[allow(unused_variables)]
2333fn open_url(url: &str) -> std::io::Result<()> {
2334 #[cfg(target_os = "macos")]
2335 {
2336 std::process::Command::new("open").arg(url).spawn()?;
2337 }
2338 #[cfg(target_os = "linux")]
2339 {
2340 std::process::Command::new("xdg-open").arg(url).spawn()?;
2341 }
2342 #[cfg(target_os = "windows")]
2343 {
2344 std::process::Command::new("cmd")
2345 .args(["/c", "start", "", url])
2346 .spawn()?;
2347 }
2348 Ok(())
2349}
2350
2351#[cfg(test)]
2352mod tests {
2353 use super::*;
2354 use crate::test_utils::TestBackend;
2355 use crate::EventBuilder;
2356
2357 #[test]
2358 fn use_memo_type_mismatch_includes_index_and_expected_type() {
2359 let mut state = FrameState::default();
2360 let mut ctx = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2361 ctx.hook_states.push(Box::new(42u32));
2362 ctx.hook_cursor = 0;
2363
2364 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2365 let deps = 1u8;
2366 let _ = ctx.use_memo(&deps, |_| 7u8);
2367 }))
2368 .expect_err("use_memo should panic on type mismatch");
2369
2370 let message = panic_message(panic);
2371 assert!(
2372 message.contains("Hook type mismatch at index 0"),
2373 "panic message should include hook index, got: {message}"
2374 );
2375 assert!(
2376 message.contains(std::any::type_name::<(u8, u8)>()),
2377 "panic message should include expected type, got: {message}"
2378 );
2379 assert!(
2380 message.contains("Hooks must be called in the same order every frame."),
2381 "panic message should explain hook ordering requirement, got: {message}"
2382 );
2383 }
2384
2385 #[test]
2386 fn light_dark_uses_current_theme_mode() {
2387 let mut dark_backend = TestBackend::new(10, 2);
2388 dark_backend.render(|ui| {
2389 let color = ui.light_dark(Color::Red, Color::Blue);
2390 ui.text("X").fg(color);
2391 });
2392 assert_eq!(dark_backend.buffer().get(0, 0).style.fg, Some(Color::Blue));
2393
2394 let mut light_backend = TestBackend::new(10, 2);
2395 light_backend.render(|ui| {
2396 ui.set_theme(Theme::light());
2397 let color = ui.light_dark(Color::Red, Color::Blue);
2398 ui.text("X").fg(color);
2399 });
2400 assert_eq!(light_backend.buffer().get(0, 0).style.fg, Some(Color::Red));
2401 }
2402
2403 #[test]
2404 fn modal_focus_trap_tabs_only_within_modal_scope() {
2405 let events = EventBuilder::new().key_code(KeyCode::Tab).build();
2406 let mut state = FrameState {
2407 focus_index: 3,
2408 prev_focus_count: 5,
2409 prev_modal_active: true,
2410 prev_modal_focus_start: 3,
2411 prev_modal_focus_count: 2,
2412 ..FrameState::default()
2413 };
2414 let mut ctx = Context::new(events, 40, 10, &mut state, Theme::dark());
2415
2416 ctx.process_focus_keys();
2417 assert_eq!(ctx.focus_index, 4);
2418
2419 let outside = ctx.register_focusable();
2420 let mut first_modal = false;
2421 let mut second_modal = false;
2422 let _ = ctx.modal(|ui| {
2423 first_modal = ui.register_focusable();
2424 second_modal = ui.register_focusable();
2425 });
2426
2427 assert!(!outside, "focus should not be granted outside modal");
2428 assert!(
2429 !first_modal,
2430 "first modal focusable should be unfocused at index 4"
2431 );
2432 assert!(
2433 second_modal,
2434 "second modal focusable should be focused at index 4"
2435 );
2436 }
2437
2438 #[test]
2439 fn modal_focus_trap_shift_tab_wraps_within_modal_scope() {
2440 let events = EventBuilder::new().key_code(KeyCode::BackTab).build();
2441 let mut state = FrameState {
2442 focus_index: 3,
2443 prev_focus_count: 5,
2444 prev_modal_active: true,
2445 prev_modal_focus_start: 3,
2446 prev_modal_focus_count: 2,
2447 ..FrameState::default()
2448 };
2449 let mut ctx = Context::new(events, 40, 10, &mut state, Theme::dark());
2450
2451 ctx.process_focus_keys();
2452 assert_eq!(ctx.focus_index, 4);
2453
2454 let mut first_modal = false;
2455 let mut second_modal = false;
2456 let _ = ctx.modal(|ui| {
2457 first_modal = ui.register_focusable();
2458 second_modal = ui.register_focusable();
2459 });
2460
2461 assert!(!first_modal);
2462 assert!(second_modal);
2463 }
2464
2465 #[test]
2466 fn screen_helper_renders_only_current_screen() {
2467 let mut backend = TestBackend::new(24, 3);
2468 let screens = ScreenState::new("settings");
2469
2470 backend.render(|ui| {
2471 ui.screen("home", &screens, |ui| {
2472 ui.text("Home Screen");
2473 });
2474 ui.screen("settings", &screens, |ui| {
2475 ui.text("Settings Screen");
2476 });
2477 });
2478
2479 let rendered = backend.to_string();
2480 assert!(rendered.contains("Settings Screen"));
2481 assert!(!rendered.contains("Home Screen"));
2482 }
2483
2484 fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
2485 if let Some(s) = panic.downcast_ref::<String>() {
2486 s.clone()
2487 } else if let Some(s) = panic.downcast_ref::<&str>() {
2488 (*s).to_string()
2489 } else {
2490 "<non-string panic payload>".to_string()
2491 }
2492 }
2493}