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 pub(crate) text_color_stack: Vec<Option<Color>>,
355}
356
357type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
358
359struct ContextSnapshot {
360 cmd_count: usize,
361 last_text_idx: Option<usize>,
362 focus_count: usize,
363 interaction_count: usize,
364 scroll_count: usize,
365 group_count: usize,
366 group_stack_len: usize,
367 overlay_depth: usize,
368 modal_active: bool,
369 hook_cursor: usize,
370 hook_states_len: usize,
371 dark_mode: bool,
372 deferred_draws_len: usize,
373 notification_queue_len: usize,
374 text_color_stack_len: usize,
375}
376
377impl ContextSnapshot {
378 fn capture(ctx: &Context) -> Self {
379 Self {
380 cmd_count: ctx.commands.len(),
381 last_text_idx: ctx.last_text_idx,
382 focus_count: ctx.focus_count,
383 interaction_count: ctx.interaction_count,
384 scroll_count: ctx.scroll_count,
385 group_count: ctx.group_count,
386 group_stack_len: ctx.group_stack.len(),
387 overlay_depth: ctx.overlay_depth,
388 modal_active: ctx.modal_active,
389 hook_cursor: ctx.hook_cursor,
390 hook_states_len: ctx.hook_states.len(),
391 dark_mode: ctx.dark_mode,
392 deferred_draws_len: ctx.deferred_draws.len(),
393 notification_queue_len: ctx.notification_queue.len(),
394 text_color_stack_len: ctx.text_color_stack.len(),
395 }
396 }
397
398 fn restore(&self, ctx: &mut Context) {
399 ctx.commands.truncate(self.cmd_count);
400 ctx.last_text_idx = self.last_text_idx;
401 ctx.focus_count = self.focus_count;
402 ctx.interaction_count = self.interaction_count;
403 ctx.scroll_count = self.scroll_count;
404 ctx.group_count = self.group_count;
405 ctx.group_stack.truncate(self.group_stack_len);
406 ctx.overlay_depth = self.overlay_depth;
407 ctx.modal_active = self.modal_active;
408 ctx.hook_cursor = self.hook_cursor;
409 ctx.hook_states.truncate(self.hook_states_len);
410 ctx.dark_mode = self.dark_mode;
411 ctx.deferred_draws.truncate(self.deferred_draws_len);
412 ctx.notification_queue.truncate(self.notification_queue_len);
413 ctx.text_color_stack.truncate(self.text_color_stack_len);
414 }
415}
416
417#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
438pub struct ContainerBuilder<'a> {
439 ctx: &'a mut Context,
440 gap: u32,
441 row_gap: Option<u32>,
442 col_gap: Option<u32>,
443 align: Align,
444 align_self_value: Option<Align>,
445 justify: Justify,
446 border: Option<Border>,
447 border_sides: BorderSides,
448 border_style: Style,
449 bg: Option<Color>,
450 text_color: Option<Color>,
451 dark_bg: Option<Color>,
452 dark_border_style: Option<Style>,
453 group_hover_bg: Option<Color>,
454 group_hover_border_style: Option<Style>,
455 group_name: Option<String>,
456 padding: Padding,
457 margin: Margin,
458 constraints: Constraints,
459 title: Option<(String, Style)>,
460 grow: u16,
461 scroll_offset: Option<u32>,
462}
463
464#[derive(Debug, Clone, Copy)]
471struct CanvasPixel {
472 bits: u32,
473 color: Color,
474}
475
476#[derive(Debug, Clone)]
478struct CanvasLabel {
479 x: usize,
480 y: usize,
481 text: String,
482 color: Color,
483}
484
485#[derive(Debug, Clone)]
487struct CanvasLayer {
488 grid: Vec<Vec<CanvasPixel>>,
489 labels: Vec<CanvasLabel>,
490}
491
492pub struct CanvasContext {
493 layers: Vec<CanvasLayer>,
494 cols: usize,
495 rows: usize,
496 px_w: usize,
497 px_h: usize,
498 current_color: Color,
499}
500
501impl CanvasContext {
502 fn new(cols: usize, rows: usize) -> Self {
503 Self {
504 layers: vec![Self::new_layer(cols, rows)],
505 cols,
506 rows,
507 px_w: cols * 2,
508 px_h: rows * 4,
509 current_color: Color::Reset,
510 }
511 }
512
513 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
514 CanvasLayer {
515 grid: vec![
516 vec![
517 CanvasPixel {
518 bits: 0,
519 color: Color::Reset,
520 };
521 cols
522 ];
523 rows
524 ],
525 labels: Vec::new(),
526 }
527 }
528
529 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
530 self.layers.last_mut()
531 }
532
533 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
534 if x >= self.px_w || y >= self.px_h {
535 return;
536 }
537
538 let char_col = x / 2;
539 let char_row = y / 4;
540 let sub_col = x % 2;
541 let sub_row = y % 4;
542 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
543 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
544
545 let bit = if sub_col == 0 {
546 LEFT_BITS[sub_row]
547 } else {
548 RIGHT_BITS[sub_row]
549 };
550
551 if let Some(layer) = self.current_layer_mut() {
552 let cell = &mut layer.grid[char_row][char_col];
553 let new_bits = cell.bits | bit;
554 if new_bits != cell.bits {
555 cell.bits = new_bits;
556 cell.color = color;
557 }
558 }
559 }
560
561 fn dot_isize(&mut self, x: isize, y: isize) {
562 if x >= 0 && y >= 0 {
563 self.dot(x as usize, y as usize);
564 }
565 }
566
567 pub fn width(&self) -> usize {
569 self.px_w
570 }
571
572 pub fn height(&self) -> usize {
574 self.px_h
575 }
576
577 pub fn dot(&mut self, x: usize, y: usize) {
579 self.dot_with_color(x, y, self.current_color);
580 }
581
582 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
584 let (mut x, mut y) = (x0 as isize, y0 as isize);
585 let (x1, y1) = (x1 as isize, y1 as isize);
586 let dx = (x1 - x).abs();
587 let dy = -(y1 - y).abs();
588 let sx = if x < x1 { 1 } else { -1 };
589 let sy = if y < y1 { 1 } else { -1 };
590 let mut err = dx + dy;
591
592 loop {
593 self.dot_isize(x, y);
594 if x == x1 && y == y1 {
595 break;
596 }
597 let e2 = 2 * err;
598 if e2 >= dy {
599 err += dy;
600 x += sx;
601 }
602 if e2 <= dx {
603 err += dx;
604 y += sy;
605 }
606 }
607 }
608
609 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
611 if w == 0 || h == 0 {
612 return;
613 }
614
615 self.line(x, y, x + w.saturating_sub(1), y);
616 self.line(
617 x + w.saturating_sub(1),
618 y,
619 x + w.saturating_sub(1),
620 y + h.saturating_sub(1),
621 );
622 self.line(
623 x + w.saturating_sub(1),
624 y + h.saturating_sub(1),
625 x,
626 y + h.saturating_sub(1),
627 );
628 self.line(x, y + h.saturating_sub(1), x, y);
629 }
630
631 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
633 let mut x = r as isize;
634 let mut y: isize = 0;
635 let mut err: isize = 1 - x;
636 let (cx, cy) = (cx as isize, cy as isize);
637
638 while x >= y {
639 for &(dx, dy) in &[
640 (x, y),
641 (y, x),
642 (-x, y),
643 (-y, x),
644 (x, -y),
645 (y, -x),
646 (-x, -y),
647 (-y, -x),
648 ] {
649 let px = cx + dx;
650 let py = cy + dy;
651 self.dot_isize(px, py);
652 }
653
654 y += 1;
655 if err < 0 {
656 err += 2 * y + 1;
657 } else {
658 x -= 1;
659 err += 2 * (y - x) + 1;
660 }
661 }
662 }
663
664 pub fn set_color(&mut self, color: Color) {
666 self.current_color = color;
667 }
668
669 pub fn color(&self) -> Color {
671 self.current_color
672 }
673
674 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
676 if w == 0 || h == 0 {
677 return;
678 }
679
680 let x_end = x.saturating_add(w).min(self.px_w);
681 let y_end = y.saturating_add(h).min(self.px_h);
682 if x >= x_end || y >= y_end {
683 return;
684 }
685
686 for yy in y..y_end {
687 self.line(x, yy, x_end.saturating_sub(1), yy);
688 }
689 }
690
691 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
693 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
694 for y in (cy - r)..=(cy + r) {
695 let dy = y - cy;
696 let span_sq = (r * r - dy * dy).max(0);
697 let dx = (span_sq as f64).sqrt() as isize;
698 for x in (cx - dx)..=(cx + dx) {
699 self.dot_isize(x, y);
700 }
701 }
702 }
703
704 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
706 self.line(x0, y0, x1, y1);
707 self.line(x1, y1, x2, y2);
708 self.line(x2, y2, x0, y0);
709 }
710
711 pub fn filled_triangle(
713 &mut self,
714 x0: usize,
715 y0: usize,
716 x1: usize,
717 y1: usize,
718 x2: usize,
719 y2: usize,
720 ) {
721 let vertices = [
722 (x0 as isize, y0 as isize),
723 (x1 as isize, y1 as isize),
724 (x2 as isize, y2 as isize),
725 ];
726 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
727 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
728
729 for y in min_y..=max_y {
730 let mut intersections: Vec<f64> = Vec::new();
731
732 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
733 let (x_a, y_a) = vertices[edge.0];
734 let (x_b, y_b) = vertices[edge.1];
735 if y_a == y_b {
736 continue;
737 }
738
739 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
740 (x_a, y_a, x_b, y_b)
741 } else {
742 (x_b, y_b, x_a, y_a)
743 };
744
745 if y < y_start || y >= y_end {
746 continue;
747 }
748
749 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
750 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
751 }
752
753 intersections.sort_by(|a, b| a.total_cmp(b));
754 let mut i = 0usize;
755 while i + 1 < intersections.len() {
756 let x_start = intersections[i].ceil() as isize;
757 let x_end = intersections[i + 1].floor() as isize;
758 for x in x_start..=x_end {
759 self.dot_isize(x, y);
760 }
761 i += 2;
762 }
763 }
764
765 self.triangle(x0, y0, x1, y1, x2, y2);
766 }
767
768 pub fn points(&mut self, pts: &[(usize, usize)]) {
770 for &(x, y) in pts {
771 self.dot(x, y);
772 }
773 }
774
775 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
777 for window in pts.windows(2) {
778 if let [(x0, y0), (x1, y1)] = window {
779 self.line(*x0, *y0, *x1, *y1);
780 }
781 }
782 }
783
784 pub fn print(&mut self, x: usize, y: usize, text: &str) {
787 if text.is_empty() {
788 return;
789 }
790
791 let color = self.current_color;
792 if let Some(layer) = self.current_layer_mut() {
793 layer.labels.push(CanvasLabel {
794 x,
795 y,
796 text: text.to_string(),
797 color,
798 });
799 }
800 }
801
802 pub fn layer(&mut self) {
804 self.layers.push(Self::new_layer(self.cols, self.rows));
805 }
806
807 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
808 let mut final_grid = vec![
809 vec![
810 CanvasPixel {
811 bits: 0,
812 color: Color::Reset,
813 };
814 self.cols
815 ];
816 self.rows
817 ];
818 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
819 vec![vec![None; self.cols]; self.rows];
820
821 for layer in &self.layers {
822 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
823 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
824 let src = layer.grid[row][col];
825 if src.bits == 0 {
826 continue;
827 }
828
829 let merged = dst.bits | src.bits;
830 if merged != dst.bits {
831 dst.bits = merged;
832 dst.color = src.color;
833 }
834 }
835 }
836
837 for label in &layer.labels {
838 let row = label.y / 4;
839 if row >= self.rows {
840 continue;
841 }
842 let start_col = label.x / 2;
843 for (offset, ch) in label.text.chars().enumerate() {
844 let col = start_col + offset;
845 if col >= self.cols {
846 break;
847 }
848 labels_overlay[row][col] = Some((ch, label.color));
849 }
850 }
851 }
852
853 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
854 for row in 0..self.rows {
855 let mut segments: Vec<(String, Color)> = Vec::new();
856 let mut current_color: Option<Color> = None;
857 let mut current_text = String::new();
858
859 for col in 0..self.cols {
860 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
861 (label_ch, label_color)
862 } else {
863 let bits = final_grid[row][col].bits;
864 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
865 (ch, final_grid[row][col].color)
866 };
867
868 match current_color {
869 Some(c) if c == color => {
870 current_text.push(ch);
871 }
872 Some(c) => {
873 segments.push((std::mem::take(&mut current_text), c));
874 current_text.push(ch);
875 current_color = Some(color);
876 }
877 None => {
878 current_text.push(ch);
879 current_color = Some(color);
880 }
881 }
882 }
883
884 if let Some(color) = current_color {
885 segments.push((current_text, color));
886 }
887 lines.push(segments);
888 }
889
890 lines
891 }
892}
893
894impl<'a> ContainerBuilder<'a> {
895 pub fn apply(mut self, style: &ContainerStyle) -> Self {
900 if let Some(v) = style.border {
901 self.border = Some(v);
902 }
903 if let Some(v) = style.border_sides {
904 self.border_sides = v;
905 }
906 if let Some(v) = style.border_style {
907 self.border_style = v;
908 }
909 if let Some(v) = style.bg {
910 self.bg = Some(v);
911 }
912 if let Some(v) = style.dark_bg {
913 self.dark_bg = Some(v);
914 }
915 if let Some(v) = style.dark_border_style {
916 self.dark_border_style = Some(v);
917 }
918 if let Some(v) = style.padding {
919 self.padding = v;
920 }
921 if let Some(v) = style.margin {
922 self.margin = v;
923 }
924 if let Some(v) = style.gap {
925 self.gap = v;
926 }
927 if let Some(v) = style.row_gap {
928 self.row_gap = Some(v);
929 }
930 if let Some(v) = style.col_gap {
931 self.col_gap = Some(v);
932 }
933 if let Some(v) = style.grow {
934 self.grow = v;
935 }
936 if let Some(v) = style.align {
937 self.align = v;
938 }
939 if let Some(v) = style.align_self {
940 self.align_self_value = Some(v);
941 }
942 if let Some(v) = style.justify {
943 self.justify = v;
944 }
945 if let Some(v) = style.text_color {
946 self.text_color = Some(v);
947 }
948 if let Some(w) = style.w {
949 self.constraints.min_width = Some(w);
950 self.constraints.max_width = Some(w);
951 }
952 if let Some(h) = style.h {
953 self.constraints.min_height = Some(h);
954 self.constraints.max_height = Some(h);
955 }
956 if let Some(v) = style.min_w {
957 self.constraints.min_width = Some(v);
958 }
959 if let Some(v) = style.max_w {
960 self.constraints.max_width = Some(v);
961 }
962 if let Some(v) = style.min_h {
963 self.constraints.min_height = Some(v);
964 }
965 if let Some(v) = style.max_h {
966 self.constraints.max_height = Some(v);
967 }
968 if let Some(v) = style.w_pct {
969 self.constraints.width_pct = Some(v);
970 }
971 if let Some(v) = style.h_pct {
972 self.constraints.height_pct = Some(v);
973 }
974 self
975 }
976
977 pub fn border(mut self, border: Border) -> Self {
979 self.border = Some(border);
980 self
981 }
982
983 pub fn border_top(mut self, show: bool) -> Self {
985 self.border_sides.top = show;
986 self
987 }
988
989 pub fn border_right(mut self, show: bool) -> Self {
991 self.border_sides.right = show;
992 self
993 }
994
995 pub fn border_bottom(mut self, show: bool) -> Self {
997 self.border_sides.bottom = show;
998 self
999 }
1000
1001 pub fn border_left(mut self, show: bool) -> Self {
1003 self.border_sides.left = show;
1004 self
1005 }
1006
1007 pub fn border_sides(mut self, sides: BorderSides) -> Self {
1009 self.border_sides = sides;
1010 self
1011 }
1012
1013 pub fn border_x(self) -> Self {
1015 self.border_sides(BorderSides {
1016 top: false,
1017 right: true,
1018 bottom: false,
1019 left: true,
1020 })
1021 }
1022
1023 pub fn border_y(self) -> Self {
1025 self.border_sides(BorderSides {
1026 top: true,
1027 right: false,
1028 bottom: true,
1029 left: false,
1030 })
1031 }
1032
1033 pub fn rounded(self) -> Self {
1035 self.border(Border::Rounded)
1036 }
1037
1038 pub fn border_style(mut self, style: Style) -> Self {
1040 self.border_style = style;
1041 self
1042 }
1043
1044 pub fn border_fg(mut self, color: Color) -> Self {
1046 self.border_style = self.border_style.fg(color);
1047 self
1048 }
1049
1050 pub fn dark_border_style(mut self, style: Style) -> Self {
1052 self.dark_border_style = Some(style);
1053 self
1054 }
1055
1056 pub fn bg(mut self, color: Color) -> Self {
1057 self.bg = Some(color);
1058 self
1059 }
1060
1061 pub fn text_color(mut self, color: Color) -> Self {
1064 self.text_color = Some(color);
1065 self
1066 }
1067
1068 pub fn dark_bg(mut self, color: Color) -> Self {
1070 self.dark_bg = Some(color);
1071 self
1072 }
1073
1074 pub fn group_hover_bg(mut self, color: Color) -> Self {
1076 self.group_hover_bg = Some(color);
1077 self
1078 }
1079
1080 pub fn group_hover_border_style(mut self, style: Style) -> Self {
1082 self.group_hover_border_style = Some(style);
1083 self
1084 }
1085
1086 pub fn p(self, value: u32) -> Self {
1090 self.pad(value)
1091 }
1092
1093 pub fn pad(mut self, value: u32) -> Self {
1095 self.padding = Padding::all(value);
1096 self
1097 }
1098
1099 pub fn px(mut self, value: u32) -> Self {
1101 self.padding.left = value;
1102 self.padding.right = value;
1103 self
1104 }
1105
1106 pub fn py(mut self, value: u32) -> Self {
1108 self.padding.top = value;
1109 self.padding.bottom = value;
1110 self
1111 }
1112
1113 pub fn pt(mut self, value: u32) -> Self {
1115 self.padding.top = value;
1116 self
1117 }
1118
1119 pub fn pr(mut self, value: u32) -> Self {
1121 self.padding.right = value;
1122 self
1123 }
1124
1125 pub fn pb(mut self, value: u32) -> Self {
1127 self.padding.bottom = value;
1128 self
1129 }
1130
1131 pub fn pl(mut self, value: u32) -> Self {
1133 self.padding.left = value;
1134 self
1135 }
1136
1137 pub fn padding(mut self, padding: Padding) -> Self {
1139 self.padding = padding;
1140 self
1141 }
1142
1143 pub fn m(mut self, value: u32) -> Self {
1147 self.margin = Margin::all(value);
1148 self
1149 }
1150
1151 pub fn mx(mut self, value: u32) -> Self {
1153 self.margin.left = value;
1154 self.margin.right = value;
1155 self
1156 }
1157
1158 pub fn my(mut self, value: u32) -> Self {
1160 self.margin.top = value;
1161 self.margin.bottom = value;
1162 self
1163 }
1164
1165 pub fn mt(mut self, value: u32) -> Self {
1167 self.margin.top = value;
1168 self
1169 }
1170
1171 pub fn mr(mut self, value: u32) -> Self {
1173 self.margin.right = value;
1174 self
1175 }
1176
1177 pub fn mb(mut self, value: u32) -> Self {
1179 self.margin.bottom = value;
1180 self
1181 }
1182
1183 pub fn ml(mut self, value: u32) -> Self {
1185 self.margin.left = value;
1186 self
1187 }
1188
1189 pub fn margin(mut self, margin: Margin) -> Self {
1191 self.margin = margin;
1192 self
1193 }
1194
1195 pub fn w(mut self, value: u32) -> Self {
1199 self.constraints.min_width = Some(value);
1200 self.constraints.max_width = Some(value);
1201 self
1202 }
1203
1204 pub fn xs_w(self, value: u32) -> Self {
1211 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1212 if is_xs {
1213 self.w(value)
1214 } else {
1215 self
1216 }
1217 }
1218
1219 pub fn sm_w(self, value: u32) -> Self {
1221 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1222 if is_sm {
1223 self.w(value)
1224 } else {
1225 self
1226 }
1227 }
1228
1229 pub fn md_w(self, value: u32) -> Self {
1231 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1232 if is_md {
1233 self.w(value)
1234 } else {
1235 self
1236 }
1237 }
1238
1239 pub fn lg_w(self, value: u32) -> Self {
1241 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1242 if is_lg {
1243 self.w(value)
1244 } else {
1245 self
1246 }
1247 }
1248
1249 pub fn xl_w(self, value: u32) -> Self {
1251 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1252 if is_xl {
1253 self.w(value)
1254 } else {
1255 self
1256 }
1257 }
1258 pub fn w_at(self, bp: Breakpoint, value: u32) -> Self {
1259 if self.ctx.breakpoint() == bp {
1260 self.w(value)
1261 } else {
1262 self
1263 }
1264 }
1265
1266 pub fn h(mut self, value: u32) -> Self {
1268 self.constraints.min_height = Some(value);
1269 self.constraints.max_height = Some(value);
1270 self
1271 }
1272
1273 pub fn xs_h(self, value: u32) -> Self {
1275 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1276 if is_xs {
1277 self.h(value)
1278 } else {
1279 self
1280 }
1281 }
1282
1283 pub fn sm_h(self, value: u32) -> Self {
1285 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1286 if is_sm {
1287 self.h(value)
1288 } else {
1289 self
1290 }
1291 }
1292
1293 pub fn md_h(self, value: u32) -> Self {
1295 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1296 if is_md {
1297 self.h(value)
1298 } else {
1299 self
1300 }
1301 }
1302
1303 pub fn lg_h(self, value: u32) -> Self {
1305 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1306 if is_lg {
1307 self.h(value)
1308 } else {
1309 self
1310 }
1311 }
1312
1313 pub fn xl_h(self, value: u32) -> Self {
1315 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1316 if is_xl {
1317 self.h(value)
1318 } else {
1319 self
1320 }
1321 }
1322 pub fn h_at(self, bp: Breakpoint, value: u32) -> Self {
1323 if self.ctx.breakpoint() == bp {
1324 self.h(value)
1325 } else {
1326 self
1327 }
1328 }
1329
1330 pub fn min_w(mut self, value: u32) -> Self {
1332 self.constraints.min_width = Some(value);
1333 self
1334 }
1335
1336 pub fn xs_min_w(self, value: u32) -> Self {
1338 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1339 if is_xs {
1340 self.min_w(value)
1341 } else {
1342 self
1343 }
1344 }
1345
1346 pub fn sm_min_w(self, value: u32) -> Self {
1348 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1349 if is_sm {
1350 self.min_w(value)
1351 } else {
1352 self
1353 }
1354 }
1355
1356 pub fn md_min_w(self, value: u32) -> Self {
1358 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1359 if is_md {
1360 self.min_w(value)
1361 } else {
1362 self
1363 }
1364 }
1365
1366 pub fn lg_min_w(self, value: u32) -> Self {
1368 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1369 if is_lg {
1370 self.min_w(value)
1371 } else {
1372 self
1373 }
1374 }
1375
1376 pub fn xl_min_w(self, value: u32) -> Self {
1378 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1379 if is_xl {
1380 self.min_w(value)
1381 } else {
1382 self
1383 }
1384 }
1385 pub fn min_w_at(self, bp: Breakpoint, value: u32) -> Self {
1386 if self.ctx.breakpoint() == bp {
1387 self.min_w(value)
1388 } else {
1389 self
1390 }
1391 }
1392
1393 pub fn max_w(mut self, value: u32) -> Self {
1395 self.constraints.max_width = Some(value);
1396 self
1397 }
1398
1399 pub fn xs_max_w(self, value: u32) -> Self {
1401 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1402 if is_xs {
1403 self.max_w(value)
1404 } else {
1405 self
1406 }
1407 }
1408
1409 pub fn sm_max_w(self, value: u32) -> Self {
1411 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1412 if is_sm {
1413 self.max_w(value)
1414 } else {
1415 self
1416 }
1417 }
1418
1419 pub fn md_max_w(self, value: u32) -> Self {
1421 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1422 if is_md {
1423 self.max_w(value)
1424 } else {
1425 self
1426 }
1427 }
1428
1429 pub fn lg_max_w(self, value: u32) -> Self {
1431 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1432 if is_lg {
1433 self.max_w(value)
1434 } else {
1435 self
1436 }
1437 }
1438
1439 pub fn xl_max_w(self, value: u32) -> Self {
1441 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1442 if is_xl {
1443 self.max_w(value)
1444 } else {
1445 self
1446 }
1447 }
1448 pub fn max_w_at(self, bp: Breakpoint, value: u32) -> Self {
1449 if self.ctx.breakpoint() == bp {
1450 self.max_w(value)
1451 } else {
1452 self
1453 }
1454 }
1455
1456 pub fn min_h(mut self, value: u32) -> Self {
1458 self.constraints.min_height = Some(value);
1459 self
1460 }
1461
1462 pub fn max_h(mut self, value: u32) -> Self {
1464 self.constraints.max_height = Some(value);
1465 self
1466 }
1467
1468 pub fn min_width(mut self, value: u32) -> Self {
1470 self.constraints.min_width = Some(value);
1471 self
1472 }
1473
1474 pub fn max_width(mut self, value: u32) -> Self {
1476 self.constraints.max_width = Some(value);
1477 self
1478 }
1479
1480 pub fn min_height(mut self, value: u32) -> Self {
1482 self.constraints.min_height = Some(value);
1483 self
1484 }
1485
1486 pub fn max_height(mut self, value: u32) -> Self {
1488 self.constraints.max_height = Some(value);
1489 self
1490 }
1491
1492 pub fn w_pct(mut self, pct: u8) -> Self {
1494 self.constraints.width_pct = Some(pct.min(100));
1495 self
1496 }
1497
1498 pub fn h_pct(mut self, pct: u8) -> Self {
1500 self.constraints.height_pct = Some(pct.min(100));
1501 self
1502 }
1503
1504 pub fn constraints(mut self, constraints: Constraints) -> Self {
1506 self.constraints = constraints;
1507 self
1508 }
1509
1510 pub fn gap(mut self, gap: u32) -> Self {
1514 self.gap = gap;
1515 self
1516 }
1517
1518 pub fn row_gap(mut self, value: u32) -> Self {
1521 self.row_gap = Some(value);
1522 self
1523 }
1524
1525 pub fn col_gap(mut self, value: u32) -> Self {
1528 self.col_gap = Some(value);
1529 self
1530 }
1531
1532 pub fn xs_gap(self, value: u32) -> Self {
1534 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1535 if is_xs {
1536 self.gap(value)
1537 } else {
1538 self
1539 }
1540 }
1541
1542 pub fn sm_gap(self, value: u32) -> Self {
1544 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1545 if is_sm {
1546 self.gap(value)
1547 } else {
1548 self
1549 }
1550 }
1551
1552 pub fn md_gap(self, value: u32) -> Self {
1559 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1560 if is_md {
1561 self.gap(value)
1562 } else {
1563 self
1564 }
1565 }
1566
1567 pub fn lg_gap(self, value: u32) -> Self {
1569 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1570 if is_lg {
1571 self.gap(value)
1572 } else {
1573 self
1574 }
1575 }
1576
1577 pub fn xl_gap(self, value: u32) -> Self {
1579 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1580 if is_xl {
1581 self.gap(value)
1582 } else {
1583 self
1584 }
1585 }
1586
1587 pub fn gap_at(self, bp: Breakpoint, value: u32) -> Self {
1588 if self.ctx.breakpoint() == bp {
1589 self.gap(value)
1590 } else {
1591 self
1592 }
1593 }
1594
1595 pub fn grow(mut self, grow: u16) -> Self {
1597 self.grow = grow;
1598 self
1599 }
1600
1601 pub fn xs_grow(self, value: u16) -> Self {
1603 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1604 if is_xs {
1605 self.grow(value)
1606 } else {
1607 self
1608 }
1609 }
1610
1611 pub fn sm_grow(self, value: u16) -> Self {
1613 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1614 if is_sm {
1615 self.grow(value)
1616 } else {
1617 self
1618 }
1619 }
1620
1621 pub fn md_grow(self, value: u16) -> Self {
1623 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1624 if is_md {
1625 self.grow(value)
1626 } else {
1627 self
1628 }
1629 }
1630
1631 pub fn lg_grow(self, value: u16) -> Self {
1633 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1634 if is_lg {
1635 self.grow(value)
1636 } else {
1637 self
1638 }
1639 }
1640
1641 pub fn xl_grow(self, value: u16) -> Self {
1643 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1644 if is_xl {
1645 self.grow(value)
1646 } else {
1647 self
1648 }
1649 }
1650 pub fn grow_at(self, bp: Breakpoint, value: u16) -> Self {
1651 if self.ctx.breakpoint() == bp {
1652 self.grow(value)
1653 } else {
1654 self
1655 }
1656 }
1657
1658 pub fn xs_p(self, value: u32) -> Self {
1660 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1661 if is_xs {
1662 self.p(value)
1663 } else {
1664 self
1665 }
1666 }
1667
1668 pub fn sm_p(self, value: u32) -> Self {
1670 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1671 if is_sm {
1672 self.p(value)
1673 } else {
1674 self
1675 }
1676 }
1677
1678 pub fn md_p(self, value: u32) -> Self {
1680 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1681 if is_md {
1682 self.p(value)
1683 } else {
1684 self
1685 }
1686 }
1687
1688 pub fn lg_p(self, value: u32) -> Self {
1690 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1691 if is_lg {
1692 self.p(value)
1693 } else {
1694 self
1695 }
1696 }
1697
1698 pub fn xl_p(self, value: u32) -> Self {
1700 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1701 if is_xl {
1702 self.p(value)
1703 } else {
1704 self
1705 }
1706 }
1707 pub fn p_at(self, bp: Breakpoint, value: u32) -> Self {
1708 if self.ctx.breakpoint() == bp {
1709 self.p(value)
1710 } else {
1711 self
1712 }
1713 }
1714
1715 pub fn align(mut self, align: Align) -> Self {
1719 self.align = align;
1720 self
1721 }
1722
1723 pub fn center(self) -> Self {
1725 self.align(Align::Center)
1726 }
1727
1728 pub fn justify(mut self, justify: Justify) -> Self {
1730 self.justify = justify;
1731 self
1732 }
1733
1734 pub fn space_between(self) -> Self {
1736 self.justify(Justify::SpaceBetween)
1737 }
1738
1739 pub fn space_around(self) -> Self {
1741 self.justify(Justify::SpaceAround)
1742 }
1743
1744 pub fn space_evenly(self) -> Self {
1746 self.justify(Justify::SpaceEvenly)
1747 }
1748
1749 pub fn flex_center(self) -> Self {
1751 self.justify(Justify::Center).align(Align::Center)
1752 }
1753
1754 pub fn align_self(mut self, align: Align) -> Self {
1757 self.align_self_value = Some(align);
1758 self
1759 }
1760
1761 pub fn title(self, title: impl Into<String>) -> Self {
1765 self.title_styled(title, Style::new())
1766 }
1767
1768 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1770 self.title = Some((title.into(), style));
1771 self
1772 }
1773
1774 pub fn scroll_offset(mut self, offset: u32) -> Self {
1778 self.scroll_offset = Some(offset);
1779 self
1780 }
1781
1782 fn group_name(mut self, name: String) -> Self {
1783 self.group_name = Some(name);
1784 self
1785 }
1786
1787 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1792 self.finish(Direction::Column, f)
1793 }
1794
1795 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1800 self.finish(Direction::Row, f)
1801 }
1802
1803 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1808 self.gap = 0;
1809 self.finish(Direction::Row, f)
1810 }
1811
1812 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1827 let draw_id = self.ctx.deferred_draws.len();
1828 self.ctx.deferred_draws.push(Some(Box::new(f)));
1829 self.ctx.interaction_count += 1;
1830 self.ctx.commands.push(Command::RawDraw {
1831 draw_id,
1832 constraints: self.constraints,
1833 grow: self.grow,
1834 margin: self.margin,
1835 });
1836 }
1837
1838 fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1839 let interaction_id = self.ctx.interaction_count;
1840 self.ctx.interaction_count += 1;
1841 let resolved_gap = match direction {
1842 Direction::Column => self.row_gap.unwrap_or(self.gap),
1843 Direction::Row => self.col_gap.unwrap_or(self.gap),
1844 };
1845
1846 let in_hovered_group = self
1847 .group_name
1848 .as_ref()
1849 .map(|name| self.ctx.is_group_hovered(name))
1850 .unwrap_or(false)
1851 || self
1852 .ctx
1853 .group_stack
1854 .last()
1855 .map(|name| self.ctx.is_group_hovered(name))
1856 .unwrap_or(false);
1857 let in_focused_group = self
1858 .group_name
1859 .as_ref()
1860 .map(|name| self.ctx.is_group_focused(name))
1861 .unwrap_or(false)
1862 || self
1863 .ctx
1864 .group_stack
1865 .last()
1866 .map(|name| self.ctx.is_group_focused(name))
1867 .unwrap_or(false);
1868
1869 let resolved_bg = if self.ctx.dark_mode {
1870 self.dark_bg.or(self.bg)
1871 } else {
1872 self.bg
1873 };
1874 let resolved_border_style = if self.ctx.dark_mode {
1875 self.dark_border_style.unwrap_or(self.border_style)
1876 } else {
1877 self.border_style
1878 };
1879 let bg_color = if in_hovered_group || in_focused_group {
1880 self.group_hover_bg.or(resolved_bg)
1881 } else {
1882 resolved_bg
1883 };
1884 let border_style = if in_hovered_group || in_focused_group {
1885 self.group_hover_border_style
1886 .unwrap_or(resolved_border_style)
1887 } else {
1888 resolved_border_style
1889 };
1890 let group_name = self.group_name.take();
1891 let is_group_container = group_name.is_some();
1892
1893 if let Some(scroll_offset) = self.scroll_offset {
1894 self.ctx.commands.push(Command::BeginScrollable {
1895 grow: self.grow,
1896 border: self.border,
1897 border_sides: self.border_sides,
1898 border_style,
1899 padding: self.padding,
1900 margin: self.margin,
1901 constraints: self.constraints,
1902 title: self.title,
1903 scroll_offset,
1904 });
1905 } else {
1906 self.ctx.commands.push(Command::BeginContainer {
1907 direction,
1908 gap: resolved_gap,
1909 align: self.align,
1910 align_self: self.align_self_value,
1911 justify: self.justify,
1912 border: self.border,
1913 border_sides: self.border_sides,
1914 border_style,
1915 bg_color,
1916 padding: self.padding,
1917 margin: self.margin,
1918 constraints: self.constraints,
1919 title: self.title,
1920 grow: self.grow,
1921 group_name,
1922 });
1923 }
1924 self.ctx.text_color_stack.push(self.text_color);
1925 f(self.ctx);
1926 self.ctx.text_color_stack.pop();
1927 self.ctx.commands.push(Command::EndContainer);
1928 self.ctx.last_text_idx = None;
1929
1930 if is_group_container {
1931 self.ctx.group_stack.pop();
1932 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1933 }
1934
1935 self.ctx.response_for(interaction_id)
1936 }
1937}
1938
1939impl Context {
1940 pub(crate) fn new(
1941 events: Vec<Event>,
1942 width: u32,
1943 height: u32,
1944 state: &mut FrameState,
1945 theme: Theme,
1946 ) -> Self {
1947 let consumed = vec![false; events.len()];
1948
1949 let mut mouse_pos = state.last_mouse_pos;
1950 let mut click_pos = None;
1951 for event in &events {
1952 if let Event::Mouse(mouse) = event {
1953 mouse_pos = Some((mouse.x, mouse.y));
1954 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1955 click_pos = Some((mouse.x, mouse.y));
1956 }
1957 }
1958 }
1959
1960 let mut focus_index = state.focus_index;
1961 if let Some((mx, my)) = click_pos {
1962 let mut best: Option<(usize, u64)> = None;
1963 for &(fid, rect) in &state.prev_focus_rects {
1964 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1965 let area = rect.width as u64 * rect.height as u64;
1966 if best.map_or(true, |(_, ba)| area < ba) {
1967 best = Some((fid, area));
1968 }
1969 }
1970 }
1971 if let Some((fid, _)) = best {
1972 focus_index = fid;
1973 }
1974 }
1975
1976 Self {
1977 commands: Vec::new(),
1978 events,
1979 consumed,
1980 should_quit: false,
1981 area_width: width,
1982 area_height: height,
1983 tick: state.tick,
1984 focus_index,
1985 focus_count: 0,
1986 hook_states: std::mem::take(&mut state.hook_states),
1987 hook_cursor: 0,
1988 prev_focus_count: state.prev_focus_count,
1989 scroll_count: 0,
1990 prev_scroll_infos: std::mem::take(&mut state.prev_scroll_infos),
1991 prev_scroll_rects: std::mem::take(&mut state.prev_scroll_rects),
1992 interaction_count: 0,
1993 prev_hit_map: std::mem::take(&mut state.prev_hit_map),
1994 group_stack: Vec::new(),
1995 prev_group_rects: std::mem::take(&mut state.prev_group_rects),
1996 group_count: 0,
1997 prev_focus_groups: std::mem::take(&mut state.prev_focus_groups),
1998 _prev_focus_rects: std::mem::take(&mut state.prev_focus_rects),
1999 mouse_pos,
2000 click_pos,
2001 last_text_idx: None,
2002 overlay_depth: 0,
2003 modal_active: false,
2004 prev_modal_active: state.prev_modal_active,
2005 clipboard_text: None,
2006 debug: state.debug_mode,
2007 theme,
2008 dark_mode: theme.is_dark,
2009 is_real_terminal: false,
2010 deferred_draws: Vec::new(),
2011 notification_queue: std::mem::take(&mut state.notification_queue),
2012 text_color_stack: Vec::new(),
2013 }
2014 }
2015
2016 pub(crate) fn process_focus_keys(&mut self) {
2017 for (i, event) in self.events.iter().enumerate() {
2018 if let Event::Key(key) = event {
2019 if key.kind != KeyEventKind::Press {
2020 continue;
2021 }
2022 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
2023 if self.prev_focus_count > 0 {
2024 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
2025 }
2026 self.consumed[i] = true;
2027 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
2028 || key.code == KeyCode::BackTab
2029 {
2030 if self.prev_focus_count > 0 {
2031 self.focus_index = if self.focus_index == 0 {
2032 self.prev_focus_count - 1
2033 } else {
2034 self.focus_index - 1
2035 };
2036 }
2037 self.consumed[i] = true;
2038 }
2039 }
2040 }
2041 }
2042
2043 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
2047 w.ui(self)
2048 }
2049
2050 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
2065 self.error_boundary_with(f, |ui, msg| {
2066 ui.styled(
2067 format!("⚠ Error: {msg}"),
2068 Style::new().fg(ui.theme.error).bold(),
2069 );
2070 });
2071 }
2072
2073 pub fn error_boundary_with(
2093 &mut self,
2094 f: impl FnOnce(&mut Context),
2095 fallback: impl FnOnce(&mut Context, String),
2096 ) {
2097 let snapshot = ContextSnapshot::capture(self);
2098
2099 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2100 f(self);
2101 }));
2102
2103 match result {
2104 Ok(()) => {}
2105 Err(panic_info) => {
2106 if self.is_real_terminal {
2107 let _ = crossterm::terminal::enable_raw_mode();
2108 let _ = crossterm::execute!(
2109 std::io::stdout(),
2110 crossterm::terminal::EnterAlternateScreen
2111 );
2112 }
2113
2114 snapshot.restore(self);
2115
2116 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
2117 (*s).to_string()
2118 } else if let Some(s) = panic_info.downcast_ref::<String>() {
2119 s.clone()
2120 } else {
2121 "widget panicked".to_string()
2122 };
2123
2124 fallback(self, msg);
2125 }
2126 }
2127 }
2128
2129 pub fn interaction(&mut self) -> Response {
2135 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2136 return Response::none();
2137 }
2138 let id = self.interaction_count;
2139 self.interaction_count += 1;
2140 self.response_for(id)
2141 }
2142
2143 pub fn register_focusable(&mut self) -> bool {
2148 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2149 return false;
2150 }
2151 let id = self.focus_count;
2152 self.focus_count += 1;
2153 self.commands.push(Command::FocusMarker(id));
2154 if self.prev_focus_count == 0 {
2155 return true;
2156 }
2157 self.focus_index % self.prev_focus_count == id
2158 }
2159
2160 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
2178 let idx = self.hook_cursor;
2179 self.hook_cursor += 1;
2180
2181 if idx >= self.hook_states.len() {
2182 self.hook_states.push(Box::new(init()));
2183 }
2184
2185 State {
2186 idx,
2187 _marker: std::marker::PhantomData,
2188 }
2189 }
2190
2191 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
2199 &mut self,
2200 deps: &D,
2201 compute: impl FnOnce(&D) -> T,
2202 ) -> &T {
2203 let idx = self.hook_cursor;
2204 self.hook_cursor += 1;
2205
2206 let should_recompute = if idx >= self.hook_states.len() {
2207 true
2208 } else {
2209 let (stored_deps, _) = self.hook_states[idx]
2210 .downcast_ref::<(D, T)>()
2211 .unwrap_or_else(|| {
2212 panic!(
2213 "use_memo type mismatch at hook index {} — expected {}",
2214 idx,
2215 std::any::type_name::<D>()
2216 )
2217 });
2218 stored_deps != deps
2219 };
2220
2221 if should_recompute {
2222 let value = compute(deps);
2223 let slot = Box::new((deps.clone(), value));
2224 if idx < self.hook_states.len() {
2225 self.hook_states[idx] = slot;
2226 } else {
2227 self.hook_states.push(slot);
2228 }
2229 }
2230
2231 let (_, value) = self.hook_states[idx]
2232 .downcast_ref::<(D, T)>()
2233 .unwrap_or_else(|| {
2234 panic!(
2235 "use_memo type mismatch at hook index {} — expected {}",
2236 idx,
2237 std::any::type_name::<D>()
2238 )
2239 });
2240 value
2241 }
2242
2243 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
2245 if self.theme.is_dark {
2246 dark
2247 } else {
2248 light
2249 }
2250 }
2251
2252 pub fn notify(&mut self, message: &str, level: ToastLevel) {
2262 let tick = self.tick;
2263 self.notification_queue
2264 .push((message.to_string(), level, tick));
2265 }
2266
2267 pub(crate) fn render_notifications(&mut self) {
2268 self.notification_queue
2269 .retain(|(_, _, created)| self.tick.saturating_sub(*created) < 180);
2270 if self.notification_queue.is_empty() {
2271 return;
2272 }
2273
2274 let items: Vec<(String, Color)> = self
2275 .notification_queue
2276 .iter()
2277 .rev()
2278 .map(|(message, level, _)| {
2279 let color = match level {
2280 ToastLevel::Info => self.theme.primary,
2281 ToastLevel::Success => self.theme.success,
2282 ToastLevel::Warning => self.theme.warning,
2283 ToastLevel::Error => self.theme.error,
2284 };
2285 (message.clone(), color)
2286 })
2287 .collect();
2288
2289 self.overlay(|ui| {
2290 ui.row(|ui| {
2291 ui.spacer();
2292 ui.col(|ui| {
2293 for (message, color) in &items {
2294 ui.styled(format!("● {message}"), Style::new().fg(*color));
2295 }
2296 });
2297 });
2298 });
2299 }
2300}
2301
2302mod widgets_display;
2303mod widgets_input;
2304mod widgets_interactive;
2305mod widgets_viz;
2306
2307#[inline]
2308fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2309 if char_index == 0 {
2310 return 0;
2311 }
2312 value
2313 .char_indices()
2314 .nth(char_index)
2315 .map_or(value.len(), |(idx, _)| idx)
2316}
2317
2318fn format_token_count(count: usize) -> String {
2319 if count >= 1_000_000 {
2320 format!("{:.1}M", count as f64 / 1_000_000.0)
2321 } else if count >= 1_000 {
2322 format!("{:.1}k", count as f64 / 1_000.0)
2323 } else {
2324 format!("{count}")
2325 }
2326}
2327
2328fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2329 let mut parts: Vec<String> = Vec::new();
2330 for (i, width) in widths.iter().enumerate() {
2331 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2332 let cell_width = UnicodeWidthStr::width(cell) as u32;
2333 let padding = (*width).saturating_sub(cell_width) as usize;
2334 parts.push(format!("{cell}{}", " ".repeat(padding)));
2335 }
2336 parts.join(separator)
2337}
2338
2339fn table_visible_len(state: &TableState) -> usize {
2340 if state.page_size == 0 {
2341 return state.visible_indices().len();
2342 }
2343
2344 let start = state
2345 .page
2346 .saturating_mul(state.page_size)
2347 .min(state.visible_indices().len());
2348 let end = (start + state.page_size).min(state.visible_indices().len());
2349 end.saturating_sub(start)
2350}
2351
2352pub(crate) fn handle_vertical_nav(
2353 selected: &mut usize,
2354 max_index: usize,
2355 key_code: KeyCode,
2356) -> bool {
2357 match key_code {
2358 KeyCode::Up | KeyCode::Char('k') => {
2359 if *selected > 0 {
2360 *selected -= 1;
2361 true
2362 } else {
2363 false
2364 }
2365 }
2366 KeyCode::Down | KeyCode::Char('j') => {
2367 if *selected < max_index {
2368 *selected += 1;
2369 true
2370 } else {
2371 false
2372 }
2373 }
2374 _ => false,
2375 }
2376}
2377
2378fn format_compact_number(value: f64) -> String {
2379 if value.fract().abs() < f64::EPSILON {
2380 return format!("{value:.0}");
2381 }
2382
2383 let mut s = format!("{value:.2}");
2384 while s.contains('.') && s.ends_with('0') {
2385 s.pop();
2386 }
2387 if s.ends_with('.') {
2388 s.pop();
2389 }
2390 s
2391}
2392
2393fn center_text(text: &str, width: usize) -> String {
2394 let text_width = UnicodeWidthStr::width(text);
2395 if text_width >= width {
2396 return text.to_string();
2397 }
2398
2399 let total = width - text_width;
2400 let left = total / 2;
2401 let right = total - left;
2402 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2403}
2404
2405struct TextareaVLine {
2406 logical_row: usize,
2407 char_start: usize,
2408 char_count: usize,
2409}
2410
2411fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2412 let mut out = Vec::new();
2413 for (row, line) in lines.iter().enumerate() {
2414 if line.is_empty() || wrap_width == u32::MAX {
2415 out.push(TextareaVLine {
2416 logical_row: row,
2417 char_start: 0,
2418 char_count: line.chars().count(),
2419 });
2420 continue;
2421 }
2422 let mut seg_start = 0usize;
2423 let mut seg_chars = 0usize;
2424 let mut seg_width = 0u32;
2425 for (idx, ch) in line.chars().enumerate() {
2426 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2427 if seg_width + cw > wrap_width && seg_chars > 0 {
2428 out.push(TextareaVLine {
2429 logical_row: row,
2430 char_start: seg_start,
2431 char_count: seg_chars,
2432 });
2433 seg_start = idx;
2434 seg_chars = 0;
2435 seg_width = 0;
2436 }
2437 seg_chars += 1;
2438 seg_width += cw;
2439 }
2440 out.push(TextareaVLine {
2441 logical_row: row,
2442 char_start: seg_start,
2443 char_count: seg_chars,
2444 });
2445 }
2446 out
2447}
2448
2449fn textarea_logical_to_visual(
2450 vlines: &[TextareaVLine],
2451 logical_row: usize,
2452 logical_col: usize,
2453) -> (usize, usize) {
2454 for (i, vl) in vlines.iter().enumerate() {
2455 if vl.logical_row != logical_row {
2456 continue;
2457 }
2458 let seg_end = vl.char_start + vl.char_count;
2459 if logical_col >= vl.char_start && logical_col < seg_end {
2460 return (i, logical_col - vl.char_start);
2461 }
2462 if logical_col == seg_end {
2463 let is_last_seg = vlines
2464 .get(i + 1)
2465 .map_or(true, |next| next.logical_row != logical_row);
2466 if is_last_seg {
2467 return (i, logical_col - vl.char_start);
2468 }
2469 }
2470 }
2471 (vlines.len().saturating_sub(1), 0)
2472}
2473
2474fn textarea_visual_to_logical(
2475 vlines: &[TextareaVLine],
2476 visual_row: usize,
2477 visual_col: usize,
2478) -> (usize, usize) {
2479 if let Some(vl) = vlines.get(visual_row) {
2480 let logical_col = vl.char_start + visual_col.min(vl.char_count);
2481 (vl.logical_row, logical_col)
2482 } else {
2483 (0, 0)
2484 }
2485}
2486
2487fn open_url(url: &str) -> std::io::Result<()> {
2488 #[cfg(target_os = "macos")]
2489 {
2490 std::process::Command::new("open").arg(url).spawn()?;
2491 }
2492 #[cfg(target_os = "linux")]
2493 {
2494 std::process::Command::new("xdg-open").arg(url).spawn()?;
2495 }
2496 #[cfg(target_os = "windows")]
2497 {
2498 std::process::Command::new("cmd")
2499 .args(["/c", "start", "", url])
2500 .spawn()?;
2501 }
2502 Ok(())
2503}
2504
2505#[cfg(test)]
2506mod tests {
2507 use super::*;
2508 use crate::test_utils::TestBackend;
2509
2510 #[test]
2511 fn use_memo_type_mismatch_includes_hook_index_and_expected_type() {
2512 let mut state = FrameState::default();
2513 let mut ctx = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2514 ctx.hook_states.push(Box::new(42u32));
2515 ctx.hook_cursor = 0;
2516
2517 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2518 let deps = 1u8;
2519 let _ = ctx.use_memo(&deps, |_| 7u8);
2520 }))
2521 .expect_err("use_memo should panic on type mismatch");
2522
2523 let message = panic_message(panic);
2524 assert!(
2525 message.contains("use_memo type mismatch at hook index 0"),
2526 "panic message should include hook index, got: {message}"
2527 );
2528 assert!(
2529 message.contains(std::any::type_name::<u8>()),
2530 "panic message should include expected type, got: {message}"
2531 );
2532 }
2533
2534 #[test]
2535 fn light_dark_uses_current_theme_mode() {
2536 let mut dark_backend = TestBackend::new(10, 2);
2537 dark_backend.render(|ui| {
2538 let color = ui.light_dark(Color::Red, Color::Blue);
2539 ui.text("X").fg(color);
2540 });
2541 assert_eq!(dark_backend.buffer().get(0, 0).style.fg, Some(Color::Blue));
2542
2543 let mut light_backend = TestBackend::new(10, 2);
2544 light_backend.render(|ui| {
2545 ui.set_theme(Theme::light());
2546 let color = ui.light_dark(Color::Red, Color::Blue);
2547 ui.text("X").fg(color);
2548 });
2549 assert_eq!(light_backend.buffer().get(0, 0).style.fg, Some(Color::Red));
2550 }
2551
2552 fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
2553 if let Some(s) = panic.downcast_ref::<String>() {
2554 s.clone()
2555 } else if let Some(s) = panic.downcast_ref::<&str>() {
2556 (*s).to_string()
2557 } else {
2558 "<non-string panic payload>".to_string()
2559 }
2560 }
2561}