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