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