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,
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}
129
130impl Bar {
131 pub fn new(label: impl Into<String>, value: f64) -> Self {
133 Self {
134 label: label.into(),
135 value,
136 color: None,
137 }
138 }
139
140 pub fn color(mut self, color: Color) -> Self {
142 self.color = Some(color);
143 self
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct BarGroup {
150 pub label: String,
152 pub bars: Vec<Bar>,
154}
155
156impl BarGroup {
157 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
159 Self {
160 label: label.into(),
161 bars,
162 }
163 }
164}
165
166pub trait Widget {
228 type Response;
231
232 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
238}
239
240pub struct Context {
256 pub(crate) commands: Vec<Command>,
258 pub(crate) events: Vec<Event>,
259 pub(crate) consumed: Vec<bool>,
260 pub(crate) should_quit: bool,
261 pub(crate) area_width: u32,
262 pub(crate) area_height: u32,
263 pub(crate) tick: u64,
264 pub(crate) focus_index: usize,
265 pub(crate) focus_count: usize,
266 pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
267 pub(crate) hook_cursor: usize,
268 prev_focus_count: usize,
269 scroll_count: usize,
270 prev_scroll_infos: Vec<(u32, u32)>,
271 prev_scroll_rects: Vec<Rect>,
272 interaction_count: usize,
273 pub(crate) prev_hit_map: Vec<Rect>,
274 pub(crate) group_stack: Vec<String>,
275 pub(crate) prev_group_rects: Vec<(String, Rect)>,
276 group_count: usize,
277 prev_focus_groups: Vec<Option<String>>,
278 _prev_focus_rects: Vec<(usize, Rect)>,
279 mouse_pos: Option<(u32, u32)>,
280 click_pos: Option<(u32, u32)>,
281 last_text_idx: Option<usize>,
282 overlay_depth: usize,
283 pub(crate) modal_active: bool,
284 prev_modal_active: bool,
285 pub(crate) clipboard_text: Option<String>,
286 debug: bool,
287 theme: Theme,
288 pub(crate) dark_mode: bool,
289 pub(crate) is_real_terminal: bool,
290 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
291 pub(crate) notification_queue: Vec<(String, ToastLevel, u64)>,
292}
293
294type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
295
296struct ContextSnapshot {
297 cmd_count: usize,
298 last_text_idx: Option<usize>,
299 focus_count: usize,
300 interaction_count: usize,
301 scroll_count: usize,
302 group_count: usize,
303 group_stack_len: usize,
304 overlay_depth: usize,
305 modal_active: bool,
306 hook_cursor: usize,
307 hook_states_len: usize,
308 dark_mode: bool,
309 deferred_draws_len: usize,
310 notification_queue_len: usize,
311}
312
313impl ContextSnapshot {
314 fn capture(ctx: &Context) -> Self {
315 Self {
316 cmd_count: ctx.commands.len(),
317 last_text_idx: ctx.last_text_idx,
318 focus_count: ctx.focus_count,
319 interaction_count: ctx.interaction_count,
320 scroll_count: ctx.scroll_count,
321 group_count: ctx.group_count,
322 group_stack_len: ctx.group_stack.len(),
323 overlay_depth: ctx.overlay_depth,
324 modal_active: ctx.modal_active,
325 hook_cursor: ctx.hook_cursor,
326 hook_states_len: ctx.hook_states.len(),
327 dark_mode: ctx.dark_mode,
328 deferred_draws_len: ctx.deferred_draws.len(),
329 notification_queue_len: ctx.notification_queue.len(),
330 }
331 }
332
333 fn restore(&self, ctx: &mut Context) {
334 ctx.commands.truncate(self.cmd_count);
335 ctx.last_text_idx = self.last_text_idx;
336 ctx.focus_count = self.focus_count;
337 ctx.interaction_count = self.interaction_count;
338 ctx.scroll_count = self.scroll_count;
339 ctx.group_count = self.group_count;
340 ctx.group_stack.truncate(self.group_stack_len);
341 ctx.overlay_depth = self.overlay_depth;
342 ctx.modal_active = self.modal_active;
343 ctx.hook_cursor = self.hook_cursor;
344 ctx.hook_states.truncate(self.hook_states_len);
345 ctx.dark_mode = self.dark_mode;
346 ctx.deferred_draws.truncate(self.deferred_draws_len);
347 ctx.notification_queue.truncate(self.notification_queue_len);
348 }
349}
350
351#[must_use = "ContainerBuilder does nothing until .col(), .row(), .line(), or .draw() is called"]
372pub struct ContainerBuilder<'a> {
373 ctx: &'a mut Context,
374 gap: u32,
375 align: Align,
376 justify: Justify,
377 border: Option<Border>,
378 border_sides: BorderSides,
379 border_style: Style,
380 bg: Option<Color>,
381 dark_bg: Option<Color>,
382 dark_border_style: Option<Style>,
383 group_hover_bg: Option<Color>,
384 group_hover_border_style: Option<Style>,
385 group_name: Option<String>,
386 padding: Padding,
387 margin: Margin,
388 constraints: Constraints,
389 title: Option<(String, Style)>,
390 grow: u16,
391 scroll_offset: Option<u32>,
392}
393
394#[derive(Debug, Clone, Copy)]
401struct CanvasPixel {
402 bits: u32,
403 color: Color,
404}
405
406#[derive(Debug, Clone)]
408struct CanvasLabel {
409 x: usize,
410 y: usize,
411 text: String,
412 color: Color,
413}
414
415#[derive(Debug, Clone)]
417struct CanvasLayer {
418 grid: Vec<Vec<CanvasPixel>>,
419 labels: Vec<CanvasLabel>,
420}
421
422pub struct CanvasContext {
423 layers: Vec<CanvasLayer>,
424 cols: usize,
425 rows: usize,
426 px_w: usize,
427 px_h: usize,
428 current_color: Color,
429}
430
431impl CanvasContext {
432 fn new(cols: usize, rows: usize) -> Self {
433 Self {
434 layers: vec![Self::new_layer(cols, rows)],
435 cols,
436 rows,
437 px_w: cols * 2,
438 px_h: rows * 4,
439 current_color: Color::Reset,
440 }
441 }
442
443 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
444 CanvasLayer {
445 grid: vec![
446 vec![
447 CanvasPixel {
448 bits: 0,
449 color: Color::Reset,
450 };
451 cols
452 ];
453 rows
454 ],
455 labels: Vec::new(),
456 }
457 }
458
459 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
460 self.layers.last_mut()
461 }
462
463 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
464 if x >= self.px_w || y >= self.px_h {
465 return;
466 }
467
468 let char_col = x / 2;
469 let char_row = y / 4;
470 let sub_col = x % 2;
471 let sub_row = y % 4;
472 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
473 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
474
475 let bit = if sub_col == 0 {
476 LEFT_BITS[sub_row]
477 } else {
478 RIGHT_BITS[sub_row]
479 };
480
481 if let Some(layer) = self.current_layer_mut() {
482 let cell = &mut layer.grid[char_row][char_col];
483 let new_bits = cell.bits | bit;
484 if new_bits != cell.bits {
485 cell.bits = new_bits;
486 cell.color = color;
487 }
488 }
489 }
490
491 fn dot_isize(&mut self, x: isize, y: isize) {
492 if x >= 0 && y >= 0 {
493 self.dot(x as usize, y as usize);
494 }
495 }
496
497 pub fn width(&self) -> usize {
499 self.px_w
500 }
501
502 pub fn height(&self) -> usize {
504 self.px_h
505 }
506
507 pub fn dot(&mut self, x: usize, y: usize) {
509 self.dot_with_color(x, y, self.current_color);
510 }
511
512 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
514 let (mut x, mut y) = (x0 as isize, y0 as isize);
515 let (x1, y1) = (x1 as isize, y1 as isize);
516 let dx = (x1 - x).abs();
517 let dy = -(y1 - y).abs();
518 let sx = if x < x1 { 1 } else { -1 };
519 let sy = if y < y1 { 1 } else { -1 };
520 let mut err = dx + dy;
521
522 loop {
523 self.dot_isize(x, y);
524 if x == x1 && y == y1 {
525 break;
526 }
527 let e2 = 2 * err;
528 if e2 >= dy {
529 err += dy;
530 x += sx;
531 }
532 if e2 <= dx {
533 err += dx;
534 y += sy;
535 }
536 }
537 }
538
539 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
541 if w == 0 || h == 0 {
542 return;
543 }
544
545 self.line(x, y, x + w.saturating_sub(1), y);
546 self.line(
547 x + w.saturating_sub(1),
548 y,
549 x + w.saturating_sub(1),
550 y + h.saturating_sub(1),
551 );
552 self.line(
553 x + w.saturating_sub(1),
554 y + h.saturating_sub(1),
555 x,
556 y + h.saturating_sub(1),
557 );
558 self.line(x, y + h.saturating_sub(1), x, y);
559 }
560
561 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
563 let mut x = r as isize;
564 let mut y: isize = 0;
565 let mut err: isize = 1 - x;
566 let (cx, cy) = (cx as isize, cy as isize);
567
568 while x >= y {
569 for &(dx, dy) in &[
570 (x, y),
571 (y, x),
572 (-x, y),
573 (-y, x),
574 (x, -y),
575 (y, -x),
576 (-x, -y),
577 (-y, -x),
578 ] {
579 let px = cx + dx;
580 let py = cy + dy;
581 self.dot_isize(px, py);
582 }
583
584 y += 1;
585 if err < 0 {
586 err += 2 * y + 1;
587 } else {
588 x -= 1;
589 err += 2 * (y - x) + 1;
590 }
591 }
592 }
593
594 pub fn set_color(&mut self, color: Color) {
596 self.current_color = color;
597 }
598
599 pub fn color(&self) -> Color {
601 self.current_color
602 }
603
604 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
606 if w == 0 || h == 0 {
607 return;
608 }
609
610 let x_end = x.saturating_add(w).min(self.px_w);
611 let y_end = y.saturating_add(h).min(self.px_h);
612 if x >= x_end || y >= y_end {
613 return;
614 }
615
616 for yy in y..y_end {
617 self.line(x, yy, x_end.saturating_sub(1), yy);
618 }
619 }
620
621 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
623 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
624 for y in (cy - r)..=(cy + r) {
625 let dy = y - cy;
626 let span_sq = (r * r - dy * dy).max(0);
627 let dx = (span_sq as f64).sqrt() as isize;
628 for x in (cx - dx)..=(cx + dx) {
629 self.dot_isize(x, y);
630 }
631 }
632 }
633
634 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
636 self.line(x0, y0, x1, y1);
637 self.line(x1, y1, x2, y2);
638 self.line(x2, y2, x0, y0);
639 }
640
641 pub fn filled_triangle(
643 &mut self,
644 x0: usize,
645 y0: usize,
646 x1: usize,
647 y1: usize,
648 x2: usize,
649 y2: usize,
650 ) {
651 let vertices = [
652 (x0 as isize, y0 as isize),
653 (x1 as isize, y1 as isize),
654 (x2 as isize, y2 as isize),
655 ];
656 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
657 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
658
659 for y in min_y..=max_y {
660 let mut intersections: Vec<f64> = Vec::new();
661
662 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
663 let (x_a, y_a) = vertices[edge.0];
664 let (x_b, y_b) = vertices[edge.1];
665 if y_a == y_b {
666 continue;
667 }
668
669 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
670 (x_a, y_a, x_b, y_b)
671 } else {
672 (x_b, y_b, x_a, y_a)
673 };
674
675 if y < y_start || y >= y_end {
676 continue;
677 }
678
679 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
680 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
681 }
682
683 intersections.sort_by(|a, b| a.total_cmp(b));
684 let mut i = 0usize;
685 while i + 1 < intersections.len() {
686 let x_start = intersections[i].ceil() as isize;
687 let x_end = intersections[i + 1].floor() as isize;
688 for x in x_start..=x_end {
689 self.dot_isize(x, y);
690 }
691 i += 2;
692 }
693 }
694
695 self.triangle(x0, y0, x1, y1, x2, y2);
696 }
697
698 pub fn points(&mut self, pts: &[(usize, usize)]) {
700 for &(x, y) in pts {
701 self.dot(x, y);
702 }
703 }
704
705 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
707 for window in pts.windows(2) {
708 if let [(x0, y0), (x1, y1)] = window {
709 self.line(*x0, *y0, *x1, *y1);
710 }
711 }
712 }
713
714 pub fn print(&mut self, x: usize, y: usize, text: &str) {
717 if text.is_empty() {
718 return;
719 }
720
721 let color = self.current_color;
722 if let Some(layer) = self.current_layer_mut() {
723 layer.labels.push(CanvasLabel {
724 x,
725 y,
726 text: text.to_string(),
727 color,
728 });
729 }
730 }
731
732 pub fn layer(&mut self) {
734 self.layers.push(Self::new_layer(self.cols, self.rows));
735 }
736
737 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
738 let mut final_grid = vec![
739 vec![
740 CanvasPixel {
741 bits: 0,
742 color: Color::Reset,
743 };
744 self.cols
745 ];
746 self.rows
747 ];
748 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
749 vec![vec![None; self.cols]; self.rows];
750
751 for layer in &self.layers {
752 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
753 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
754 let src = layer.grid[row][col];
755 if src.bits == 0 {
756 continue;
757 }
758
759 let merged = dst.bits | src.bits;
760 if merged != dst.bits {
761 dst.bits = merged;
762 dst.color = src.color;
763 }
764 }
765 }
766
767 for label in &layer.labels {
768 let row = label.y / 4;
769 if row >= self.rows {
770 continue;
771 }
772 let start_col = label.x / 2;
773 for (offset, ch) in label.text.chars().enumerate() {
774 let col = start_col + offset;
775 if col >= self.cols {
776 break;
777 }
778 labels_overlay[row][col] = Some((ch, label.color));
779 }
780 }
781 }
782
783 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
784 for row in 0..self.rows {
785 let mut segments: Vec<(String, Color)> = Vec::new();
786 let mut current_color: Option<Color> = None;
787 let mut current_text = String::new();
788
789 for col in 0..self.cols {
790 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
791 (label_ch, label_color)
792 } else {
793 let bits = final_grid[row][col].bits;
794 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
795 (ch, final_grid[row][col].color)
796 };
797
798 match current_color {
799 Some(c) if c == color => {
800 current_text.push(ch);
801 }
802 Some(c) => {
803 segments.push((std::mem::take(&mut current_text), c));
804 current_text.push(ch);
805 current_color = Some(color);
806 }
807 None => {
808 current_text.push(ch);
809 current_color = Some(color);
810 }
811 }
812 }
813
814 if let Some(color) = current_color {
815 segments.push((current_text, color));
816 }
817 lines.push(segments);
818 }
819
820 lines
821 }
822}
823
824impl<'a> ContainerBuilder<'a> {
825 pub fn apply(mut self, style: &ContainerStyle) -> Self {
830 if let Some(v) = style.border {
831 self.border = Some(v);
832 }
833 if let Some(v) = style.border_sides {
834 self.border_sides = v;
835 }
836 if let Some(v) = style.border_style {
837 self.border_style = v;
838 }
839 if let Some(v) = style.bg {
840 self.bg = Some(v);
841 }
842 if let Some(v) = style.dark_bg {
843 self.dark_bg = Some(v);
844 }
845 if let Some(v) = style.dark_border_style {
846 self.dark_border_style = Some(v);
847 }
848 if let Some(v) = style.padding {
849 self.padding = v;
850 }
851 if let Some(v) = style.margin {
852 self.margin = v;
853 }
854 if let Some(v) = style.gap {
855 self.gap = v;
856 }
857 if let Some(v) = style.grow {
858 self.grow = v;
859 }
860 if let Some(v) = style.align {
861 self.align = v;
862 }
863 if let Some(v) = style.justify {
864 self.justify = v;
865 }
866 if let Some(w) = style.w {
867 self.constraints.min_width = Some(w);
868 self.constraints.max_width = Some(w);
869 }
870 if let Some(h) = style.h {
871 self.constraints.min_height = Some(h);
872 self.constraints.max_height = Some(h);
873 }
874 if let Some(v) = style.min_w {
875 self.constraints.min_width = Some(v);
876 }
877 if let Some(v) = style.max_w {
878 self.constraints.max_width = Some(v);
879 }
880 if let Some(v) = style.min_h {
881 self.constraints.min_height = Some(v);
882 }
883 if let Some(v) = style.max_h {
884 self.constraints.max_height = Some(v);
885 }
886 if let Some(v) = style.w_pct {
887 self.constraints.width_pct = Some(v);
888 }
889 if let Some(v) = style.h_pct {
890 self.constraints.height_pct = Some(v);
891 }
892 self
893 }
894
895 pub fn border(mut self, border: Border) -> Self {
897 self.border = Some(border);
898 self
899 }
900
901 pub fn border_top(mut self, show: bool) -> Self {
903 self.border_sides.top = show;
904 self
905 }
906
907 pub fn border_right(mut self, show: bool) -> Self {
909 self.border_sides.right = show;
910 self
911 }
912
913 pub fn border_bottom(mut self, show: bool) -> Self {
915 self.border_sides.bottom = show;
916 self
917 }
918
919 pub fn border_left(mut self, show: bool) -> Self {
921 self.border_sides.left = show;
922 self
923 }
924
925 pub fn border_sides(mut self, sides: BorderSides) -> Self {
927 self.border_sides = sides;
928 self
929 }
930
931 pub fn rounded(self) -> Self {
933 self.border(Border::Rounded)
934 }
935
936 pub fn border_style(mut self, style: Style) -> Self {
938 self.border_style = style;
939 self
940 }
941
942 pub fn dark_border_style(mut self, style: Style) -> Self {
944 self.dark_border_style = Some(style);
945 self
946 }
947
948 pub fn bg(mut self, color: Color) -> Self {
949 self.bg = Some(color);
950 self
951 }
952
953 pub fn dark_bg(mut self, color: Color) -> Self {
955 self.dark_bg = Some(color);
956 self
957 }
958
959 pub fn group_hover_bg(mut self, color: Color) -> Self {
961 self.group_hover_bg = Some(color);
962 self
963 }
964
965 pub fn group_hover_border_style(mut self, style: Style) -> Self {
967 self.group_hover_border_style = Some(style);
968 self
969 }
970
971 pub fn p(self, value: u32) -> Self {
975 self.pad(value)
976 }
977
978 pub fn pad(mut self, value: u32) -> Self {
980 self.padding = Padding::all(value);
981 self
982 }
983
984 pub fn px(mut self, value: u32) -> Self {
986 self.padding.left = value;
987 self.padding.right = value;
988 self
989 }
990
991 pub fn py(mut self, value: u32) -> Self {
993 self.padding.top = value;
994 self.padding.bottom = value;
995 self
996 }
997
998 pub fn pt(mut self, value: u32) -> Self {
1000 self.padding.top = value;
1001 self
1002 }
1003
1004 pub fn pr(mut self, value: u32) -> Self {
1006 self.padding.right = value;
1007 self
1008 }
1009
1010 pub fn pb(mut self, value: u32) -> Self {
1012 self.padding.bottom = value;
1013 self
1014 }
1015
1016 pub fn pl(mut self, value: u32) -> Self {
1018 self.padding.left = value;
1019 self
1020 }
1021
1022 pub fn padding(mut self, padding: Padding) -> Self {
1024 self.padding = padding;
1025 self
1026 }
1027
1028 pub fn m(mut self, value: u32) -> Self {
1032 self.margin = Margin::all(value);
1033 self
1034 }
1035
1036 pub fn mx(mut self, value: u32) -> Self {
1038 self.margin.left = value;
1039 self.margin.right = value;
1040 self
1041 }
1042
1043 pub fn my(mut self, value: u32) -> Self {
1045 self.margin.top = value;
1046 self.margin.bottom = value;
1047 self
1048 }
1049
1050 pub fn mt(mut self, value: u32) -> Self {
1052 self.margin.top = value;
1053 self
1054 }
1055
1056 pub fn mr(mut self, value: u32) -> Self {
1058 self.margin.right = value;
1059 self
1060 }
1061
1062 pub fn mb(mut self, value: u32) -> Self {
1064 self.margin.bottom = value;
1065 self
1066 }
1067
1068 pub fn ml(mut self, value: u32) -> Self {
1070 self.margin.left = value;
1071 self
1072 }
1073
1074 pub fn margin(mut self, margin: Margin) -> Self {
1076 self.margin = margin;
1077 self
1078 }
1079
1080 pub fn w(mut self, value: u32) -> Self {
1084 self.constraints.min_width = Some(value);
1085 self.constraints.max_width = Some(value);
1086 self
1087 }
1088
1089 pub fn xs_w(self, value: u32) -> Self {
1096 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1097 if is_xs {
1098 self.w(value)
1099 } else {
1100 self
1101 }
1102 }
1103
1104 pub fn sm_w(self, value: u32) -> Self {
1106 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1107 if is_sm {
1108 self.w(value)
1109 } else {
1110 self
1111 }
1112 }
1113
1114 pub fn md_w(self, value: u32) -> Self {
1116 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1117 if is_md {
1118 self.w(value)
1119 } else {
1120 self
1121 }
1122 }
1123
1124 pub fn lg_w(self, value: u32) -> Self {
1126 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1127 if is_lg {
1128 self.w(value)
1129 } else {
1130 self
1131 }
1132 }
1133
1134 pub fn xl_w(self, value: u32) -> Self {
1136 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1137 if is_xl {
1138 self.w(value)
1139 } else {
1140 self
1141 }
1142 }
1143 pub fn w_at(self, bp: Breakpoint, value: u32) -> Self {
1144 if self.ctx.breakpoint() == bp {
1145 self.w(value)
1146 } else {
1147 self
1148 }
1149 }
1150
1151 pub fn h(mut self, value: u32) -> Self {
1153 self.constraints.min_height = Some(value);
1154 self.constraints.max_height = Some(value);
1155 self
1156 }
1157
1158 pub fn xs_h(self, value: u32) -> Self {
1160 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1161 if is_xs {
1162 self.h(value)
1163 } else {
1164 self
1165 }
1166 }
1167
1168 pub fn sm_h(self, value: u32) -> Self {
1170 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1171 if is_sm {
1172 self.h(value)
1173 } else {
1174 self
1175 }
1176 }
1177
1178 pub fn md_h(self, value: u32) -> Self {
1180 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1181 if is_md {
1182 self.h(value)
1183 } else {
1184 self
1185 }
1186 }
1187
1188 pub fn lg_h(self, value: u32) -> Self {
1190 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1191 if is_lg {
1192 self.h(value)
1193 } else {
1194 self
1195 }
1196 }
1197
1198 pub fn xl_h(self, value: u32) -> Self {
1200 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1201 if is_xl {
1202 self.h(value)
1203 } else {
1204 self
1205 }
1206 }
1207 pub fn h_at(self, bp: Breakpoint, value: u32) -> Self {
1208 if self.ctx.breakpoint() == bp {
1209 self.h(value)
1210 } else {
1211 self
1212 }
1213 }
1214
1215 pub fn min_w(mut self, value: u32) -> Self {
1217 self.constraints.min_width = Some(value);
1218 self
1219 }
1220
1221 pub fn xs_min_w(self, value: u32) -> Self {
1223 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1224 if is_xs {
1225 self.min_w(value)
1226 } else {
1227 self
1228 }
1229 }
1230
1231 pub fn sm_min_w(self, value: u32) -> Self {
1233 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1234 if is_sm {
1235 self.min_w(value)
1236 } else {
1237 self
1238 }
1239 }
1240
1241 pub fn md_min_w(self, value: u32) -> Self {
1243 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1244 if is_md {
1245 self.min_w(value)
1246 } else {
1247 self
1248 }
1249 }
1250
1251 pub fn lg_min_w(self, value: u32) -> Self {
1253 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1254 if is_lg {
1255 self.min_w(value)
1256 } else {
1257 self
1258 }
1259 }
1260
1261 pub fn xl_min_w(self, value: u32) -> Self {
1263 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1264 if is_xl {
1265 self.min_w(value)
1266 } else {
1267 self
1268 }
1269 }
1270 pub fn min_w_at(self, bp: Breakpoint, value: u32) -> Self {
1271 if self.ctx.breakpoint() == bp {
1272 self.min_w(value)
1273 } else {
1274 self
1275 }
1276 }
1277
1278 pub fn max_w(mut self, value: u32) -> Self {
1280 self.constraints.max_width = Some(value);
1281 self
1282 }
1283
1284 pub fn xs_max_w(self, value: u32) -> Self {
1286 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1287 if is_xs {
1288 self.max_w(value)
1289 } else {
1290 self
1291 }
1292 }
1293
1294 pub fn sm_max_w(self, value: u32) -> Self {
1296 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1297 if is_sm {
1298 self.max_w(value)
1299 } else {
1300 self
1301 }
1302 }
1303
1304 pub fn md_max_w(self, value: u32) -> Self {
1306 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1307 if is_md {
1308 self.max_w(value)
1309 } else {
1310 self
1311 }
1312 }
1313
1314 pub fn lg_max_w(self, value: u32) -> Self {
1316 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1317 if is_lg {
1318 self.max_w(value)
1319 } else {
1320 self
1321 }
1322 }
1323
1324 pub fn xl_max_w(self, value: u32) -> Self {
1326 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1327 if is_xl {
1328 self.max_w(value)
1329 } else {
1330 self
1331 }
1332 }
1333 pub fn max_w_at(self, bp: Breakpoint, value: u32) -> Self {
1334 if self.ctx.breakpoint() == bp {
1335 self.max_w(value)
1336 } else {
1337 self
1338 }
1339 }
1340
1341 pub fn min_h(mut self, value: u32) -> Self {
1343 self.constraints.min_height = Some(value);
1344 self
1345 }
1346
1347 pub fn max_h(mut self, value: u32) -> Self {
1349 self.constraints.max_height = Some(value);
1350 self
1351 }
1352
1353 pub fn min_width(mut self, value: u32) -> Self {
1355 self.constraints.min_width = Some(value);
1356 self
1357 }
1358
1359 pub fn max_width(mut self, value: u32) -> Self {
1361 self.constraints.max_width = Some(value);
1362 self
1363 }
1364
1365 pub fn min_height(mut self, value: u32) -> Self {
1367 self.constraints.min_height = Some(value);
1368 self
1369 }
1370
1371 pub fn max_height(mut self, value: u32) -> Self {
1373 self.constraints.max_height = Some(value);
1374 self
1375 }
1376
1377 pub fn w_pct(mut self, pct: u8) -> Self {
1379 self.constraints.width_pct = Some(pct.min(100));
1380 self
1381 }
1382
1383 pub fn h_pct(mut self, pct: u8) -> Self {
1385 self.constraints.height_pct = Some(pct.min(100));
1386 self
1387 }
1388
1389 pub fn constraints(mut self, constraints: Constraints) -> Self {
1391 self.constraints = constraints;
1392 self
1393 }
1394
1395 pub fn gap(mut self, gap: u32) -> Self {
1399 self.gap = gap;
1400 self
1401 }
1402
1403 pub fn xs_gap(self, value: u32) -> Self {
1405 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1406 if is_xs {
1407 self.gap(value)
1408 } else {
1409 self
1410 }
1411 }
1412
1413 pub fn sm_gap(self, value: u32) -> Self {
1415 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1416 if is_sm {
1417 self.gap(value)
1418 } else {
1419 self
1420 }
1421 }
1422
1423 pub fn md_gap(self, value: u32) -> Self {
1430 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1431 if is_md {
1432 self.gap(value)
1433 } else {
1434 self
1435 }
1436 }
1437
1438 pub fn lg_gap(self, value: u32) -> Self {
1440 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1441 if is_lg {
1442 self.gap(value)
1443 } else {
1444 self
1445 }
1446 }
1447
1448 pub fn xl_gap(self, value: u32) -> Self {
1450 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1451 if is_xl {
1452 self.gap(value)
1453 } else {
1454 self
1455 }
1456 }
1457
1458 pub fn gap_at(self, bp: Breakpoint, value: u32) -> Self {
1459 if self.ctx.breakpoint() == bp {
1460 self.gap(value)
1461 } else {
1462 self
1463 }
1464 }
1465
1466 pub fn grow(mut self, grow: u16) -> Self {
1468 self.grow = grow;
1469 self
1470 }
1471
1472 pub fn xs_grow(self, value: u16) -> Self {
1474 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1475 if is_xs {
1476 self.grow(value)
1477 } else {
1478 self
1479 }
1480 }
1481
1482 pub fn sm_grow(self, value: u16) -> Self {
1484 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1485 if is_sm {
1486 self.grow(value)
1487 } else {
1488 self
1489 }
1490 }
1491
1492 pub fn md_grow(self, value: u16) -> Self {
1494 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1495 if is_md {
1496 self.grow(value)
1497 } else {
1498 self
1499 }
1500 }
1501
1502 pub fn lg_grow(self, value: u16) -> Self {
1504 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1505 if is_lg {
1506 self.grow(value)
1507 } else {
1508 self
1509 }
1510 }
1511
1512 pub fn xl_grow(self, value: u16) -> Self {
1514 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1515 if is_xl {
1516 self.grow(value)
1517 } else {
1518 self
1519 }
1520 }
1521 pub fn grow_at(self, bp: Breakpoint, value: u16) -> Self {
1522 if self.ctx.breakpoint() == bp {
1523 self.grow(value)
1524 } else {
1525 self
1526 }
1527 }
1528
1529 pub fn xs_p(self, value: u32) -> Self {
1531 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1532 if is_xs {
1533 self.p(value)
1534 } else {
1535 self
1536 }
1537 }
1538
1539 pub fn sm_p(self, value: u32) -> Self {
1541 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1542 if is_sm {
1543 self.p(value)
1544 } else {
1545 self
1546 }
1547 }
1548
1549 pub fn md_p(self, value: u32) -> Self {
1551 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1552 if is_md {
1553 self.p(value)
1554 } else {
1555 self
1556 }
1557 }
1558
1559 pub fn lg_p(self, value: u32) -> Self {
1561 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1562 if is_lg {
1563 self.p(value)
1564 } else {
1565 self
1566 }
1567 }
1568
1569 pub fn xl_p(self, value: u32) -> Self {
1571 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1572 if is_xl {
1573 self.p(value)
1574 } else {
1575 self
1576 }
1577 }
1578 pub fn p_at(self, bp: Breakpoint, value: u32) -> Self {
1579 if self.ctx.breakpoint() == bp {
1580 self.p(value)
1581 } else {
1582 self
1583 }
1584 }
1585
1586 pub fn align(mut self, align: Align) -> Self {
1590 self.align = align;
1591 self
1592 }
1593
1594 pub fn center(self) -> Self {
1596 self.align(Align::Center)
1597 }
1598
1599 pub fn justify(mut self, justify: Justify) -> Self {
1601 self.justify = justify;
1602 self
1603 }
1604
1605 pub fn space_between(self) -> Self {
1607 self.justify(Justify::SpaceBetween)
1608 }
1609
1610 pub fn space_around(self) -> Self {
1612 self.justify(Justify::SpaceAround)
1613 }
1614
1615 pub fn space_evenly(self) -> Self {
1617 self.justify(Justify::SpaceEvenly)
1618 }
1619
1620 pub fn title(self, title: impl Into<String>) -> Self {
1624 self.title_styled(title, Style::new())
1625 }
1626
1627 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1629 self.title = Some((title.into(), style));
1630 self
1631 }
1632
1633 pub fn scroll_offset(mut self, offset: u32) -> Self {
1637 self.scroll_offset = Some(offset);
1638 self
1639 }
1640
1641 fn group_name(mut self, name: String) -> Self {
1642 self.group_name = Some(name);
1643 self
1644 }
1645
1646 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1651 self.finish(Direction::Column, f)
1652 }
1653
1654 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1659 self.finish(Direction::Row, f)
1660 }
1661
1662 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1667 self.gap = 0;
1668 self.finish(Direction::Row, f)
1669 }
1670
1671 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1686 let draw_id = self.ctx.deferred_draws.len();
1687 self.ctx.deferred_draws.push(Some(Box::new(f)));
1688 self.ctx.interaction_count += 1;
1689 self.ctx.commands.push(Command::RawDraw {
1690 draw_id,
1691 constraints: self.constraints,
1692 grow: self.grow,
1693 margin: self.margin,
1694 });
1695 }
1696
1697 fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1698 let interaction_id = self.ctx.interaction_count;
1699 self.ctx.interaction_count += 1;
1700
1701 let in_hovered_group = self
1702 .group_name
1703 .as_ref()
1704 .map(|name| self.ctx.is_group_hovered(name))
1705 .unwrap_or(false)
1706 || self
1707 .ctx
1708 .group_stack
1709 .last()
1710 .map(|name| self.ctx.is_group_hovered(name))
1711 .unwrap_or(false);
1712 let in_focused_group = self
1713 .group_name
1714 .as_ref()
1715 .map(|name| self.ctx.is_group_focused(name))
1716 .unwrap_or(false)
1717 || self
1718 .ctx
1719 .group_stack
1720 .last()
1721 .map(|name| self.ctx.is_group_focused(name))
1722 .unwrap_or(false);
1723
1724 let resolved_bg = if self.ctx.dark_mode {
1725 self.dark_bg.or(self.bg)
1726 } else {
1727 self.bg
1728 };
1729 let resolved_border_style = if self.ctx.dark_mode {
1730 self.dark_border_style.unwrap_or(self.border_style)
1731 } else {
1732 self.border_style
1733 };
1734 let bg_color = if in_hovered_group || in_focused_group {
1735 self.group_hover_bg.or(resolved_bg)
1736 } else {
1737 resolved_bg
1738 };
1739 let border_style = if in_hovered_group || in_focused_group {
1740 self.group_hover_border_style
1741 .unwrap_or(resolved_border_style)
1742 } else {
1743 resolved_border_style
1744 };
1745 let group_name = self.group_name.take();
1746 let is_group_container = group_name.is_some();
1747
1748 if let Some(scroll_offset) = self.scroll_offset {
1749 self.ctx.commands.push(Command::BeginScrollable {
1750 grow: self.grow,
1751 border: self.border,
1752 border_sides: self.border_sides,
1753 border_style,
1754 padding: self.padding,
1755 margin: self.margin,
1756 constraints: self.constraints,
1757 title: self.title,
1758 scroll_offset,
1759 });
1760 } else {
1761 self.ctx.commands.push(Command::BeginContainer {
1762 direction,
1763 gap: self.gap,
1764 align: self.align,
1765 justify: self.justify,
1766 border: self.border,
1767 border_sides: self.border_sides,
1768 border_style,
1769 bg_color,
1770 padding: self.padding,
1771 margin: self.margin,
1772 constraints: self.constraints,
1773 title: self.title,
1774 grow: self.grow,
1775 group_name,
1776 });
1777 }
1778 f(self.ctx);
1779 self.ctx.commands.push(Command::EndContainer);
1780 self.ctx.last_text_idx = None;
1781
1782 if is_group_container {
1783 self.ctx.group_stack.pop();
1784 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1785 }
1786
1787 self.ctx.response_for(interaction_id)
1788 }
1789}
1790
1791impl Context {
1792 pub(crate) fn new(
1793 events: Vec<Event>,
1794 width: u32,
1795 height: u32,
1796 state: &mut FrameState,
1797 theme: Theme,
1798 ) -> Self {
1799 let consumed = vec![false; events.len()];
1800
1801 let mut mouse_pos = state.last_mouse_pos;
1802 let mut click_pos = None;
1803 for event in &events {
1804 if let Event::Mouse(mouse) = event {
1805 mouse_pos = Some((mouse.x, mouse.y));
1806 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1807 click_pos = Some((mouse.x, mouse.y));
1808 }
1809 }
1810 }
1811
1812 let mut focus_index = state.focus_index;
1813 if let Some((mx, my)) = click_pos {
1814 let mut best: Option<(usize, u64)> = None;
1815 for &(fid, rect) in &state.prev_focus_rects {
1816 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1817 let area = rect.width as u64 * rect.height as u64;
1818 if best.map_or(true, |(_, ba)| area < ba) {
1819 best = Some((fid, area));
1820 }
1821 }
1822 }
1823 if let Some((fid, _)) = best {
1824 focus_index = fid;
1825 }
1826 }
1827
1828 Self {
1829 commands: Vec::new(),
1830 events,
1831 consumed,
1832 should_quit: false,
1833 area_width: width,
1834 area_height: height,
1835 tick: state.tick,
1836 focus_index,
1837 focus_count: 0,
1838 hook_states: std::mem::take(&mut state.hook_states),
1839 hook_cursor: 0,
1840 prev_focus_count: state.prev_focus_count,
1841 scroll_count: 0,
1842 prev_scroll_infos: std::mem::take(&mut state.prev_scroll_infos),
1843 prev_scroll_rects: std::mem::take(&mut state.prev_scroll_rects),
1844 interaction_count: 0,
1845 prev_hit_map: std::mem::take(&mut state.prev_hit_map),
1846 group_stack: Vec::new(),
1847 prev_group_rects: std::mem::take(&mut state.prev_group_rects),
1848 group_count: 0,
1849 prev_focus_groups: std::mem::take(&mut state.prev_focus_groups),
1850 _prev_focus_rects: std::mem::take(&mut state.prev_focus_rects),
1851 mouse_pos,
1852 click_pos,
1853 last_text_idx: None,
1854 overlay_depth: 0,
1855 modal_active: false,
1856 prev_modal_active: state.prev_modal_active,
1857 clipboard_text: None,
1858 debug: state.debug_mode,
1859 theme,
1860 dark_mode: theme.is_dark,
1861 is_real_terminal: false,
1862 deferred_draws: Vec::new(),
1863 notification_queue: std::mem::take(&mut state.notification_queue),
1864 }
1865 }
1866
1867 pub(crate) fn process_focus_keys(&mut self) {
1868 for (i, event) in self.events.iter().enumerate() {
1869 if let Event::Key(key) = event {
1870 if key.kind != KeyEventKind::Press {
1871 continue;
1872 }
1873 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1874 if self.prev_focus_count > 0 {
1875 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1876 }
1877 self.consumed[i] = true;
1878 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1879 || key.code == KeyCode::BackTab
1880 {
1881 if self.prev_focus_count > 0 {
1882 self.focus_index = if self.focus_index == 0 {
1883 self.prev_focus_count - 1
1884 } else {
1885 self.focus_index - 1
1886 };
1887 }
1888 self.consumed[i] = true;
1889 }
1890 }
1891 }
1892 }
1893
1894 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1898 w.ui(self)
1899 }
1900
1901 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1916 self.error_boundary_with(f, |ui, msg| {
1917 ui.styled(
1918 format!("⚠ Error: {msg}"),
1919 Style::new().fg(ui.theme.error).bold(),
1920 );
1921 });
1922 }
1923
1924 pub fn error_boundary_with(
1944 &mut self,
1945 f: impl FnOnce(&mut Context),
1946 fallback: impl FnOnce(&mut Context, String),
1947 ) {
1948 let snapshot = ContextSnapshot::capture(self);
1949
1950 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1951 f(self);
1952 }));
1953
1954 match result {
1955 Ok(()) => {}
1956 Err(panic_info) => {
1957 if self.is_real_terminal {
1958 let _ = crossterm::terminal::enable_raw_mode();
1959 let _ = crossterm::execute!(
1960 std::io::stdout(),
1961 crossterm::terminal::EnterAlternateScreen
1962 );
1963 }
1964
1965 snapshot.restore(self);
1966
1967 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1968 (*s).to_string()
1969 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1970 s.clone()
1971 } else {
1972 "widget panicked".to_string()
1973 };
1974
1975 fallback(self, msg);
1976 }
1977 }
1978 }
1979
1980 pub fn interaction(&mut self) -> Response {
1986 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1987 return Response::none();
1988 }
1989 let id = self.interaction_count;
1990 self.interaction_count += 1;
1991 self.response_for(id)
1992 }
1993
1994 pub fn register_focusable(&mut self) -> bool {
1999 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2000 return false;
2001 }
2002 let id = self.focus_count;
2003 self.focus_count += 1;
2004 self.commands.push(Command::FocusMarker(id));
2005 if self.prev_focus_count == 0 {
2006 return true;
2007 }
2008 self.focus_index % self.prev_focus_count == id
2009 }
2010
2011 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
2029 let idx = self.hook_cursor;
2030 self.hook_cursor += 1;
2031
2032 if idx >= self.hook_states.len() {
2033 self.hook_states.push(Box::new(init()));
2034 }
2035
2036 State {
2037 idx,
2038 _marker: std::marker::PhantomData,
2039 }
2040 }
2041
2042 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
2050 &mut self,
2051 deps: &D,
2052 compute: impl FnOnce(&D) -> T,
2053 ) -> &T {
2054 let idx = self.hook_cursor;
2055 self.hook_cursor += 1;
2056
2057 let should_recompute = if idx >= self.hook_states.len() {
2058 true
2059 } else {
2060 let (stored_deps, _) = self.hook_states[idx]
2061 .downcast_ref::<(D, T)>()
2062 .unwrap_or_else(|| {
2063 panic!(
2064 "use_memo type mismatch at hook index {} — expected {}",
2065 idx,
2066 std::any::type_name::<D>()
2067 )
2068 });
2069 stored_deps != deps
2070 };
2071
2072 if should_recompute {
2073 let value = compute(deps);
2074 let slot = Box::new((deps.clone(), value));
2075 if idx < self.hook_states.len() {
2076 self.hook_states[idx] = slot;
2077 } else {
2078 self.hook_states.push(slot);
2079 }
2080 }
2081
2082 let (_, value) = self.hook_states[idx]
2083 .downcast_ref::<(D, T)>()
2084 .unwrap_or_else(|| {
2085 panic!(
2086 "use_memo type mismatch at hook index {} — expected {}",
2087 idx,
2088 std::any::type_name::<D>()
2089 )
2090 });
2091 value
2092 }
2093
2094 pub fn light_dark(&self, light: Color, dark: Color) -> Color {
2096 if self.theme.is_dark {
2097 dark
2098 } else {
2099 light
2100 }
2101 }
2102
2103 pub fn notify(&mut self, message: &str, level: ToastLevel) {
2113 let tick = self.tick;
2114 self.notification_queue
2115 .push((message.to_string(), level, tick));
2116 }
2117
2118 pub(crate) fn render_notifications(&mut self) {
2119 self.notification_queue
2120 .retain(|(_, _, created)| self.tick.saturating_sub(*created) < 180);
2121 if self.notification_queue.is_empty() {
2122 return;
2123 }
2124
2125 let items: Vec<(String, Color)> = self
2126 .notification_queue
2127 .iter()
2128 .rev()
2129 .map(|(message, level, _)| {
2130 let color = match level {
2131 ToastLevel::Info => self.theme.primary,
2132 ToastLevel::Success => self.theme.success,
2133 ToastLevel::Warning => self.theme.warning,
2134 ToastLevel::Error => self.theme.error,
2135 };
2136 (message.clone(), color)
2137 })
2138 .collect();
2139
2140 self.overlay(|ui| {
2141 ui.row(|ui| {
2142 ui.spacer();
2143 ui.col(|ui| {
2144 for (message, color) in &items {
2145 ui.styled(format!("● {message}"), Style::new().fg(*color));
2146 }
2147 });
2148 });
2149 });
2150 }
2151}
2152
2153mod widgets_display;
2154mod widgets_input;
2155mod widgets_interactive;
2156mod widgets_viz;
2157
2158#[inline]
2159fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2160 if char_index == 0 {
2161 return 0;
2162 }
2163 value
2164 .char_indices()
2165 .nth(char_index)
2166 .map_or(value.len(), |(idx, _)| idx)
2167}
2168
2169fn format_token_count(count: usize) -> String {
2170 if count >= 1_000_000 {
2171 format!("{:.1}M", count as f64 / 1_000_000.0)
2172 } else if count >= 1_000 {
2173 format!("{:.1}k", count as f64 / 1_000.0)
2174 } else {
2175 format!("{count}")
2176 }
2177}
2178
2179fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2180 let mut parts: Vec<String> = Vec::new();
2181 for (i, width) in widths.iter().enumerate() {
2182 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2183 let cell_width = UnicodeWidthStr::width(cell) as u32;
2184 let padding = (*width).saturating_sub(cell_width) as usize;
2185 parts.push(format!("{cell}{}", " ".repeat(padding)));
2186 }
2187 parts.join(separator)
2188}
2189
2190fn table_visible_len(state: &TableState) -> usize {
2191 if state.page_size == 0 {
2192 return state.visible_indices().len();
2193 }
2194
2195 let start = state
2196 .page
2197 .saturating_mul(state.page_size)
2198 .min(state.visible_indices().len());
2199 let end = (start + state.page_size).min(state.visible_indices().len());
2200 end.saturating_sub(start)
2201}
2202
2203pub(crate) fn handle_vertical_nav(
2204 selected: &mut usize,
2205 max_index: usize,
2206 key_code: KeyCode,
2207) -> bool {
2208 match key_code {
2209 KeyCode::Up | KeyCode::Char('k') => {
2210 if *selected > 0 {
2211 *selected -= 1;
2212 true
2213 } else {
2214 false
2215 }
2216 }
2217 KeyCode::Down | KeyCode::Char('j') => {
2218 if *selected < max_index {
2219 *selected += 1;
2220 true
2221 } else {
2222 false
2223 }
2224 }
2225 _ => false,
2226 }
2227}
2228
2229fn format_compact_number(value: f64) -> String {
2230 if value.fract().abs() < f64::EPSILON {
2231 return format!("{value:.0}");
2232 }
2233
2234 let mut s = format!("{value:.2}");
2235 while s.contains('.') && s.ends_with('0') {
2236 s.pop();
2237 }
2238 if s.ends_with('.') {
2239 s.pop();
2240 }
2241 s
2242}
2243
2244fn center_text(text: &str, width: usize) -> String {
2245 let text_width = UnicodeWidthStr::width(text);
2246 if text_width >= width {
2247 return text.to_string();
2248 }
2249
2250 let total = width - text_width;
2251 let left = total / 2;
2252 let right = total - left;
2253 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2254}
2255
2256struct TextareaVLine {
2257 logical_row: usize,
2258 char_start: usize,
2259 char_count: usize,
2260}
2261
2262fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2263 let mut out = Vec::new();
2264 for (row, line) in lines.iter().enumerate() {
2265 if line.is_empty() || wrap_width == u32::MAX {
2266 out.push(TextareaVLine {
2267 logical_row: row,
2268 char_start: 0,
2269 char_count: line.chars().count(),
2270 });
2271 continue;
2272 }
2273 let mut seg_start = 0usize;
2274 let mut seg_chars = 0usize;
2275 let mut seg_width = 0u32;
2276 for (idx, ch) in line.chars().enumerate() {
2277 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2278 if seg_width + cw > wrap_width && seg_chars > 0 {
2279 out.push(TextareaVLine {
2280 logical_row: row,
2281 char_start: seg_start,
2282 char_count: seg_chars,
2283 });
2284 seg_start = idx;
2285 seg_chars = 0;
2286 seg_width = 0;
2287 }
2288 seg_chars += 1;
2289 seg_width += cw;
2290 }
2291 out.push(TextareaVLine {
2292 logical_row: row,
2293 char_start: seg_start,
2294 char_count: seg_chars,
2295 });
2296 }
2297 out
2298}
2299
2300fn textarea_logical_to_visual(
2301 vlines: &[TextareaVLine],
2302 logical_row: usize,
2303 logical_col: usize,
2304) -> (usize, usize) {
2305 for (i, vl) in vlines.iter().enumerate() {
2306 if vl.logical_row != logical_row {
2307 continue;
2308 }
2309 let seg_end = vl.char_start + vl.char_count;
2310 if logical_col >= vl.char_start && logical_col < seg_end {
2311 return (i, logical_col - vl.char_start);
2312 }
2313 if logical_col == seg_end {
2314 let is_last_seg = vlines
2315 .get(i + 1)
2316 .map_or(true, |next| next.logical_row != logical_row);
2317 if is_last_seg {
2318 return (i, logical_col - vl.char_start);
2319 }
2320 }
2321 }
2322 (vlines.len().saturating_sub(1), 0)
2323}
2324
2325fn textarea_visual_to_logical(
2326 vlines: &[TextareaVLine],
2327 visual_row: usize,
2328 visual_col: usize,
2329) -> (usize, usize) {
2330 if let Some(vl) = vlines.get(visual_row) {
2331 let logical_col = vl.char_start + visual_col.min(vl.char_count);
2332 (vl.logical_row, logical_col)
2333 } else {
2334 (0, 0)
2335 }
2336}
2337
2338fn open_url(url: &str) -> std::io::Result<()> {
2339 #[cfg(target_os = "macos")]
2340 {
2341 std::process::Command::new("open").arg(url).spawn()?;
2342 }
2343 #[cfg(target_os = "linux")]
2344 {
2345 std::process::Command::new("xdg-open").arg(url).spawn()?;
2346 }
2347 #[cfg(target_os = "windows")]
2348 {
2349 std::process::Command::new("cmd")
2350 .args(["/c", "start", "", url])
2351 .spawn()?;
2352 }
2353 Ok(())
2354}
2355
2356#[cfg(test)]
2357mod tests {
2358 use super::*;
2359 use crate::test_utils::TestBackend;
2360
2361 #[test]
2362 fn use_memo_type_mismatch_includes_hook_index_and_expected_type() {
2363 let mut state = FrameState::default();
2364 let mut ctx = Context::new(Vec::new(), 20, 5, &mut state, Theme::dark());
2365 ctx.hook_states.push(Box::new(42u32));
2366 ctx.hook_cursor = 0;
2367
2368 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2369 let deps = 1u8;
2370 let _ = ctx.use_memo(&deps, |_| 7u8);
2371 }))
2372 .expect_err("use_memo should panic on type mismatch");
2373
2374 let message = panic_message(panic);
2375 assert!(
2376 message.contains("use_memo type mismatch at hook index 0"),
2377 "panic message should include hook index, got: {message}"
2378 );
2379 assert!(
2380 message.contains(std::any::type_name::<u8>()),
2381 "panic message should include expected type, got: {message}"
2382 );
2383 }
2384
2385 #[test]
2386 fn light_dark_uses_current_theme_mode() {
2387 let mut dark_backend = TestBackend::new(10, 2);
2388 dark_backend.render(|ui| {
2389 let color = ui.light_dark(Color::Red, Color::Blue);
2390 ui.text("X").fg(color);
2391 });
2392 assert_eq!(dark_backend.buffer().get(0, 0).style.fg, Some(Color::Blue));
2393
2394 let mut light_backend = TestBackend::new(10, 2);
2395 light_backend.render(|ui| {
2396 ui.set_theme(Theme::light());
2397 let color = ui.light_dark(Color::Red, Color::Blue);
2398 ui.text("X").fg(color);
2399 });
2400 assert_eq!(light_backend.buffer().get(0, 0).style.fg, Some(Color::Red));
2401 }
2402
2403 fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
2404 if let Some(s) = panic.downcast_ref::<String>() {
2405 s.clone()
2406 } else if let Some(s) = panic.downcast_ref::<&str>() {
2407 (*s).to_string()
2408 } else {
2409 "<non-string panic payload>".to_string()
2410 }
2411 }
2412}