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