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)]
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)]
90pub struct Response {
91 pub clicked: bool,
93 pub hovered: bool,
95 pub changed: bool,
97 pub focused: bool,
99 pub rect: Rect,
101}
102
103impl Response {
104 pub fn none() -> Self {
106 Self::default()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum BarDirection {
113 Horizontal,
115 Vertical,
117}
118
119#[derive(Debug, Clone)]
121pub struct Bar {
122 pub label: String,
124 pub value: f64,
126 pub color: Option<Color>,
128 pub text_value: Option<String>,
129 pub value_style: Option<Style>,
130}
131
132impl Bar {
133 pub fn new(label: impl Into<String>, value: f64) -> Self {
135 Self {
136 label: label.into(),
137 value,
138 color: None,
139 text_value: None,
140 value_style: None,
141 }
142 }
143
144 pub fn color(mut self, color: Color) -> Self {
146 self.color = Some(color);
147 self
148 }
149
150 pub fn text_value(mut self, text: impl Into<String>) -> Self {
151 self.text_value = Some(text.into());
152 self
153 }
154
155 pub fn value_style(mut self, style: Style) -> Self {
156 self.value_style = Some(style);
157 self
158 }
159}
160
161#[derive(Debug, Clone, Copy)]
162pub struct BarChartConfig {
163 pub direction: BarDirection,
164 pub bar_width: u16,
165 pub bar_gap: u16,
166 pub group_gap: u16,
167 pub max_value: Option<f64>,
168}
169
170impl Default for BarChartConfig {
171 fn default() -> Self {
172 Self {
173 direction: BarDirection::Horizontal,
174 bar_width: 1,
175 bar_gap: 0,
176 group_gap: 2,
177 max_value: None,
178 }
179 }
180}
181
182impl BarChartConfig {
183 pub fn direction(&mut self, direction: BarDirection) -> &mut Self {
184 self.direction = direction;
185 self
186 }
187
188 pub fn bar_width(&mut self, bar_width: u16) -> &mut Self {
189 self.bar_width = bar_width.max(1);
190 self
191 }
192
193 pub fn bar_gap(&mut self, bar_gap: u16) -> &mut Self {
194 self.bar_gap = bar_gap;
195 self
196 }
197
198 pub fn group_gap(&mut self, group_gap: u16) -> &mut Self {
199 self.group_gap = group_gap;
200 self
201 }
202
203 pub fn max_value(&mut self, max_value: f64) -> &mut Self {
204 self.max_value = Some(max_value);
205 self
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct BarGroup {
212 pub label: String,
214 pub bars: Vec<Bar>,
216}
217
218impl BarGroup {
219 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
221 Self {
222 label: label.into(),
223 bars,
224 }
225 }
226}
227
228pub trait Widget {
290 type Response;
293
294 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
300}
301
302pub struct Context {
318 pub(crate) commands: Vec<Command>,
320 pub(crate) events: Vec<Event>,
321 pub(crate) consumed: Vec<bool>,
322 pub(crate) should_quit: bool,
323 pub(crate) area_width: u32,
324 pub(crate) area_height: u32,
325 pub(crate) tick: u64,
326 pub(crate) focus_index: usize,
327 pub(crate) focus_count: usize,
328 pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
329 pub(crate) hook_cursor: usize,
330 prev_focus_count: usize,
331 scroll_count: usize,
332 prev_scroll_infos: Vec<(u32, u32)>,
333 prev_scroll_rects: Vec<Rect>,
334 interaction_count: usize,
335 pub(crate) prev_hit_map: Vec<Rect>,
336 pub(crate) group_stack: Vec<String>,
337 pub(crate) prev_group_rects: Vec<(String, Rect)>,
338 group_count: usize,
339 prev_focus_groups: Vec<Option<String>>,
340 _prev_focus_rects: Vec<(usize, Rect)>,
341 mouse_pos: Option<(u32, u32)>,
342 click_pos: Option<(u32, u32)>,
343 last_text_idx: Option<usize>,
344 overlay_depth: usize,
345 pub(crate) modal_active: bool,
346 prev_modal_active: bool,
347 pub(crate) clipboard_text: Option<String>,
348 debug: bool,
349 theme: Theme,
350 pub(crate) dark_mode: bool,
351 pub(crate) is_real_terminal: bool,
352 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
353 pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
354}
355
356type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
357
358struct ContextSnapshot {
359 cmd_count: usize,
360 last_text_idx: Option<usize>,
361 focus_count: usize,
362 interaction_count: usize,
363 scroll_count: usize,
364 group_count: usize,
365 group_stack_len: usize,
366 overlay_depth: usize,
367 modal_active: bool,
368 hook_cursor: usize,
369 hook_states_len: usize,
370 dark_mode: bool,
371 deferred_draws_len: usize,
372 notification_queue_len: usize,
373}
374
375impl ContextSnapshot {
376 fn capture(ctx: &Context) -> Self {
377 Self {
378 cmd_count: ctx.commands.len(),
379 last_text_idx: ctx.last_text_idx,
380 focus_count: ctx.focus_count,
381 interaction_count: ctx.interaction_count,
382 scroll_count: ctx.scroll_count,
383 group_count: ctx.group_count,
384 group_stack_len: ctx.group_stack.len(),
385 overlay_depth: ctx.overlay_depth,
386 modal_active: ctx.modal_active,
387 hook_cursor: ctx.hook_cursor,
388 hook_states_len: ctx.hook_states.len(),
389 dark_mode: ctx.dark_mode,
390 deferred_draws_len: ctx.deferred_draws.len(),
391 notification_queue_len: ctx.notification_queue.len(),
392 }
393 }
394
395 fn restore(&self, ctx: &mut Context) {
396 ctx.commands.truncate(self.cmd_count);
397 ctx.last_text_idx = self.last_text_idx;
398 ctx.focus_count = self.focus_count;
399 ctx.interaction_count = self.interaction_count;
400 ctx.scroll_count = self.scroll_count;
401 ctx.group_count = self.group_count;
402 ctx.group_stack.truncate(self.group_stack_len);
403 ctx.overlay_depth = self.overlay_depth;
404 ctx.modal_active = self.modal_active;
405 ctx.hook_cursor = self.hook_cursor;
406 ctx.hook_states.truncate(self.hook_states_len);
407 ctx.dark_mode = self.dark_mode;
408 ctx.deferred_draws.truncate(self.deferred_draws_len);
409 ctx.notification_queue.truncate(self.notification_queue_len);
410 }
411}
412
413#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
434pub struct ContainerBuilder<'a> {
435 ctx: &'a mut Context,
436 gap: u32,
437 align: Align,
438 justify: Justify,
439 border: Option<Border>,
440 border_sides: BorderSides,
441 border_style: Style,
442 bg: Option<Color>,
443 dark_bg: Option<Color>,
444 dark_border_style: Option<Style>,
445 group_hover_bg: Option<Color>,
446 group_hover_border_style: Option<Style>,
447 group_name: Option<String>,
448 padding: Padding,
449 margin: Margin,
450 constraints: Constraints,
451 title: Option<(String, Style)>,
452 grow: u16,
453 scroll_offset: Option<u32>,
454}
455
456#[derive(Debug, Clone, Copy)]
463struct CanvasPixel {
464 bits: u32,
465 color: Color,
466}
467
468#[derive(Debug, Clone)]
470struct CanvasLabel {
471 x: usize,
472 y: usize,
473 text: String,
474 color: Color,
475}
476
477#[derive(Debug, Clone)]
479struct CanvasLayer {
480 grid: Vec<Vec<CanvasPixel>>,
481 labels: Vec<CanvasLabel>,
482}
483
484pub struct CanvasContext {
485 layers: Vec<CanvasLayer>,
486 cols: usize,
487 rows: usize,
488 px_w: usize,
489 px_h: usize,
490 current_color: Color,
491}
492
493impl CanvasContext {
494 fn new(cols: usize, rows: usize) -> Self {
495 Self {
496 layers: vec![Self::new_layer(cols, rows)],
497 cols,
498 rows,
499 px_w: cols * 2,
500 px_h: rows * 4,
501 current_color: Color::Reset,
502 }
503 }
504
505 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
506 CanvasLayer {
507 grid: vec![
508 vec![
509 CanvasPixel {
510 bits: 0,
511 color: Color::Reset,
512 };
513 cols
514 ];
515 rows
516 ],
517 labels: Vec::new(),
518 }
519 }
520
521 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
522 self.layers.last_mut()
523 }
524
525 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
526 if x >= self.px_w || y >= self.px_h {
527 return;
528 }
529
530 let char_col = x / 2;
531 let char_row = y / 4;
532 let sub_col = x % 2;
533 let sub_row = y % 4;
534 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
535 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
536
537 let bit = if sub_col == 0 {
538 LEFT_BITS[sub_row]
539 } else {
540 RIGHT_BITS[sub_row]
541 };
542
543 if let Some(layer) = self.current_layer_mut() {
544 let cell = &mut layer.grid[char_row][char_col];
545 let new_bits = cell.bits | bit;
546 if new_bits != cell.bits {
547 cell.bits = new_bits;
548 cell.color = color;
549 }
550 }
551 }
552
553 fn dot_isize(&mut self, x: isize, y: isize) {
554 if x >= 0 && y >= 0 {
555 self.dot(x as usize, y as usize);
556 }
557 }
558
559 pub fn width(&self) -> usize {
561 self.px_w
562 }
563
564 pub fn height(&self) -> usize {
566 self.px_h
567 }
568
569 pub fn dot(&mut self, x: usize, y: usize) {
571 self.dot_with_color(x, y, self.current_color);
572 }
573
574 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
576 let (mut x, mut y) = (x0 as isize, y0 as isize);
577 let (x1, y1) = (x1 as isize, y1 as isize);
578 let dx = (x1 - x).abs();
579 let dy = -(y1 - y).abs();
580 let sx = if x < x1 { 1 } else { -1 };
581 let sy = if y < y1 { 1 } else { -1 };
582 let mut err = dx + dy;
583
584 loop {
585 self.dot_isize(x, y);
586 if x == x1 && y == y1 {
587 break;
588 }
589 let e2 = 2 * err;
590 if e2 >= dy {
591 err += dy;
592 x += sx;
593 }
594 if e2 <= dx {
595 err += dx;
596 y += sy;
597 }
598 }
599 }
600
601 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
603 if w == 0 || h == 0 {
604 return;
605 }
606
607 self.line(x, y, x + w.saturating_sub(1), y);
608 self.line(
609 x + w.saturating_sub(1),
610 y,
611 x + w.saturating_sub(1),
612 y + h.saturating_sub(1),
613 );
614 self.line(
615 x + w.saturating_sub(1),
616 y + h.saturating_sub(1),
617 x,
618 y + h.saturating_sub(1),
619 );
620 self.line(x, y + h.saturating_sub(1), x, y);
621 }
622
623 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
625 let mut x = r as isize;
626 let mut y: isize = 0;
627 let mut err: isize = 1 - x;
628 let (cx, cy) = (cx as isize, cy as isize);
629
630 while x >= y {
631 for &(dx, dy) in &[
632 (x, y),
633 (y, x),
634 (-x, y),
635 (-y, x),
636 (x, -y),
637 (y, -x),
638 (-x, -y),
639 (-y, -x),
640 ] {
641 let px = cx + dx;
642 let py = cy + dy;
643 self.dot_isize(px, py);
644 }
645
646 y += 1;
647 if err < 0 {
648 err += 2 * y + 1;
649 } else {
650 x -= 1;
651 err += 2 * (y - x) + 1;
652 }
653 }
654 }
655
656 pub fn set_color(&mut self, color: Color) {
658 self.current_color = color;
659 }
660
661 pub fn color(&self) -> Color {
663 self.current_color
664 }
665
666 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
668 if w == 0 || h == 0 {
669 return;
670 }
671
672 let x_end = x.saturating_add(w).min(self.px_w);
673 let y_end = y.saturating_add(h).min(self.px_h);
674 if x >= x_end || y >= y_end {
675 return;
676 }
677
678 for yy in y..y_end {
679 self.line(x, yy, x_end.saturating_sub(1), yy);
680 }
681 }
682
683 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
685 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
686 for y in (cy - r)..=(cy + r) {
687 let dy = y - cy;
688 let span_sq = (r * r - dy * dy).max(0);
689 let dx = (span_sq as f64).sqrt() as isize;
690 for x in (cx - dx)..=(cx + dx) {
691 self.dot_isize(x, y);
692 }
693 }
694 }
695
696 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
698 self.line(x0, y0, x1, y1);
699 self.line(x1, y1, x2, y2);
700 self.line(x2, y2, x0, y0);
701 }
702
703 pub fn filled_triangle(
705 &mut self,
706 x0: usize,
707 y0: usize,
708 x1: usize,
709 y1: usize,
710 x2: usize,
711 y2: usize,
712 ) {
713 let vertices = [
714 (x0 as isize, y0 as isize),
715 (x1 as isize, y1 as isize),
716 (x2 as isize, y2 as isize),
717 ];
718 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
719 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
720
721 for y in min_y..=max_y {
722 let mut intersections: Vec<f64> = Vec::new();
723
724 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
725 let (x_a, y_a) = vertices[edge.0];
726 let (x_b, y_b) = vertices[edge.1];
727 if y_a == y_b {
728 continue;
729 }
730
731 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
732 (x_a, y_a, x_b, y_b)
733 } else {
734 (x_b, y_b, x_a, y_a)
735 };
736
737 if y < y_start || y >= y_end {
738 continue;
739 }
740
741 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
742 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
743 }
744
745 intersections.sort_by(|a, b| a.total_cmp(b));
746 let mut i = 0usize;
747 while i + 1 < intersections.len() {
748 let x_start = intersections[i].ceil() as isize;
749 let x_end = intersections[i + 1].floor() as isize;
750 for x in x_start..=x_end {
751 self.dot_isize(x, y);
752 }
753 i += 2;
754 }
755 }
756
757 self.triangle(x0, y0, x1, y1, x2, y2);
758 }
759
760 pub fn points(&mut self, pts: &[(usize, usize)]) {
762 for &(x, y) in pts {
763 self.dot(x, y);
764 }
765 }
766
767 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
769 for window in pts.windows(2) {
770 if let [(x0, y0), (x1, y1)] = window {
771 self.line(*x0, *y0, *x1, *y1);
772 }
773 }
774 }
775
776 pub fn print(&mut self, x: usize, y: usize, text: &str) {
779 if text.is_empty() {
780 return;
781 }
782
783 let color = self.current_color;
784 if let Some(layer) = self.current_layer_mut() {
785 layer.labels.push(CanvasLabel {
786 x,
787 y,
788 text: text.to_string(),
789 color,
790 });
791 }
792 }
793
794 pub fn layer(&mut self) {
796 self.layers.push(Self::new_layer(self.cols, self.rows));
797 }
798
799 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
800 let mut final_grid = vec![
801 vec![
802 CanvasPixel {
803 bits: 0,
804 color: Color::Reset,
805 };
806 self.cols
807 ];
808 self.rows
809 ];
810 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
811 vec![vec![None; self.cols]; self.rows];
812
813 for layer in &self.layers {
814 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
815 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
816 let src = layer.grid[row][col];
817 if src.bits == 0 {
818 continue;
819 }
820
821 let merged = dst.bits | src.bits;
822 if merged != dst.bits {
823 dst.bits = merged;
824 dst.color = src.color;
825 }
826 }
827 }
828
829 for label in &layer.labels {
830 let row = label.y / 4;
831 if row >= self.rows {
832 continue;
833 }
834 let start_col = label.x / 2;
835 for (offset, ch) in label.text.chars().enumerate() {
836 let col = start_col + offset;
837 if col >= self.cols {
838 break;
839 }
840 labels_overlay[row][col] = Some((ch, label.color));
841 }
842 }
843 }
844
845 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
846 for row in 0..self.rows {
847 let mut segments: Vec<(String, Color)> = Vec::new();
848 let mut current_color: Option<Color> = None;
849 let mut current_text = String::new();
850
851 for col in 0..self.cols {
852 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
853 (label_ch, label_color)
854 } else {
855 let bits = final_grid[row][col].bits;
856 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
857 (ch, final_grid[row][col].color)
858 };
859
860 match current_color {
861 Some(c) if c == color => {
862 current_text.push(ch);
863 }
864 Some(c) => {
865 segments.push((std::mem::take(&mut current_text), c));
866 current_text.push(ch);
867 current_color = Some(color);
868 }
869 None => {
870 current_text.push(ch);
871 current_color = Some(color);
872 }
873 }
874 }
875
876 if let Some(color) = current_color {
877 segments.push((current_text, color));
878 }
879 lines.push(segments);
880 }
881
882 lines
883 }
884}
885
886impl<'a> ContainerBuilder<'a> {
887 pub fn apply(mut self, style: &ContainerStyle) -> Self {
892 if let Some(v) = style.border {
893 self.border = Some(v);
894 }
895 if let Some(v) = style.border_sides {
896 self.border_sides = v;
897 }
898 if let Some(v) = style.border_style {
899 self.border_style = v;
900 }
901 if let Some(v) = style.bg {
902 self.bg = Some(v);
903 }
904 if let Some(v) = style.dark_bg {
905 self.dark_bg = Some(v);
906 }
907 if let Some(v) = style.dark_border_style {
908 self.dark_border_style = Some(v);
909 }
910 if let Some(v) = style.padding {
911 self.padding = v;
912 }
913 if let Some(v) = style.margin {
914 self.margin = v;
915 }
916 if let Some(v) = style.gap {
917 self.gap = v;
918 }
919 if let Some(v) = style.grow {
920 self.grow = v;
921 }
922 if let Some(v) = style.align {
923 self.align = v;
924 }
925 if let Some(v) = style.justify {
926 self.justify = v;
927 }
928 if let Some(w) = style.w {
929 self.constraints.min_width = Some(w);
930 self.constraints.max_width = Some(w);
931 }
932 if let Some(h) = style.h {
933 self.constraints.min_height = Some(h);
934 self.constraints.max_height = Some(h);
935 }
936 if let Some(v) = style.min_w {
937 self.constraints.min_width = Some(v);
938 }
939 if let Some(v) = style.max_w {
940 self.constraints.max_width = Some(v);
941 }
942 if let Some(v) = style.min_h {
943 self.constraints.min_height = Some(v);
944 }
945 if let Some(v) = style.max_h {
946 self.constraints.max_height = Some(v);
947 }
948 if let Some(v) = style.w_pct {
949 self.constraints.width_pct = Some(v);
950 }
951 if let Some(v) = style.h_pct {
952 self.constraints.height_pct = Some(v);
953 }
954 self
955 }
956
957 pub fn border(mut self, border: Border) -> Self {
959 self.border = Some(border);
960 self
961 }
962
963 pub fn border_top(mut self, show: bool) -> Self {
965 self.border_sides.top = show;
966 self
967 }
968
969 pub fn border_right(mut self, show: bool) -> Self {
971 self.border_sides.right = show;
972 self
973 }
974
975 pub fn border_bottom(mut self, show: bool) -> Self {
977 self.border_sides.bottom = show;
978 self
979 }
980
981 pub fn border_left(mut self, show: bool) -> Self {
983 self.border_sides.left = show;
984 self
985 }
986
987 pub fn border_sides(mut self, sides: BorderSides) -> Self {
989 self.border_sides = sides;
990 self
991 }
992
993 pub fn rounded(self) -> Self {
995 self.border(Border::Rounded)
996 }
997
998 pub fn border_style(mut self, style: Style) -> Self {
1000 self.border_style = style;
1001 self
1002 }
1003
1004 pub fn border_fg(mut self, color: Color) -> Self {
1006 self.border_style = self.border_style.fg(color);
1007 self
1008 }
1009
1010 pub fn dark_border_style(mut self, style: Style) -> Self {
1012 self.dark_border_style = Some(style);
1013 self
1014 }
1015
1016 pub fn bg(mut self, color: Color) -> Self {
1017 self.bg = Some(color);
1018 self
1019 }
1020
1021 pub fn dark_bg(mut self, color: Color) -> Self {
1023 self.dark_bg = Some(color);
1024 self
1025 }
1026
1027 pub fn group_hover_bg(mut self, color: Color) -> Self {
1029 self.group_hover_bg = Some(color);
1030 self
1031 }
1032
1033 pub fn group_hover_border_style(mut self, style: Style) -> Self {
1035 self.group_hover_border_style = Some(style);
1036 self
1037 }
1038
1039 pub fn p(self, value: u32) -> Self {
1043 self.pad(value)
1044 }
1045
1046 pub fn pad(mut self, value: u32) -> Self {
1048 self.padding = Padding::all(value);
1049 self
1050 }
1051
1052 pub fn px(mut self, value: u32) -> Self {
1054 self.padding.left = value;
1055 self.padding.right = value;
1056 self
1057 }
1058
1059 pub fn py(mut self, value: u32) -> Self {
1061 self.padding.top = value;
1062 self.padding.bottom = value;
1063 self
1064 }
1065
1066 pub fn pt(mut self, value: u32) -> Self {
1068 self.padding.top = value;
1069 self
1070 }
1071
1072 pub fn pr(mut self, value: u32) -> Self {
1074 self.padding.right = value;
1075 self
1076 }
1077
1078 pub fn pb(mut self, value: u32) -> Self {
1080 self.padding.bottom = value;
1081 self
1082 }
1083
1084 pub fn pl(mut self, value: u32) -> Self {
1086 self.padding.left = value;
1087 self
1088 }
1089
1090 pub fn padding(mut self, padding: Padding) -> Self {
1092 self.padding = padding;
1093 self
1094 }
1095
1096 pub fn m(mut self, value: u32) -> Self {
1100 self.margin = Margin::all(value);
1101 self
1102 }
1103
1104 pub fn mx(mut self, value: u32) -> Self {
1106 self.margin.left = value;
1107 self.margin.right = value;
1108 self
1109 }
1110
1111 pub fn my(mut self, value: u32) -> Self {
1113 self.margin.top = value;
1114 self.margin.bottom = value;
1115 self
1116 }
1117
1118 pub fn mt(mut self, value: u32) -> Self {
1120 self.margin.top = value;
1121 self
1122 }
1123
1124 pub fn mr(mut self, value: u32) -> Self {
1126 self.margin.right = value;
1127 self
1128 }
1129
1130 pub fn mb(mut self, value: u32) -> Self {
1132 self.margin.bottom = value;
1133 self
1134 }
1135
1136 pub fn ml(mut self, value: u32) -> Self {
1138 self.margin.left = value;
1139 self
1140 }
1141
1142 pub fn margin(mut self, margin: Margin) -> Self {
1144 self.margin = margin;
1145 self
1146 }
1147
1148 pub fn w(mut self, value: u32) -> Self {
1152 self.constraints.min_width = Some(value);
1153 self.constraints.max_width = Some(value);
1154 self
1155 }
1156
1157 pub fn xs_w(self, value: u32) -> Self {
1164 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1165 if is_xs {
1166 self.w(value)
1167 } else {
1168 self
1169 }
1170 }
1171
1172 pub fn sm_w(self, value: u32) -> Self {
1174 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1175 if is_sm {
1176 self.w(value)
1177 } else {
1178 self
1179 }
1180 }
1181
1182 pub fn md_w(self, value: u32) -> Self {
1184 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1185 if is_md {
1186 self.w(value)
1187 } else {
1188 self
1189 }
1190 }
1191
1192 pub fn lg_w(self, value: u32) -> Self {
1194 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1195 if is_lg {
1196 self.w(value)
1197 } else {
1198 self
1199 }
1200 }
1201
1202 pub fn xl_w(self, value: u32) -> Self {
1204 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1205 if is_xl {
1206 self.w(value)
1207 } else {
1208 self
1209 }
1210 }
1211 pub fn w_at(self, bp: Breakpoint, value: u32) -> Self {
1212 if self.ctx.breakpoint() == bp {
1213 self.w(value)
1214 } else {
1215 self
1216 }
1217 }
1218
1219 pub fn h(mut self, value: u32) -> Self {
1221 self.constraints.min_height = Some(value);
1222 self.constraints.max_height = Some(value);
1223 self
1224 }
1225
1226 pub fn xs_h(self, value: u32) -> Self {
1228 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1229 if is_xs {
1230 self.h(value)
1231 } else {
1232 self
1233 }
1234 }
1235
1236 pub fn sm_h(self, value: u32) -> Self {
1238 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1239 if is_sm {
1240 self.h(value)
1241 } else {
1242 self
1243 }
1244 }
1245
1246 pub fn md_h(self, value: u32) -> Self {
1248 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1249 if is_md {
1250 self.h(value)
1251 } else {
1252 self
1253 }
1254 }
1255
1256 pub fn lg_h(self, value: u32) -> Self {
1258 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1259 if is_lg {
1260 self.h(value)
1261 } else {
1262 self
1263 }
1264 }
1265
1266 pub fn xl_h(self, value: u32) -> Self {
1268 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1269 if is_xl {
1270 self.h(value)
1271 } else {
1272 self
1273 }
1274 }
1275 pub fn h_at(self, bp: Breakpoint, value: u32) -> Self {
1276 if self.ctx.breakpoint() == bp {
1277 self.h(value)
1278 } else {
1279 self
1280 }
1281 }
1282
1283 pub fn min_w(mut self, value: u32) -> Self {
1285 self.constraints.min_width = Some(value);
1286 self
1287 }
1288
1289 pub fn xs_min_w(self, value: u32) -> Self {
1291 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1292 if is_xs {
1293 self.min_w(value)
1294 } else {
1295 self
1296 }
1297 }
1298
1299 pub fn sm_min_w(self, value: u32) -> Self {
1301 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1302 if is_sm {
1303 self.min_w(value)
1304 } else {
1305 self
1306 }
1307 }
1308
1309 pub fn md_min_w(self, value: u32) -> Self {
1311 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1312 if is_md {
1313 self.min_w(value)
1314 } else {
1315 self
1316 }
1317 }
1318
1319 pub fn lg_min_w(self, value: u32) -> Self {
1321 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1322 if is_lg {
1323 self.min_w(value)
1324 } else {
1325 self
1326 }
1327 }
1328
1329 pub fn xl_min_w(self, value: u32) -> Self {
1331 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1332 if is_xl {
1333 self.min_w(value)
1334 } else {
1335 self
1336 }
1337 }
1338 pub fn min_w_at(self, bp: Breakpoint, value: u32) -> Self {
1339 if self.ctx.breakpoint() == bp {
1340 self.min_w(value)
1341 } else {
1342 self
1343 }
1344 }
1345
1346 pub fn max_w(mut self, value: u32) -> Self {
1348 self.constraints.max_width = Some(value);
1349 self
1350 }
1351
1352 pub fn xs_max_w(self, value: u32) -> Self {
1354 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1355 if is_xs {
1356 self.max_w(value)
1357 } else {
1358 self
1359 }
1360 }
1361
1362 pub fn sm_max_w(self, value: u32) -> Self {
1364 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1365 if is_sm {
1366 self.max_w(value)
1367 } else {
1368 self
1369 }
1370 }
1371
1372 pub fn md_max_w(self, value: u32) -> Self {
1374 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1375 if is_md {
1376 self.max_w(value)
1377 } else {
1378 self
1379 }
1380 }
1381
1382 pub fn lg_max_w(self, value: u32) -> Self {
1384 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1385 if is_lg {
1386 self.max_w(value)
1387 } else {
1388 self
1389 }
1390 }
1391
1392 pub fn xl_max_w(self, value: u32) -> Self {
1394 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1395 if is_xl {
1396 self.max_w(value)
1397 } else {
1398 self
1399 }
1400 }
1401 pub fn max_w_at(self, bp: Breakpoint, value: u32) -> Self {
1402 if self.ctx.breakpoint() == bp {
1403 self.max_w(value)
1404 } else {
1405 self
1406 }
1407 }
1408
1409 pub fn min_h(mut self, value: u32) -> Self {
1411 self.constraints.min_height = Some(value);
1412 self
1413 }
1414
1415 pub fn max_h(mut self, value: u32) -> Self {
1417 self.constraints.max_height = Some(value);
1418 self
1419 }
1420
1421 pub fn min_width(mut self, value: u32) -> Self {
1423 self.constraints.min_width = Some(value);
1424 self
1425 }
1426
1427 pub fn max_width(mut self, value: u32) -> Self {
1429 self.constraints.max_width = Some(value);
1430 self
1431 }
1432
1433 pub fn min_height(mut self, value: u32) -> Self {
1435 self.constraints.min_height = Some(value);
1436 self
1437 }
1438
1439 pub fn max_height(mut self, value: u32) -> Self {
1441 self.constraints.max_height = Some(value);
1442 self
1443 }
1444
1445 pub fn w_pct(mut self, pct: u8) -> Self {
1447 self.constraints.width_pct = Some(pct.min(100));
1448 self
1449 }
1450
1451 pub fn h_pct(mut self, pct: u8) -> Self {
1453 self.constraints.height_pct = Some(pct.min(100));
1454 self
1455 }
1456
1457 pub fn constraints(mut self, constraints: Constraints) -> Self {
1459 self.constraints = constraints;
1460 self
1461 }
1462
1463 pub fn gap(mut self, gap: u32) -> Self {
1467 self.gap = gap;
1468 self
1469 }
1470
1471 pub fn xs_gap(self, value: u32) -> Self {
1473 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1474 if is_xs {
1475 self.gap(value)
1476 } else {
1477 self
1478 }
1479 }
1480
1481 pub fn sm_gap(self, value: u32) -> Self {
1483 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1484 if is_sm {
1485 self.gap(value)
1486 } else {
1487 self
1488 }
1489 }
1490
1491 pub fn md_gap(self, value: u32) -> Self {
1498 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1499 if is_md {
1500 self.gap(value)
1501 } else {
1502 self
1503 }
1504 }
1505
1506 pub fn lg_gap(self, value: u32) -> Self {
1508 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1509 if is_lg {
1510 self.gap(value)
1511 } else {
1512 self
1513 }
1514 }
1515
1516 pub fn xl_gap(self, value: u32) -> Self {
1518 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1519 if is_xl {
1520 self.gap(value)
1521 } else {
1522 self
1523 }
1524 }
1525
1526 pub fn gap_at(self, bp: Breakpoint, value: u32) -> Self {
1527 if self.ctx.breakpoint() == bp {
1528 self.gap(value)
1529 } else {
1530 self
1531 }
1532 }
1533
1534 pub fn grow(mut self, grow: u16) -> Self {
1536 self.grow = grow;
1537 self
1538 }
1539
1540 pub fn xs_grow(self, value: u16) -> Self {
1542 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1543 if is_xs {
1544 self.grow(value)
1545 } else {
1546 self
1547 }
1548 }
1549
1550 pub fn sm_grow(self, value: u16) -> Self {
1552 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1553 if is_sm {
1554 self.grow(value)
1555 } else {
1556 self
1557 }
1558 }
1559
1560 pub fn md_grow(self, value: u16) -> Self {
1562 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1563 if is_md {
1564 self.grow(value)
1565 } else {
1566 self
1567 }
1568 }
1569
1570 pub fn lg_grow(self, value: u16) -> Self {
1572 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1573 if is_lg {
1574 self.grow(value)
1575 } else {
1576 self
1577 }
1578 }
1579
1580 pub fn xl_grow(self, value: u16) -> Self {
1582 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1583 if is_xl {
1584 self.grow(value)
1585 } else {
1586 self
1587 }
1588 }
1589 pub fn grow_at(self, bp: Breakpoint, value: u16) -> Self {
1590 if self.ctx.breakpoint() == bp {
1591 self.grow(value)
1592 } else {
1593 self
1594 }
1595 }
1596
1597 pub fn xs_p(self, value: u32) -> Self {
1599 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1600 if is_xs {
1601 self.p(value)
1602 } else {
1603 self
1604 }
1605 }
1606
1607 pub fn sm_p(self, value: u32) -> Self {
1609 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1610 if is_sm {
1611 self.p(value)
1612 } else {
1613 self
1614 }
1615 }
1616
1617 pub fn md_p(self, value: u32) -> Self {
1619 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1620 if is_md {
1621 self.p(value)
1622 } else {
1623 self
1624 }
1625 }
1626
1627 pub fn lg_p(self, value: u32) -> Self {
1629 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1630 if is_lg {
1631 self.p(value)
1632 } else {
1633 self
1634 }
1635 }
1636
1637 pub fn xl_p(self, value: u32) -> Self {
1639 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1640 if is_xl {
1641 self.p(value)
1642 } else {
1643 self
1644 }
1645 }
1646 pub fn p_at(self, bp: Breakpoint, value: u32) -> Self {
1647 if self.ctx.breakpoint() == bp {
1648 self.p(value)
1649 } else {
1650 self
1651 }
1652 }
1653
1654 pub fn align(mut self, align: Align) -> Self {
1658 self.align = align;
1659 self
1660 }
1661
1662 pub fn center(self) -> Self {
1664 self.align(Align::Center)
1665 }
1666
1667 pub fn justify(mut self, justify: Justify) -> Self {
1669 self.justify = justify;
1670 self
1671 }
1672
1673 pub fn space_between(self) -> Self {
1675 self.justify(Justify::SpaceBetween)
1676 }
1677
1678 pub fn space_around(self) -> Self {
1680 self.justify(Justify::SpaceAround)
1681 }
1682
1683 pub fn space_evenly(self) -> Self {
1685 self.justify(Justify::SpaceEvenly)
1686 }
1687
1688 pub fn title(self, title: impl Into<String>) -> Self {
1692 self.title_styled(title, Style::new())
1693 }
1694
1695 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1697 self.title = Some((title.into(), style));
1698 self
1699 }
1700
1701 pub fn scroll_offset(mut self, offset: u32) -> Self {
1705 self.scroll_offset = Some(offset);
1706 self
1707 }
1708
1709 fn group_name(mut self, name: String) -> Self {
1710 self.group_name = Some(name);
1711 self
1712 }
1713
1714 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1719 self.finish(Direction::Column, f)
1720 }
1721
1722 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1727 self.finish(Direction::Row, f)
1728 }
1729
1730 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1735 self.gap = 0;
1736 self.finish(Direction::Row, f)
1737 }
1738
1739 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1754 let draw_id = self.ctx.deferred_draws.len();
1755 self.ctx.deferred_draws.push(Some(Box::new(f)));
1756 self.ctx.interaction_count += 1;
1757 self.ctx.commands.push(Command::RawDraw {
1758 draw_id,
1759 constraints: self.constraints,
1760 grow: self.grow,
1761 margin: self.margin,
1762 });
1763 }
1764
1765 fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1766 let interaction_id = self.ctx.interaction_count;
1767 self.ctx.interaction_count += 1;
1768
1769 let in_hovered_group = self
1770 .group_name
1771 .as_ref()
1772 .map(|name| self.ctx.is_group_hovered(name))
1773 .unwrap_or(false)
1774 || self
1775 .ctx
1776 .group_stack
1777 .last()
1778 .map(|name| self.ctx.is_group_hovered(name))
1779 .unwrap_or(false);
1780 let in_focused_group = self
1781 .group_name
1782 .as_ref()
1783 .map(|name| self.ctx.is_group_focused(name))
1784 .unwrap_or(false)
1785 || self
1786 .ctx
1787 .group_stack
1788 .last()
1789 .map(|name| self.ctx.is_group_focused(name))
1790 .unwrap_or(false);
1791
1792 let resolved_bg = if self.ctx.dark_mode {
1793 self.dark_bg.or(self.bg)
1794 } else {
1795 self.bg
1796 };
1797 let resolved_border_style = if self.ctx.dark_mode {
1798 self.dark_border_style.unwrap_or(self.border_style)
1799 } else {
1800 self.border_style
1801 };
1802 let bg_color = if in_hovered_group || in_focused_group {
1803 self.group_hover_bg.or(resolved_bg)
1804 } else {
1805 resolved_bg
1806 };
1807 let border_style = if in_hovered_group || in_focused_group {
1808 self.group_hover_border_style
1809 .unwrap_or(resolved_border_style)
1810 } else {
1811 resolved_border_style
1812 };
1813 let group_name = self.group_name.take();
1814 let is_group_container = group_name.is_some();
1815
1816 if let Some(scroll_offset) = self.scroll_offset {
1817 self.ctx.commands.push(Command::BeginScrollable {
1818 grow: self.grow,
1819 border: self.border,
1820 border_sides: self.border_sides,
1821 border_style,
1822 padding: self.padding,
1823 margin: self.margin,
1824 constraints: self.constraints,
1825 title: self.title,
1826 scroll_offset,
1827 });
1828 } else {
1829 self.ctx.commands.push(Command::BeginContainer {
1830 direction,
1831 gap: self.gap,
1832 align: self.align,
1833 justify: self.justify,
1834 border: self.border,
1835 border_sides: self.border_sides,
1836 border_style,
1837 bg_color,
1838 padding: self.padding,
1839 margin: self.margin,
1840 constraints: self.constraints,
1841 title: self.title,
1842 grow: self.grow,
1843 group_name,
1844 });
1845 }
1846 f(self.ctx);
1847 self.ctx.commands.push(Command::EndContainer);
1848 self.ctx.last_text_idx = None;
1849
1850 if is_group_container {
1851 self.ctx.group_stack.pop();
1852 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1853 }
1854
1855 self.ctx.response_for(interaction_id)
1856 }
1857}
1858
1859impl Context {
1860 pub(crate) fn new(
1861 events: Vec<Event>,
1862 width: u32,
1863 height: u32,
1864 state: &mut FrameState,
1865 theme: Theme,
1866 ) -> Self {
1867 let consumed = vec![false; events.len()];
1868
1869 let mut mouse_pos = state.last_mouse_pos;
1870 let mut click_pos = None;
1871 for event in &events {
1872 if let Event::Mouse(mouse) = event {
1873 mouse_pos = Some((mouse.x, mouse.y));
1874 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1875 click_pos = Some((mouse.x, mouse.y));
1876 }
1877 }
1878 }
1879
1880 let mut focus_index = state.focus_index;
1881 if let Some((mx, my)) = click_pos {
1882 let mut best: Option<(usize, u64)> = None;
1883 for &(fid, rect) in &state.prev_focus_rects {
1884 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1885 let area = rect.width as u64 * rect.height as u64;
1886 if best.map_or(true, |(_, ba)| area < ba) {
1887 best = Some((fid, area));
1888 }
1889 }
1890 }
1891 if let Some((fid, _)) = best {
1892 focus_index = fid;
1893 }
1894 }
1895
1896 Self {
1897 commands: Vec::new(),
1898 events,
1899 consumed,
1900 should_quit: false,
1901 area_width: width,
1902 area_height: height,
1903 tick: state.tick,
1904 focus_index,
1905 focus_count: 0,
1906 hook_states: std::mem::take(&mut state.hook_states),
1907 hook_cursor: 0,
1908 prev_focus_count: state.prev_focus_count,
1909 scroll_count: 0,
1910 prev_scroll_infos: std::mem::take(&mut state.prev_scroll_infos),
1911 prev_scroll_rects: std::mem::take(&mut state.prev_scroll_rects),
1912 interaction_count: 0,
1913 prev_hit_map: std::mem::take(&mut state.prev_hit_map),
1914 group_stack: Vec::new(),
1915 prev_group_rects: std::mem::take(&mut state.prev_group_rects),
1916 group_count: 0,
1917 prev_focus_groups: std::mem::take(&mut state.prev_focus_groups),
1918 _prev_focus_rects: std::mem::take(&mut state.prev_focus_rects),
1919 mouse_pos,
1920 click_pos,
1921 last_text_idx: None,
1922 overlay_depth: 0,
1923 modal_active: false,
1924 prev_modal_active: state.prev_modal_active,
1925 clipboard_text: None,
1926 debug: state.debug_mode,
1927 theme,
1928 dark_mode: theme.is_dark,
1929 is_real_terminal: false,
1930 deferred_draws: Vec::new(),
1931 notification_queue: std::mem::take(&mut state.notification_queue),
1932 }
1933 }
1934
1935 pub(crate) fn process_focus_keys(&mut self) {
1936 for (i, event) in self.events.iter().enumerate() {
1937 if let Event::Key(key) = event {
1938 if key.kind != KeyEventKind::Press {
1939 continue;
1940 }
1941 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1942 if self.prev_focus_count > 0 {
1943 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1944 }
1945 self.consumed[i] = true;
1946 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1947 || key.code == KeyCode::BackTab
1948 {
1949 if self.prev_focus_count > 0 {
1950 self.focus_index = if self.focus_index == 0 {
1951 self.prev_focus_count - 1
1952 } else {
1953 self.focus_index - 1
1954 };
1955 }
1956 self.consumed[i] = true;
1957 }
1958 }
1959 }
1960 }
1961
1962 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1966 w.ui(self)
1967 }
1968
1969 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1984 self.error_boundary_with(f, |ui, msg| {
1985 ui.styled(
1986 format!("⚠ Error: {msg}"),
1987 Style::new().fg(ui.theme.error).bold(),
1988 );
1989 });
1990 }
1991
1992 pub fn error_boundary_with(
2012 &mut self,
2013 f: impl FnOnce(&mut Context),
2014 fallback: impl FnOnce(&mut Context, String),
2015 ) {
2016 let snapshot = ContextSnapshot::capture(self);
2017
2018 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2019 f(self);
2020 }));
2021
2022 match result {
2023 Ok(()) => {}
2024 Err(panic_info) => {
2025 if self.is_real_terminal {
2026 let _ = crossterm::terminal::enable_raw_mode();
2027 let _ = crossterm::execute!(
2028 std::io::stdout(),
2029 crossterm::terminal::EnterAlternateScreen
2030 );
2031 }
2032
2033 snapshot.restore(self);
2034
2035 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
2036 (*s).to_string()
2037 } else if let Some(s) = panic_info.downcast_ref::<String>() {
2038 s.clone()
2039 } else {
2040 "widget panicked".to_string()
2041 };
2042
2043 fallback(self, msg);
2044 }
2045 }
2046 }
2047
2048 pub fn interaction(&mut self) -> Response {
2054 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2055 return Response::none();
2056 }
2057 let id = self.interaction_count;
2058 self.interaction_count += 1;
2059 self.response_for(id)
2060 }
2061
2062 pub fn register_focusable(&mut self) -> bool {
2067 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2068 return false;
2069 }
2070 let id = self.focus_count;
2071 self.focus_count += 1;
2072 self.commands.push(Command::FocusMarker(id));
2073 if self.prev_focus_count == 0 {
2074 return true;
2075 }
2076 self.focus_index % self.prev_focus_count == id
2077 }
2078
2079 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
2097 let idx = self.hook_cursor;
2098 self.hook_cursor += 1;
2099
2100 if idx >= self.hook_states.len() {
2101 self.hook_states.push(Box::new(init()));
2102 }
2103
2104 State {
2105 idx,
2106 _marker: std::marker::PhantomData,
2107 }
2108 }
2109
2110 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
2118 &mut self,
2119 deps: &D,
2120 compute: impl FnOnce(&D) -> T,
2121 ) -> &T {
2122 let idx = self.hook_cursor;
2123 self.hook_cursor += 1;
2124
2125 let should_recompute = if idx >= self.hook_states.len() {
2126 true
2127 } else {
2128 let (stored_deps, _) = self.hook_states[idx]
2129 .downcast_ref::<(D, T)>()
2130 .unwrap_or_else(|| {
2131 panic!(
2132 "use_memo type mismatch at hook index {} — expected {}",
2133 idx,
2134 std::any::type_name::<D>()
2135 )
2136 });
2137 stored_deps != deps
2138 };
2139
2140 if should_recompute {
2141 let value = compute(deps);
2142 let slot = Box::new((deps.clone(), value));
2143 if idx < self.hook_states.len() {
2144 self.hook_states[idx] = slot;
2145 } else {
2146 self.hook_states.push(slot);
2147 }
2148 }
2149
2150 let (_, value) = self.hook_states[idx]
2151 .downcast_ref::<(D, T)>()
2152 .unwrap_or_else(|| {
2153 panic!(
2154 "use_memo type mismatch at hook index {} — expected {}",
2155 idx,
2156 std::any::type_name::<D>()
2157 )
2158 });
2159 value
2160 }
2161
2162 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
2164 if self.theme.is_dark {
2165 dark
2166 } else {
2167 light
2168 }
2169 }
2170
2171 pub fn notify(&mut self, message: &str, level: ToastLevel) {
2181 let tick = self.tick;
2182 self.notification_queue
2183 .push((message.to_string(), level, tick));
2184 }
2185
2186 pub(crate) fn render_notifications(&mut self) {
2187 self.notification_queue
2188 .retain(|(_, _, created)| self.tick.saturating_sub(*created) < 180);
2189 if self.notification_queue.is_empty() {
2190 return;
2191 }
2192
2193 let items: Vec<(String, Color)> = self
2194 .notification_queue
2195 .iter()
2196 .rev()
2197 .map(|(message, level, _)| {
2198 let color = match level {
2199 ToastLevel::Info => self.theme.primary,
2200 ToastLevel::Success => self.theme.success,
2201 ToastLevel::Warning => self.theme.warning,
2202 ToastLevel::Error => self.theme.error,
2203 };
2204 (message.clone(), color)
2205 })
2206 .collect();
2207
2208 self.overlay(|ui| {
2209 ui.row(|ui| {
2210 ui.spacer();
2211 ui.col(|ui| {
2212 for (message, color) in &items {
2213 ui.styled(format!("● {message}"), Style::new().fg(*color));
2214 }
2215 });
2216 });
2217 });
2218 }
2219}
2220
2221mod widgets_display;
2222mod widgets_input;
2223mod widgets_interactive;
2224mod widgets_viz;
2225
2226#[inline]
2227fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2228 if char_index == 0 {
2229 return 0;
2230 }
2231 value
2232 .char_indices()
2233 .nth(char_index)
2234 .map_or(value.len(), |(idx, _)| idx)
2235}
2236
2237fn format_token_count(count: usize) -> String {
2238 if count >= 1_000_000 {
2239 format!("{:.1}M", count as f64 / 1_000_000.0)
2240 } else if count >= 1_000 {
2241 format!("{:.1}k", count as f64 / 1_000.0)
2242 } else {
2243 format!("{count}")
2244 }
2245}
2246
2247fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2248 let mut parts: Vec<String> = Vec::new();
2249 for (i, width) in widths.iter().enumerate() {
2250 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2251 let cell_width = UnicodeWidthStr::width(cell) as u32;
2252 let padding = (*width).saturating_sub(cell_width) as usize;
2253 parts.push(format!("{cell}{}", " ".repeat(padding)));
2254 }
2255 parts.join(separator)
2256}
2257
2258fn table_visible_len(state: &TableState) -> usize {
2259 if state.page_size == 0 {
2260 return state.visible_indices().len();
2261 }
2262
2263 let start = state
2264 .page
2265 .saturating_mul(state.page_size)
2266 .min(state.visible_indices().len());
2267 let end = (start + state.page_size).min(state.visible_indices().len());
2268 end.saturating_sub(start)
2269}
2270
2271pub(crate) fn handle_vertical_nav(
2272 selected: &mut usize,
2273 max_index: usize,
2274 key_code: KeyCode,
2275) -> bool {
2276 match key_code {
2277 KeyCode::Up | KeyCode::Char('k') => {
2278 if *selected > 0 {
2279 *selected -= 1;
2280 true
2281 } else {
2282 false
2283 }
2284 }
2285 KeyCode::Down | KeyCode::Char('j') => {
2286 if *selected < max_index {
2287 *selected += 1;
2288 true
2289 } else {
2290 false
2291 }
2292 }
2293 _ => false,
2294 }
2295}
2296
2297fn format_compact_number(value: f64) -> String {
2298 if value.fract().abs() < f64::EPSILON {
2299 return format!("{value:.0}");
2300 }
2301
2302 let mut s = format!("{value:.2}");
2303 while s.contains('.') && s.ends_with('0') {
2304 s.pop();
2305 }
2306 if s.ends_with('.') {
2307 s.pop();
2308 }
2309 s
2310}
2311
2312fn center_text(text: &str, width: usize) -> String {
2313 let text_width = UnicodeWidthStr::width(text);
2314 if text_width >= width {
2315 return text.to_string();
2316 }
2317
2318 let total = width - text_width;
2319 let left = total / 2;
2320 let right = total - left;
2321 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2322}
2323
2324struct TextareaVLine {
2325 logical_row: usize,
2326 char_start: usize,
2327 char_count: usize,
2328}
2329
2330fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2331 let mut out = Vec::new();
2332 for (row, line) in lines.iter().enumerate() {
2333 if line.is_empty() || wrap_width == u32::MAX {
2334 out.push(TextareaVLine {
2335 logical_row: row,
2336 char_start: 0,
2337 char_count: line.chars().count(),
2338 });
2339 continue;
2340 }
2341 let mut seg_start = 0usize;
2342 let mut seg_chars = 0usize;
2343 let mut seg_width = 0u32;
2344 for (idx, ch) in line.chars().enumerate() {
2345 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2346 if seg_width + cw > wrap_width && seg_chars > 0 {
2347 out.push(TextareaVLine {
2348 logical_row: row,
2349 char_start: seg_start,
2350 char_count: seg_chars,
2351 });
2352 seg_start = idx;
2353 seg_chars = 0;
2354 seg_width = 0;
2355 }
2356 seg_chars += 1;
2357 seg_width += cw;
2358 }
2359 out.push(TextareaVLine {
2360 logical_row: row,
2361 char_start: seg_start,
2362 char_count: seg_chars,
2363 });
2364 }
2365 out
2366}
2367
2368fn textarea_logical_to_visual(
2369 vlines: &[TextareaVLine],
2370 logical_row: usize,
2371 logical_col: usize,
2372) -> (usize, usize) {
2373 for (i, vl) in vlines.iter().enumerate() {
2374 if vl.logical_row != logical_row {
2375 continue;
2376 }
2377 let seg_end = vl.char_start + vl.char_count;
2378 if logical_col >= vl.char_start && logical_col < seg_end {
2379 return (i, logical_col - vl.char_start);
2380 }
2381 if logical_col == seg_end {
2382 let is_last_seg = vlines
2383 .get(i + 1)
2384 .map_or(true, |next| next.logical_row != logical_row);
2385 if is_last_seg {
2386 return (i, logical_col - vl.char_start);
2387 }
2388 }
2389 }
2390 (vlines.len().saturating_sub(1), 0)
2391}
2392
2393fn textarea_visual_to_logical(
2394 vlines: &[TextareaVLine],
2395 visual_row: usize,
2396 visual_col: usize,
2397) -> (usize, usize) {
2398 if let Some(vl) = vlines.get(visual_row) {
2399 let logical_col = vl.char_start + visual_col.min(vl.char_count);
2400 (vl.logical_row, logical_col)
2401 } else {
2402 (0, 0)
2403 }
2404}
2405
2406fn open_url(url: &str) -> std::io::Result<()> {
2407 #[cfg(target_os = "macos")]
2408 {
2409 std::process::Command::new("open").arg(url).spawn()?;
2410 }
2411 #[cfg(target_os = "linux")]
2412 {
2413 std::process::Command::new("xdg-open").arg(url).spawn()?;
2414 }
2415 #[cfg(target_os = "windows")]
2416 {
2417 std::process::Command::new("cmd")
2418 .args(["/c", "start", "", url])
2419 .spawn()?;
2420 }
2421 Ok(())
2422}
2423
2424#[cfg(test)]
2425mod tests {
2426 use super::*;
2427 use crate::test_utils::TestBackend;
2428
2429 #[test]
2430 fn use_memo_type_mismatch_includes_hook_index_and_expected_type() {
2431 let mut state = FrameState::default();
2432 let mut ctx = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2433 ctx.hook_states.push(Box::new(42u32));
2434 ctx.hook_cursor = 0;
2435
2436 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2437 let deps = 1u8;
2438 let _ = ctx.use_memo(&deps, |_| 7u8);
2439 }))
2440 .expect_err("use_memo should panic on type mismatch");
2441
2442 let message = panic_message(panic);
2443 assert!(
2444 message.contains("use_memo type mismatch at hook index 0"),
2445 "panic message should include hook index, got: {message}"
2446 );
2447 assert!(
2448 message.contains(std::any::type_name::<u8>()),
2449 "panic message should include expected type, got: {message}"
2450 );
2451 }
2452
2453 #[test]
2454 fn light_dark_uses_current_theme_mode() {
2455 let mut dark_backend = TestBackend::new(10, 2);
2456 dark_backend.render(|ui| {
2457 let color = ui.light_dark(Color::Red, Color::Blue);
2458 ui.text("X").fg(color);
2459 });
2460 assert_eq!(dark_backend.buffer().get(0, 0).style.fg, Some(Color::Blue));
2461
2462 let mut light_backend = TestBackend::new(10, 2);
2463 light_backend.render(|ui| {
2464 ui.set_theme(Theme::light());
2465 let color = ui.light_dark(Color::Red, Color::Blue);
2466 ui.text("X").fg(color);
2467 });
2468 assert_eq!(light_backend.buffer().get(0, 0).style.fg, Some(Color::Red));
2469 }
2470
2471 fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
2472 if let Some(s) = panic.downcast_ref::<String>() {
2473 s.clone()
2474 } else if let Some(s) = panic.downcast_ref::<&str>() {
2475 (*s).to_string()
2476 } else {
2477 "<non-string panic payload>".to_string()
2478 }
2479 }
2480}