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