1use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
2use crate::layout::{Command, Direction};
3use crate::rect::Rect;
4use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
5use crate::widgets::{
6 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
7 ToastLevel, ToastState,
8};
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, Copy, Default)]
17pub struct Response {
18 pub clicked: bool,
20 pub hovered: bool,
22}
23
24pub trait Widget {
86 type Response;
89
90 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
96}
97
98pub struct Context {
114 pub(crate) commands: Vec<Command>,
115 pub(crate) events: Vec<Event>,
116 pub(crate) consumed: Vec<bool>,
117 pub(crate) should_quit: bool,
118 pub(crate) area_width: u32,
119 pub(crate) area_height: u32,
120 pub(crate) tick: u64,
121 pub(crate) focus_index: usize,
122 pub(crate) focus_count: usize,
123 prev_focus_count: usize,
124 scroll_count: usize,
125 prev_scroll_infos: Vec<(u32, u32)>,
126 interaction_count: usize,
127 prev_hit_map: Vec<Rect>,
128 mouse_pos: Option<(u32, u32)>,
129 click_pos: Option<(u32, u32)>,
130 last_mouse_pos: Option<(u32, u32)>,
131 last_text_idx: Option<usize>,
132 debug: bool,
133 theme: Theme,
134}
135
136#[must_use = "configure and finalize with .col() or .row()"]
157pub struct ContainerBuilder<'a> {
158 ctx: &'a mut Context,
159 gap: u32,
160 align: Align,
161 border: Option<Border>,
162 border_style: Style,
163 padding: Padding,
164 margin: Margin,
165 constraints: Constraints,
166 title: Option<(String, Style)>,
167 grow: u16,
168 scroll_offset: Option<u32>,
169}
170
171pub struct CanvasContext {
177 grid: Vec<Vec<u32>>,
178 px_w: usize,
179 px_h: usize,
180}
181
182impl CanvasContext {
183 fn new(cols: usize, rows: usize) -> Self {
184 Self {
185 grid: vec![vec![0u32; cols]; rows],
186 px_w: cols * 2,
187 px_h: rows * 4,
188 }
189 }
190
191 pub fn width(&self) -> usize {
193 self.px_w
194 }
195
196 pub fn height(&self) -> usize {
198 self.px_h
199 }
200
201 pub fn dot(&mut self, x: usize, y: usize) {
203 if x >= self.px_w || y >= self.px_h {
204 return;
205 }
206
207 let char_col = x / 2;
208 let char_row = y / 4;
209 let sub_col = x % 2;
210 let sub_row = y % 4;
211 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
212 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
213
214 self.grid[char_row][char_col] |= if sub_col == 0 {
215 LEFT_BITS[sub_row]
216 } else {
217 RIGHT_BITS[sub_row]
218 };
219 }
220
221 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
223 let (mut x, mut y) = (x0 as isize, y0 as isize);
224 let (x1, y1) = (x1 as isize, y1 as isize);
225 let dx = (x1 - x).abs();
226 let dy = -(y1 - y).abs();
227 let sx = if x < x1 { 1 } else { -1 };
228 let sy = if y < y1 { 1 } else { -1 };
229 let mut err = dx + dy;
230
231 loop {
232 if x >= 0 && y >= 0 {
233 self.dot(x as usize, y as usize);
234 }
235 if x == x1 && y == y1 {
236 break;
237 }
238 let e2 = 2 * err;
239 if e2 >= dy {
240 err += dy;
241 x += sx;
242 }
243 if e2 <= dx {
244 err += dx;
245 y += sy;
246 }
247 }
248 }
249
250 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
252 if w == 0 || h == 0 {
253 return;
254 }
255
256 self.line(x, y, x + w.saturating_sub(1), y);
257 self.line(
258 x + w.saturating_sub(1),
259 y,
260 x + w.saturating_sub(1),
261 y + h.saturating_sub(1),
262 );
263 self.line(
264 x + w.saturating_sub(1),
265 y + h.saturating_sub(1),
266 x,
267 y + h.saturating_sub(1),
268 );
269 self.line(x, y + h.saturating_sub(1), x, y);
270 }
271
272 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
274 let mut x = r as isize;
275 let mut y: isize = 0;
276 let mut err: isize = 1 - x;
277 let (cx, cy) = (cx as isize, cy as isize);
278
279 while x >= y {
280 for &(dx, dy) in &[
281 (x, y),
282 (y, x),
283 (-x, y),
284 (-y, x),
285 (x, -y),
286 (y, -x),
287 (-x, -y),
288 (-y, -x),
289 ] {
290 let px = cx + dx;
291 let py = cy + dy;
292 if px >= 0 && py >= 0 {
293 self.dot(px as usize, py as usize);
294 }
295 }
296
297 y += 1;
298 if err < 0 {
299 err += 2 * y + 1;
300 } else {
301 x -= 1;
302 err += 2 * (y - x) + 1;
303 }
304 }
305 }
306
307 fn render(&self) -> Vec<String> {
308 self.grid
309 .iter()
310 .map(|row| {
311 row.iter()
312 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
313 .collect()
314 })
315 .collect()
316 }
317}
318
319impl<'a> ContainerBuilder<'a> {
320 pub fn border(mut self, border: Border) -> Self {
324 self.border = Some(border);
325 self
326 }
327
328 pub fn rounded(self) -> Self {
330 self.border(Border::Rounded)
331 }
332
333 pub fn border_style(mut self, style: Style) -> Self {
335 self.border_style = style;
336 self
337 }
338
339 pub fn p(self, value: u32) -> Self {
343 self.pad(value)
344 }
345
346 pub fn pad(mut self, value: u32) -> Self {
348 self.padding = Padding::all(value);
349 self
350 }
351
352 pub fn px(mut self, value: u32) -> Self {
354 self.padding.left = value;
355 self.padding.right = value;
356 self
357 }
358
359 pub fn py(mut self, value: u32) -> Self {
361 self.padding.top = value;
362 self.padding.bottom = value;
363 self
364 }
365
366 pub fn pt(mut self, value: u32) -> Self {
368 self.padding.top = value;
369 self
370 }
371
372 pub fn pr(mut self, value: u32) -> Self {
374 self.padding.right = value;
375 self
376 }
377
378 pub fn pb(mut self, value: u32) -> Self {
380 self.padding.bottom = value;
381 self
382 }
383
384 pub fn pl(mut self, value: u32) -> Self {
386 self.padding.left = value;
387 self
388 }
389
390 pub fn padding(mut self, padding: Padding) -> Self {
392 self.padding = padding;
393 self
394 }
395
396 pub fn m(mut self, value: u32) -> Self {
400 self.margin = Margin::all(value);
401 self
402 }
403
404 pub fn mx(mut self, value: u32) -> Self {
406 self.margin.left = value;
407 self.margin.right = value;
408 self
409 }
410
411 pub fn my(mut self, value: u32) -> Self {
413 self.margin.top = value;
414 self.margin.bottom = value;
415 self
416 }
417
418 pub fn mt(mut self, value: u32) -> Self {
420 self.margin.top = value;
421 self
422 }
423
424 pub fn mr(mut self, value: u32) -> Self {
426 self.margin.right = value;
427 self
428 }
429
430 pub fn mb(mut self, value: u32) -> Self {
432 self.margin.bottom = value;
433 self
434 }
435
436 pub fn ml(mut self, value: u32) -> Self {
438 self.margin.left = value;
439 self
440 }
441
442 pub fn margin(mut self, margin: Margin) -> Self {
444 self.margin = margin;
445 self
446 }
447
448 pub fn w(mut self, value: u32) -> Self {
452 self.constraints.min_width = Some(value);
453 self.constraints.max_width = Some(value);
454 self
455 }
456
457 pub fn h(mut self, value: u32) -> Self {
459 self.constraints.min_height = Some(value);
460 self.constraints.max_height = Some(value);
461 self
462 }
463
464 pub fn min_w(mut self, value: u32) -> Self {
466 self.constraints.min_width = Some(value);
467 self
468 }
469
470 pub fn max_w(mut self, value: u32) -> Self {
472 self.constraints.max_width = Some(value);
473 self
474 }
475
476 pub fn min_h(mut self, value: u32) -> Self {
478 self.constraints.min_height = Some(value);
479 self
480 }
481
482 pub fn max_h(mut self, value: u32) -> Self {
484 self.constraints.max_height = Some(value);
485 self
486 }
487
488 pub fn min_width(mut self, value: u32) -> Self {
490 self.constraints.min_width = Some(value);
491 self
492 }
493
494 pub fn max_width(mut self, value: u32) -> Self {
496 self.constraints.max_width = Some(value);
497 self
498 }
499
500 pub fn min_height(mut self, value: u32) -> Self {
502 self.constraints.min_height = Some(value);
503 self
504 }
505
506 pub fn max_height(mut self, value: u32) -> Self {
508 self.constraints.max_height = Some(value);
509 self
510 }
511
512 pub fn constraints(mut self, constraints: Constraints) -> Self {
514 self.constraints = constraints;
515 self
516 }
517
518 pub fn gap(mut self, gap: u32) -> Self {
522 self.gap = gap;
523 self
524 }
525
526 pub fn grow(mut self, grow: u16) -> Self {
528 self.grow = grow;
529 self
530 }
531
532 pub fn align(mut self, align: Align) -> Self {
536 self.align = align;
537 self
538 }
539
540 pub fn center(self) -> Self {
542 self.align(Align::Center)
543 }
544
545 pub fn title(self, title: impl Into<String>) -> Self {
549 self.title_styled(title, Style::new())
550 }
551
552 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
554 self.title = Some((title.into(), style));
555 self
556 }
557
558 pub fn scroll_offset(mut self, offset: u32) -> Self {
562 self.scroll_offset = Some(offset);
563 self
564 }
565
566 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
571 self.finish(Direction::Column, f)
572 }
573
574 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
579 self.finish(Direction::Row, f)
580 }
581
582 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
583 let interaction_id = self.ctx.interaction_count;
584 self.ctx.interaction_count += 1;
585
586 if let Some(scroll_offset) = self.scroll_offset {
587 self.ctx.commands.push(Command::BeginScrollable {
588 grow: self.grow,
589 border: self.border,
590 border_style: self.border_style,
591 padding: self.padding,
592 margin: self.margin,
593 constraints: self.constraints,
594 title: self.title,
595 scroll_offset,
596 });
597 } else {
598 self.ctx.commands.push(Command::BeginContainer {
599 direction,
600 gap: self.gap,
601 align: self.align,
602 border: self.border,
603 border_style: self.border_style,
604 padding: self.padding,
605 margin: self.margin,
606 constraints: self.constraints,
607 title: self.title,
608 grow: self.grow,
609 });
610 }
611 f(self.ctx);
612 self.ctx.commands.push(Command::EndContainer);
613 self.ctx.last_text_idx = None;
614
615 self.ctx.response_for(interaction_id)
616 }
617}
618
619impl Context {
620 #[allow(clippy::too_many_arguments)]
621 pub(crate) fn new(
622 events: Vec<Event>,
623 width: u32,
624 height: u32,
625 tick: u64,
626 focus_index: usize,
627 prev_focus_count: usize,
628 prev_scroll_infos: Vec<(u32, u32)>,
629 prev_hit_map: Vec<Rect>,
630 debug: bool,
631 theme: Theme,
632 last_mouse_pos: Option<(u32, u32)>,
633 ) -> Self {
634 let consumed = vec![false; events.len()];
635
636 let mut mouse_pos = last_mouse_pos;
637 let mut click_pos = None;
638 for event in &events {
639 if let Event::Mouse(mouse) = event {
640 mouse_pos = Some((mouse.x, mouse.y));
641 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
642 click_pos = Some((mouse.x, mouse.y));
643 }
644 }
645 }
646
647 Self {
648 commands: Vec::new(),
649 events,
650 consumed,
651 should_quit: false,
652 area_width: width,
653 area_height: height,
654 tick,
655 focus_index,
656 focus_count: 0,
657 prev_focus_count,
658 scroll_count: 0,
659 prev_scroll_infos,
660 interaction_count: 0,
661 prev_hit_map,
662 mouse_pos,
663 click_pos,
664 last_mouse_pos,
665 last_text_idx: None,
666 debug,
667 theme,
668 }
669 }
670
671 pub(crate) fn process_focus_keys(&mut self) {
672 for (i, event) in self.events.iter().enumerate() {
673 if let Event::Key(key) = event {
674 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
675 if self.prev_focus_count > 0 {
676 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
677 }
678 self.consumed[i] = true;
679 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
680 || key.code == KeyCode::BackTab
681 {
682 if self.prev_focus_count > 0 {
683 self.focus_index = if self.focus_index == 0 {
684 self.prev_focus_count - 1
685 } else {
686 self.focus_index - 1
687 };
688 }
689 self.consumed[i] = true;
690 }
691 }
692 }
693 }
694
695 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
699 w.ui(self)
700 }
701
702 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
717 self.error_boundary_with(f, |ui, msg| {
718 ui.styled(
719 format!("⚠ Error: {msg}"),
720 Style::new().fg(ui.theme.error).bold(),
721 );
722 });
723 }
724
725 pub fn error_boundary_with(
745 &mut self,
746 f: impl FnOnce(&mut Context),
747 fallback: impl FnOnce(&mut Context, String),
748 ) {
749 let cmd_count = self.commands.len();
750 let last_text_idx = self.last_text_idx;
751
752 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
753 f(self);
754 }));
755
756 match result {
757 Ok(()) => {}
758 Err(panic_info) => {
759 self.commands.truncate(cmd_count);
760 self.last_text_idx = last_text_idx;
761
762 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
763 (*s).to_string()
764 } else if let Some(s) = panic_info.downcast_ref::<String>() {
765 s.clone()
766 } else {
767 "widget panicked".to_string()
768 };
769
770 fallback(self, msg);
771 }
772 }
773 }
774
775 pub fn interaction(&mut self) -> Response {
781 let id = self.interaction_count;
782 self.interaction_count += 1;
783 self.response_for(id)
784 }
785
786 pub fn register_focusable(&mut self) -> bool {
791 let id = self.focus_count;
792 self.focus_count += 1;
793 if self.prev_focus_count == 0 {
794 return true;
795 }
796 self.focus_index % self.prev_focus_count == id
797 }
798
799 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
812 let content = s.into();
813 self.commands.push(Command::Text {
814 content,
815 style: Style::new(),
816 grow: 0,
817 align: Align::Start,
818 wrap: false,
819 margin: Margin::default(),
820 constraints: Constraints::default(),
821 });
822 self.last_text_idx = Some(self.commands.len() - 1);
823 self
824 }
825
826 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
831 let content = s.into();
832 self.commands.push(Command::Text {
833 content,
834 style: Style::new(),
835 grow: 0,
836 align: Align::Start,
837 wrap: true,
838 margin: Margin::default(),
839 constraints: Constraints::default(),
840 });
841 self.last_text_idx = Some(self.commands.len() - 1);
842 self
843 }
844
845 pub fn bold(&mut self) -> &mut Self {
849 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
850 self
851 }
852
853 pub fn dim(&mut self) -> &mut Self {
858 let text_dim = self.theme.text_dim;
859 self.modify_last_style(|s| {
860 s.modifiers |= Modifiers::DIM;
861 if s.fg.is_none() {
862 s.fg = Some(text_dim);
863 }
864 });
865 self
866 }
867
868 pub fn italic(&mut self) -> &mut Self {
870 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
871 self
872 }
873
874 pub fn underline(&mut self) -> &mut Self {
876 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
877 self
878 }
879
880 pub fn reversed(&mut self) -> &mut Self {
882 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
883 self
884 }
885
886 pub fn strikethrough(&mut self) -> &mut Self {
888 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
889 self
890 }
891
892 pub fn fg(&mut self, color: Color) -> &mut Self {
894 self.modify_last_style(|s| s.fg = Some(color));
895 self
896 }
897
898 pub fn bg(&mut self, color: Color) -> &mut Self {
900 self.modify_last_style(|s| s.bg = Some(color));
901 self
902 }
903
904 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
909 self.commands.push(Command::Text {
910 content: s.into(),
911 style,
912 grow: 0,
913 align: Align::Start,
914 wrap: false,
915 margin: Margin::default(),
916 constraints: Constraints::default(),
917 });
918 self.last_text_idx = Some(self.commands.len() - 1);
919 self
920 }
921
922 pub fn wrap(&mut self) -> &mut Self {
924 if let Some(idx) = self.last_text_idx {
925 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
926 *wrap = true;
927 }
928 }
929 self
930 }
931
932 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
933 if let Some(idx) = self.last_text_idx {
934 if let Command::Text { style, .. } = &mut self.commands[idx] {
935 f(style);
936 }
937 }
938 }
939
940 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
958 self.push_container(Direction::Column, 0, f)
959 }
960
961 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
965 self.push_container(Direction::Column, gap, f)
966 }
967
968 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
985 self.push_container(Direction::Row, 0, f)
986 }
987
988 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
992 self.push_container(Direction::Row, gap, f)
993 }
994
995 pub fn container(&mut self) -> ContainerBuilder<'_> {
1016 let border = self.theme.border;
1017 ContainerBuilder {
1018 ctx: self,
1019 gap: 0,
1020 align: Align::Start,
1021 border: None,
1022 border_style: Style::new().fg(border),
1023 padding: Padding::default(),
1024 margin: Margin::default(),
1025 constraints: Constraints::default(),
1026 title: None,
1027 grow: 0,
1028 scroll_offset: None,
1029 }
1030 }
1031
1032 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1051 let index = self.scroll_count;
1052 self.scroll_count += 1;
1053 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1054 state.set_bounds(ch, vh);
1055 let max = ch.saturating_sub(vh) as usize;
1056 state.offset = state.offset.min(max);
1057 }
1058
1059 let next_id = self.interaction_count;
1060 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1061 self.auto_scroll(&rect, state);
1062 }
1063
1064 self.container().scroll_offset(state.offset as u32)
1065 }
1066
1067 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1068 let last_y = self.last_mouse_pos.map(|(_, y)| y);
1069 let mut to_consume: Vec<usize> = Vec::new();
1070
1071 for (i, event) in self.events.iter().enumerate() {
1072 if self.consumed[i] {
1073 continue;
1074 }
1075 if let Event::Mouse(mouse) = event {
1076 let in_bounds = mouse.x >= rect.x
1077 && mouse.x < rect.right()
1078 && mouse.y >= rect.y
1079 && mouse.y < rect.bottom();
1080 if !in_bounds {
1081 continue;
1082 }
1083 match mouse.kind {
1084 MouseKind::ScrollUp => {
1085 state.scroll_up(1);
1086 to_consume.push(i);
1087 }
1088 MouseKind::ScrollDown => {
1089 state.scroll_down(1);
1090 to_consume.push(i);
1091 }
1092 MouseKind::Drag(MouseButton::Left) => {
1093 if let Some(prev_y) = last_y {
1094 let delta = mouse.y as i32 - prev_y as i32;
1095 if delta < 0 {
1096 state.scroll_down((-delta) as usize);
1097 } else if delta > 0 {
1098 state.scroll_up(delta as usize);
1099 }
1100 }
1101 to_consume.push(i);
1102 }
1103 _ => {}
1104 }
1105 }
1106 }
1107
1108 for i in to_consume {
1109 self.consumed[i] = true;
1110 }
1111 }
1112
1113 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1117 self.container().border(border)
1118 }
1119
1120 fn push_container(
1121 &mut self,
1122 direction: Direction,
1123 gap: u32,
1124 f: impl FnOnce(&mut Context),
1125 ) -> Response {
1126 let interaction_id = self.interaction_count;
1127 self.interaction_count += 1;
1128 let border = self.theme.border;
1129
1130 self.commands.push(Command::BeginContainer {
1131 direction,
1132 gap,
1133 align: Align::Start,
1134 border: None,
1135 border_style: Style::new().fg(border),
1136 padding: Padding::default(),
1137 margin: Margin::default(),
1138 constraints: Constraints::default(),
1139 title: None,
1140 grow: 0,
1141 });
1142 f(self);
1143 self.commands.push(Command::EndContainer);
1144 self.last_text_idx = None;
1145
1146 self.response_for(interaction_id)
1147 }
1148
1149 fn response_for(&self, interaction_id: usize) -> Response {
1150 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1151 let clicked = self
1152 .click_pos
1153 .map(|(mx, my)| {
1154 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1155 })
1156 .unwrap_or(false);
1157 let hovered = self
1158 .mouse_pos
1159 .map(|(mx, my)| {
1160 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1161 })
1162 .unwrap_or(false);
1163 Response { clicked, hovered }
1164 } else {
1165 Response::default()
1166 }
1167 }
1168
1169 pub fn grow(&mut self, value: u16) -> &mut Self {
1174 if let Some(idx) = self.last_text_idx {
1175 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1176 *grow = value;
1177 }
1178 }
1179 self
1180 }
1181
1182 pub fn align(&mut self, align: Align) -> &mut Self {
1184 if let Some(idx) = self.last_text_idx {
1185 if let Command::Text {
1186 align: text_align, ..
1187 } = &mut self.commands[idx]
1188 {
1189 *text_align = align;
1190 }
1191 }
1192 self
1193 }
1194
1195 pub fn spacer(&mut self) -> &mut Self {
1199 self.commands.push(Command::Spacer { grow: 1 });
1200 self.last_text_idx = None;
1201 self
1202 }
1203
1204 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1220 let focused = self.register_focusable();
1221 state.cursor = state.cursor.min(state.value.chars().count());
1222
1223 if focused {
1224 let mut consumed_indices = Vec::new();
1225 for (i, event) in self.events.iter().enumerate() {
1226 if let Event::Key(key) = event {
1227 match key.code {
1228 KeyCode::Char(ch) => {
1229 if let Some(max) = state.max_length {
1230 if state.value.chars().count() >= max {
1231 continue;
1232 }
1233 }
1234 let index = byte_index_for_char(&state.value, state.cursor);
1235 state.value.insert(index, ch);
1236 state.cursor += 1;
1237 consumed_indices.push(i);
1238 }
1239 KeyCode::Backspace => {
1240 if state.cursor > 0 {
1241 let start = byte_index_for_char(&state.value, state.cursor - 1);
1242 let end = byte_index_for_char(&state.value, state.cursor);
1243 state.value.replace_range(start..end, "");
1244 state.cursor -= 1;
1245 }
1246 consumed_indices.push(i);
1247 }
1248 KeyCode::Left => {
1249 state.cursor = state.cursor.saturating_sub(1);
1250 consumed_indices.push(i);
1251 }
1252 KeyCode::Right => {
1253 state.cursor = (state.cursor + 1).min(state.value.chars().count());
1254 consumed_indices.push(i);
1255 }
1256 KeyCode::Home => {
1257 state.cursor = 0;
1258 consumed_indices.push(i);
1259 }
1260 KeyCode::End => {
1261 state.cursor = state.value.chars().count();
1262 consumed_indices.push(i);
1263 }
1264 _ => {}
1265 }
1266 }
1267 }
1268
1269 for index in consumed_indices {
1270 self.consumed[index] = true;
1271 }
1272 }
1273
1274 if state.value.is_empty() {
1275 self.styled(
1276 state.placeholder.clone(),
1277 Style::new().dim().fg(self.theme.text_dim),
1278 )
1279 } else {
1280 let mut rendered = String::new();
1281 for (idx, ch) in state.value.chars().enumerate() {
1282 if focused && idx == state.cursor {
1283 rendered.push('▎');
1284 }
1285 rendered.push(ch);
1286 }
1287 if focused && state.cursor >= state.value.chars().count() {
1288 rendered.push('▎');
1289 }
1290 self.styled(rendered, Style::new().fg(self.theme.text))
1291 }
1292 }
1293
1294 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1300 self.styled(
1301 state.frame(self.tick).to_string(),
1302 Style::new().fg(self.theme.primary),
1303 )
1304 }
1305
1306 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1311 state.cleanup(self.tick);
1312 if state.messages.is_empty() {
1313 return self;
1314 }
1315
1316 self.interaction_count += 1;
1317 self.commands.push(Command::BeginContainer {
1318 direction: Direction::Column,
1319 gap: 0,
1320 align: Align::Start,
1321 border: None,
1322 border_style: Style::new().fg(self.theme.border),
1323 padding: Padding::default(),
1324 margin: Margin::default(),
1325 constraints: Constraints::default(),
1326 title: None,
1327 grow: 0,
1328 });
1329 for message in state.messages.iter().rev() {
1330 let color = match message.level {
1331 ToastLevel::Info => self.theme.primary,
1332 ToastLevel::Success => self.theme.success,
1333 ToastLevel::Warning => self.theme.warning,
1334 ToastLevel::Error => self.theme.error,
1335 };
1336 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
1337 }
1338 self.commands.push(Command::EndContainer);
1339 self.last_text_idx = None;
1340
1341 self
1342 }
1343
1344 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1349 if state.lines.is_empty() {
1350 state.lines.push(String::new());
1351 }
1352 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1353 state.cursor_col = state
1354 .cursor_col
1355 .min(state.lines[state.cursor_row].chars().count());
1356
1357 let focused = self.register_focusable();
1358
1359 if focused {
1360 let mut consumed_indices = Vec::new();
1361 for (i, event) in self.events.iter().enumerate() {
1362 if let Event::Key(key) = event {
1363 match key.code {
1364 KeyCode::Char(ch) => {
1365 if let Some(max) = state.max_length {
1366 let total: usize =
1367 state.lines.iter().map(|line| line.chars().count()).sum();
1368 if total >= max {
1369 continue;
1370 }
1371 }
1372 let index = byte_index_for_char(
1373 &state.lines[state.cursor_row],
1374 state.cursor_col,
1375 );
1376 state.lines[state.cursor_row].insert(index, ch);
1377 state.cursor_col += 1;
1378 consumed_indices.push(i);
1379 }
1380 KeyCode::Enter => {
1381 let split_index = byte_index_for_char(
1382 &state.lines[state.cursor_row],
1383 state.cursor_col,
1384 );
1385 let remainder = state.lines[state.cursor_row].split_off(split_index);
1386 state.cursor_row += 1;
1387 state.lines.insert(state.cursor_row, remainder);
1388 state.cursor_col = 0;
1389 consumed_indices.push(i);
1390 }
1391 KeyCode::Backspace => {
1392 if state.cursor_col > 0 {
1393 let start = byte_index_for_char(
1394 &state.lines[state.cursor_row],
1395 state.cursor_col - 1,
1396 );
1397 let end = byte_index_for_char(
1398 &state.lines[state.cursor_row],
1399 state.cursor_col,
1400 );
1401 state.lines[state.cursor_row].replace_range(start..end, "");
1402 state.cursor_col -= 1;
1403 } else if state.cursor_row > 0 {
1404 let current = state.lines.remove(state.cursor_row);
1405 state.cursor_row -= 1;
1406 state.cursor_col = state.lines[state.cursor_row].chars().count();
1407 state.lines[state.cursor_row].push_str(¤t);
1408 }
1409 consumed_indices.push(i);
1410 }
1411 KeyCode::Left => {
1412 if state.cursor_col > 0 {
1413 state.cursor_col -= 1;
1414 } else if state.cursor_row > 0 {
1415 state.cursor_row -= 1;
1416 state.cursor_col = state.lines[state.cursor_row].chars().count();
1417 }
1418 consumed_indices.push(i);
1419 }
1420 KeyCode::Right => {
1421 let line_len = state.lines[state.cursor_row].chars().count();
1422 if state.cursor_col < line_len {
1423 state.cursor_col += 1;
1424 } else if state.cursor_row + 1 < state.lines.len() {
1425 state.cursor_row += 1;
1426 state.cursor_col = 0;
1427 }
1428 consumed_indices.push(i);
1429 }
1430 KeyCode::Up => {
1431 if state.cursor_row > 0 {
1432 state.cursor_row -= 1;
1433 state.cursor_col = state
1434 .cursor_col
1435 .min(state.lines[state.cursor_row].chars().count());
1436 }
1437 consumed_indices.push(i);
1438 }
1439 KeyCode::Down => {
1440 if state.cursor_row + 1 < state.lines.len() {
1441 state.cursor_row += 1;
1442 state.cursor_col = state
1443 .cursor_col
1444 .min(state.lines[state.cursor_row].chars().count());
1445 }
1446 consumed_indices.push(i);
1447 }
1448 KeyCode::Home => {
1449 state.cursor_col = 0;
1450 consumed_indices.push(i);
1451 }
1452 KeyCode::End => {
1453 state.cursor_col = state.lines[state.cursor_row].chars().count();
1454 consumed_indices.push(i);
1455 }
1456 _ => {}
1457 }
1458 }
1459 }
1460
1461 for index in consumed_indices {
1462 self.consumed[index] = true;
1463 }
1464 }
1465
1466 self.interaction_count += 1;
1467 self.commands.push(Command::BeginContainer {
1468 direction: Direction::Column,
1469 gap: 0,
1470 align: Align::Start,
1471 border: None,
1472 border_style: Style::new().fg(self.theme.border),
1473 padding: Padding::default(),
1474 margin: Margin::default(),
1475 constraints: Constraints::default(),
1476 title: None,
1477 grow: 0,
1478 });
1479 for row in 0..visible_rows as usize {
1480 let line = state.lines.get(row).cloned().unwrap_or_default();
1481 let mut rendered = line.clone();
1482 let mut style = if line.is_empty() {
1483 Style::new().fg(self.theme.text_dim)
1484 } else {
1485 Style::new().fg(self.theme.text)
1486 };
1487
1488 if focused && row == state.cursor_row {
1489 rendered.clear();
1490 for (idx, ch) in line.chars().enumerate() {
1491 if idx == state.cursor_col {
1492 rendered.push('▎');
1493 }
1494 rendered.push(ch);
1495 }
1496 if state.cursor_col >= line.chars().count() {
1497 rendered.push('▎');
1498 }
1499 style = Style::new().fg(self.theme.text);
1500 }
1501
1502 self.styled(rendered, style);
1503 }
1504 self.commands.push(Command::EndContainer);
1505 self.last_text_idx = None;
1506
1507 self
1508 }
1509
1510 pub fn progress(&mut self, ratio: f64) -> &mut Self {
1515 self.progress_bar(ratio, 20)
1516 }
1517
1518 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1523 let clamped = ratio.clamp(0.0, 1.0);
1524 let filled = (clamped * width as f64).round() as u32;
1525 let empty = width.saturating_sub(filled);
1526 let mut bar = String::new();
1527 for _ in 0..filled {
1528 bar.push('█');
1529 }
1530 for _ in 0..empty {
1531 bar.push('░');
1532 }
1533 self.text(bar)
1534 }
1535
1536 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1555 if data.is_empty() {
1556 return self;
1557 }
1558
1559 let max_label_width = data
1560 .iter()
1561 .map(|(label, _)| UnicodeWidthStr::width(*label))
1562 .max()
1563 .unwrap_or(0);
1564 let max_value = data
1565 .iter()
1566 .map(|(_, value)| *value)
1567 .fold(f64::NEG_INFINITY, f64::max);
1568 let denom = if max_value > 0.0 { max_value } else { 1.0 };
1569
1570 self.interaction_count += 1;
1571 self.commands.push(Command::BeginContainer {
1572 direction: Direction::Column,
1573 gap: 0,
1574 align: Align::Start,
1575 border: None,
1576 border_style: Style::new().fg(self.theme.border),
1577 padding: Padding::default(),
1578 margin: Margin::default(),
1579 constraints: Constraints::default(),
1580 title: None,
1581 grow: 0,
1582 });
1583
1584 for (label, value) in data {
1585 let label_width = UnicodeWidthStr::width(*label);
1586 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
1587 let normalized = (*value / denom).clamp(0.0, 1.0);
1588 let bar_len = (normalized * max_width as f64).round() as usize;
1589 let bar = "█".repeat(bar_len);
1590
1591 self.interaction_count += 1;
1592 self.commands.push(Command::BeginContainer {
1593 direction: Direction::Row,
1594 gap: 1,
1595 align: Align::Start,
1596 border: None,
1597 border_style: Style::new().fg(self.theme.border),
1598 padding: Padding::default(),
1599 margin: Margin::default(),
1600 constraints: Constraints::default(),
1601 title: None,
1602 grow: 0,
1603 });
1604 self.styled(
1605 format!("{label}{label_padding}"),
1606 Style::new().fg(self.theme.text),
1607 );
1608 self.styled(bar, Style::new().fg(self.theme.primary));
1609 self.styled(
1610 format_compact_number(*value),
1611 Style::new().fg(self.theme.text_dim),
1612 );
1613 self.commands.push(Command::EndContainer);
1614 self.last_text_idx = None;
1615 }
1616
1617 self.commands.push(Command::EndContainer);
1618 self.last_text_idx = None;
1619
1620 self
1621 }
1622
1623 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
1637 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
1638
1639 let w = width as usize;
1640 let window = if data.len() > w {
1641 &data[data.len() - w..]
1642 } else {
1643 data
1644 };
1645
1646 if window.is_empty() {
1647 return self;
1648 }
1649
1650 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
1651 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1652 let range = max - min;
1653
1654 let line: String = window
1655 .iter()
1656 .map(|&value| {
1657 let normalized = if range == 0.0 {
1658 0.5
1659 } else {
1660 (value - min) / range
1661 };
1662 let idx = (normalized * 7.0).round() as usize;
1663 BLOCKS[idx.min(7)]
1664 })
1665 .collect();
1666
1667 self.styled(line, Style::new().fg(self.theme.primary))
1668 }
1669
1670 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
1684 if data.is_empty() || width == 0 || height == 0 {
1685 return self;
1686 }
1687
1688 let cols = width as usize;
1689 let rows = height as usize;
1690 let px_w = cols * 2;
1691 let px_h = rows * 4;
1692
1693 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
1694 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1695 let range = if (max - min).abs() < f64::EPSILON {
1696 1.0
1697 } else {
1698 max - min
1699 };
1700
1701 let points: Vec<usize> = (0..px_w)
1702 .map(|px| {
1703 let data_idx = if px_w <= 1 {
1704 0.0
1705 } else {
1706 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
1707 };
1708 let idx = data_idx.floor() as usize;
1709 let frac = data_idx - idx as f64;
1710 let value = if idx + 1 < data.len() {
1711 data[idx] * (1.0 - frac) + data[idx + 1] * frac
1712 } else {
1713 data[idx.min(data.len() - 1)]
1714 };
1715
1716 let normalized = (value - min) / range;
1717 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
1718 py.min(px_h - 1)
1719 })
1720 .collect();
1721
1722 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
1723 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
1724
1725 let mut grid = vec![vec![0u32; cols]; rows];
1726
1727 for i in 0..points.len() {
1728 let px = i;
1729 let py = points[i];
1730 let char_col = px / 2;
1731 let char_row = py / 4;
1732 let sub_col = px % 2;
1733 let sub_row = py % 4;
1734
1735 if char_col < cols && char_row < rows {
1736 grid[char_row][char_col] |= if sub_col == 0 {
1737 LEFT_BITS[sub_row]
1738 } else {
1739 RIGHT_BITS[sub_row]
1740 };
1741 }
1742
1743 if i + 1 < points.len() {
1744 let py_next = points[i + 1];
1745 let (y_start, y_end) = if py <= py_next {
1746 (py, py_next)
1747 } else {
1748 (py_next, py)
1749 };
1750 for y in y_start..=y_end {
1751 let cell_row = y / 4;
1752 let sub_y = y % 4;
1753 if char_col < cols && cell_row < rows {
1754 grid[cell_row][char_col] |= if sub_col == 0 {
1755 LEFT_BITS[sub_y]
1756 } else {
1757 RIGHT_BITS[sub_y]
1758 };
1759 }
1760 }
1761 }
1762 }
1763
1764 let style = Style::new().fg(self.theme.primary);
1765 for row in grid {
1766 let line: String = row
1767 .iter()
1768 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
1769 .collect();
1770 self.styled(line, style);
1771 }
1772
1773 self
1774 }
1775
1776 pub fn canvas(
1793 &mut self,
1794 width: u32,
1795 height: u32,
1796 draw: impl FnOnce(&mut CanvasContext),
1797 ) -> &mut Self {
1798 if width == 0 || height == 0 {
1799 return self;
1800 }
1801
1802 let mut canvas = CanvasContext::new(width as usize, height as usize);
1803 draw(&mut canvas);
1804
1805 let style = Style::new().fg(self.theme.primary);
1806 for line in canvas.render() {
1807 self.styled(line, style);
1808 }
1809
1810 self
1811 }
1812
1813 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
1830 let interaction_id = self.interaction_count;
1831 self.interaction_count += 1;
1832 let border = self.theme.border;
1833
1834 self.commands.push(Command::BeginContainer {
1835 direction: Direction::Column,
1836 gap: 0,
1837 align: Align::Start,
1838 border: None,
1839 border_style: Style::new().fg(border),
1840 padding: Padding::default(),
1841 margin: Margin::default(),
1842 constraints: Constraints::default(),
1843 title: None,
1844 grow: 0,
1845 });
1846
1847 let children_start = self.commands.len();
1848 f(self);
1849 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
1850
1851 let mut elements: Vec<Vec<Command>> = Vec::new();
1852 let mut iter = child_commands.into_iter().peekable();
1853 while let Some(cmd) = iter.next() {
1854 match cmd {
1855 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
1856 let mut depth = 1_u32;
1857 let mut element = vec![cmd];
1858 for next in iter.by_ref() {
1859 match next {
1860 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
1861 depth += 1;
1862 }
1863 Command::EndContainer => {
1864 depth = depth.saturating_sub(1);
1865 }
1866 _ => {}
1867 }
1868 let at_end = matches!(next, Command::EndContainer) && depth == 0;
1869 element.push(next);
1870 if at_end {
1871 break;
1872 }
1873 }
1874 elements.push(element);
1875 }
1876 Command::EndContainer => {}
1877 _ => elements.push(vec![cmd]),
1878 }
1879 }
1880
1881 let cols = cols.max(1) as usize;
1882 for row in elements.chunks(cols) {
1883 self.interaction_count += 1;
1884 self.commands.push(Command::BeginContainer {
1885 direction: Direction::Row,
1886 gap: 0,
1887 align: Align::Start,
1888 border: None,
1889 border_style: Style::new().fg(border),
1890 padding: Padding::default(),
1891 margin: Margin::default(),
1892 constraints: Constraints::default(),
1893 title: None,
1894 grow: 0,
1895 });
1896
1897 for element in row {
1898 self.interaction_count += 1;
1899 self.commands.push(Command::BeginContainer {
1900 direction: Direction::Column,
1901 gap: 0,
1902 align: Align::Start,
1903 border: None,
1904 border_style: Style::new().fg(border),
1905 padding: Padding::default(),
1906 margin: Margin::default(),
1907 constraints: Constraints::default(),
1908 title: None,
1909 grow: 1,
1910 });
1911 self.commands.extend(element.iter().cloned());
1912 self.commands.push(Command::EndContainer);
1913 }
1914
1915 self.commands.push(Command::EndContainer);
1916 }
1917
1918 self.commands.push(Command::EndContainer);
1919 self.last_text_idx = None;
1920
1921 self.response_for(interaction_id)
1922 }
1923
1924 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
1929 if state.items.is_empty() {
1930 state.selected = 0;
1931 return self;
1932 }
1933
1934 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1935
1936 let focused = self.register_focusable();
1937
1938 if focused {
1939 let mut consumed_indices = Vec::new();
1940 for (i, event) in self.events.iter().enumerate() {
1941 if let Event::Key(key) = event {
1942 match key.code {
1943 KeyCode::Up | KeyCode::Char('k') => {
1944 state.selected = state.selected.saturating_sub(1);
1945 consumed_indices.push(i);
1946 }
1947 KeyCode::Down | KeyCode::Char('j') => {
1948 state.selected =
1949 (state.selected + 1).min(state.items.len().saturating_sub(1));
1950 consumed_indices.push(i);
1951 }
1952 _ => {}
1953 }
1954 }
1955 }
1956
1957 for index in consumed_indices {
1958 self.consumed[index] = true;
1959 }
1960 }
1961
1962 for (idx, item) in state.items.iter().enumerate() {
1963 if idx == state.selected {
1964 if focused {
1965 self.styled(
1966 format!("▸ {item}"),
1967 Style::new().bold().fg(self.theme.primary),
1968 );
1969 } else {
1970 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
1971 }
1972 } else {
1973 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
1974 }
1975 }
1976
1977 self
1978 }
1979
1980 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
1985 if state.is_dirty() {
1986 state.recompute_widths();
1987 }
1988
1989 let focused = self.register_focusable();
1990
1991 if focused && !state.rows.is_empty() {
1992 let mut consumed_indices = Vec::new();
1993 for (i, event) in self.events.iter().enumerate() {
1994 if let Event::Key(key) = event {
1995 match key.code {
1996 KeyCode::Up | KeyCode::Char('k') => {
1997 state.selected = state.selected.saturating_sub(1);
1998 consumed_indices.push(i);
1999 }
2000 KeyCode::Down | KeyCode::Char('j') => {
2001 state.selected =
2002 (state.selected + 1).min(state.rows.len().saturating_sub(1));
2003 consumed_indices.push(i);
2004 }
2005 _ => {}
2006 }
2007 }
2008 }
2009 for index in consumed_indices {
2010 self.consumed[index] = true;
2011 }
2012 }
2013
2014 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2015
2016 let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2017 self.styled(header_line, Style::new().bold().fg(self.theme.text));
2018
2019 let separator = state
2020 .column_widths()
2021 .iter()
2022 .map(|w| "─".repeat(*w as usize))
2023 .collect::<Vec<_>>()
2024 .join("─┼─");
2025 self.text(separator);
2026
2027 for (idx, row) in state.rows.iter().enumerate() {
2028 let line = format_table_row(row, state.column_widths(), " │ ");
2029 if idx == state.selected {
2030 let mut style = Style::new()
2031 .bg(self.theme.selected_bg)
2032 .fg(self.theme.selected_fg);
2033 if focused {
2034 style = style.bold();
2035 }
2036 self.styled(line, style);
2037 } else {
2038 self.styled(line, Style::new().fg(self.theme.text));
2039 }
2040 }
2041
2042 self
2043 }
2044
2045 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
2050 if state.labels.is_empty() {
2051 state.selected = 0;
2052 return self;
2053 }
2054
2055 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
2056 let focused = self.register_focusable();
2057
2058 if focused {
2059 let mut consumed_indices = Vec::new();
2060 for (i, event) in self.events.iter().enumerate() {
2061 if let Event::Key(key) = event {
2062 match key.code {
2063 KeyCode::Left => {
2064 state.selected = if state.selected == 0 {
2065 state.labels.len().saturating_sub(1)
2066 } else {
2067 state.selected - 1
2068 };
2069 consumed_indices.push(i);
2070 }
2071 KeyCode::Right => {
2072 state.selected = (state.selected + 1) % state.labels.len();
2073 consumed_indices.push(i);
2074 }
2075 _ => {}
2076 }
2077 }
2078 }
2079
2080 for index in consumed_indices {
2081 self.consumed[index] = true;
2082 }
2083 }
2084
2085 self.interaction_count += 1;
2086 self.commands.push(Command::BeginContainer {
2087 direction: Direction::Row,
2088 gap: 1,
2089 align: Align::Start,
2090 border: None,
2091 border_style: Style::new().fg(self.theme.border),
2092 padding: Padding::default(),
2093 margin: Margin::default(),
2094 constraints: Constraints::default(),
2095 title: None,
2096 grow: 0,
2097 });
2098 for (idx, label) in state.labels.iter().enumerate() {
2099 let style = if idx == state.selected {
2100 let s = Style::new().fg(self.theme.primary).bold();
2101 if focused {
2102 s.underline()
2103 } else {
2104 s
2105 }
2106 } else {
2107 Style::new().fg(self.theme.text_dim)
2108 };
2109 self.styled(format!("[ {label} ]"), style);
2110 }
2111 self.commands.push(Command::EndContainer);
2112 self.last_text_idx = None;
2113
2114 self
2115 }
2116
2117 pub fn button(&mut self, label: impl Into<String>) -> bool {
2122 let focused = self.register_focusable();
2123 let interaction_id = self.interaction_count;
2124 self.interaction_count += 1;
2125 let response = self.response_for(interaction_id);
2126
2127 let mut activated = response.clicked;
2128 if focused {
2129 let mut consumed_indices = Vec::new();
2130 for (i, event) in self.events.iter().enumerate() {
2131 if let Event::Key(key) = event {
2132 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2133 activated = true;
2134 consumed_indices.push(i);
2135 }
2136 }
2137 }
2138
2139 for index in consumed_indices {
2140 self.consumed[index] = true;
2141 }
2142 }
2143
2144 let style = if focused {
2145 Style::new().fg(self.theme.primary).bold()
2146 } else if response.hovered {
2147 Style::new().fg(self.theme.accent)
2148 } else {
2149 Style::new().fg(self.theme.text)
2150 };
2151
2152 self.commands.push(Command::BeginContainer {
2153 direction: Direction::Row,
2154 gap: 0,
2155 align: Align::Start,
2156 border: None,
2157 border_style: Style::new().fg(self.theme.border),
2158 padding: Padding::default(),
2159 margin: Margin::default(),
2160 constraints: Constraints::default(),
2161 title: None,
2162 grow: 0,
2163 });
2164 self.styled(format!("[ {} ]", label.into()), style);
2165 self.commands.push(Command::EndContainer);
2166 self.last_text_idx = None;
2167
2168 activated
2169 }
2170
2171 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
2176 let focused = self.register_focusable();
2177 let interaction_id = self.interaction_count;
2178 self.interaction_count += 1;
2179 let response = self.response_for(interaction_id);
2180 let mut should_toggle = response.clicked;
2181
2182 if focused {
2183 let mut consumed_indices = Vec::new();
2184 for (i, event) in self.events.iter().enumerate() {
2185 if let Event::Key(key) = event {
2186 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2187 should_toggle = true;
2188 consumed_indices.push(i);
2189 }
2190 }
2191 }
2192
2193 for index in consumed_indices {
2194 self.consumed[index] = true;
2195 }
2196 }
2197
2198 if should_toggle {
2199 *checked = !*checked;
2200 }
2201
2202 self.commands.push(Command::BeginContainer {
2203 direction: Direction::Row,
2204 gap: 1,
2205 align: Align::Start,
2206 border: None,
2207 border_style: Style::new().fg(self.theme.border),
2208 padding: Padding::default(),
2209 margin: Margin::default(),
2210 constraints: Constraints::default(),
2211 title: None,
2212 grow: 0,
2213 });
2214 let marker_style = if *checked {
2215 Style::new().fg(self.theme.success)
2216 } else {
2217 Style::new().fg(self.theme.text_dim)
2218 };
2219 let marker = if *checked { "[x]" } else { "[ ]" };
2220 let label_text = label.into();
2221 if focused {
2222 self.styled(format!("▸ {marker}"), marker_style.bold());
2223 self.styled(label_text, Style::new().fg(self.theme.text).bold());
2224 } else {
2225 self.styled(marker, marker_style);
2226 self.styled(label_text, Style::new().fg(self.theme.text));
2227 }
2228 self.commands.push(Command::EndContainer);
2229 self.last_text_idx = None;
2230
2231 self
2232 }
2233
2234 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
2240 let focused = self.register_focusable();
2241 let interaction_id = self.interaction_count;
2242 self.interaction_count += 1;
2243 let response = self.response_for(interaction_id);
2244 let mut should_toggle = response.clicked;
2245
2246 if focused {
2247 let mut consumed_indices = Vec::new();
2248 for (i, event) in self.events.iter().enumerate() {
2249 if let Event::Key(key) = event {
2250 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
2251 should_toggle = true;
2252 consumed_indices.push(i);
2253 }
2254 }
2255 }
2256
2257 for index in consumed_indices {
2258 self.consumed[index] = true;
2259 }
2260 }
2261
2262 if should_toggle {
2263 *on = !*on;
2264 }
2265
2266 self.commands.push(Command::BeginContainer {
2267 direction: Direction::Row,
2268 gap: 2,
2269 align: Align::Start,
2270 border: None,
2271 border_style: Style::new().fg(self.theme.border),
2272 padding: Padding::default(),
2273 margin: Margin::default(),
2274 constraints: Constraints::default(),
2275 title: None,
2276 grow: 0,
2277 });
2278 let label_text = label.into();
2279 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
2280 let switch_style = if *on {
2281 Style::new().fg(self.theme.success)
2282 } else {
2283 Style::new().fg(self.theme.text_dim)
2284 };
2285 if focused {
2286 self.styled(
2287 format!("▸ {label_text}"),
2288 Style::new().fg(self.theme.text).bold(),
2289 );
2290 self.styled(switch, switch_style.bold());
2291 } else {
2292 self.styled(label_text, Style::new().fg(self.theme.text));
2293 self.styled(switch, switch_style);
2294 }
2295 self.commands.push(Command::EndContainer);
2296 self.last_text_idx = None;
2297
2298 self
2299 }
2300
2301 pub fn separator(&mut self) -> &mut Self {
2306 self.commands.push(Command::Text {
2307 content: "─".repeat(200),
2308 style: Style::new().fg(self.theme.border).dim(),
2309 grow: 0,
2310 align: Align::Start,
2311 wrap: false,
2312 margin: Margin::default(),
2313 constraints: Constraints::default(),
2314 });
2315 self.last_text_idx = Some(self.commands.len() - 1);
2316 self
2317 }
2318
2319 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
2325 if bindings.is_empty() {
2326 return self;
2327 }
2328
2329 self.interaction_count += 1;
2330 self.commands.push(Command::BeginContainer {
2331 direction: Direction::Row,
2332 gap: 2,
2333 align: Align::Start,
2334 border: None,
2335 border_style: Style::new().fg(self.theme.border),
2336 padding: Padding::default(),
2337 margin: Margin::default(),
2338 constraints: Constraints::default(),
2339 title: None,
2340 grow: 0,
2341 });
2342 for (idx, (key, action)) in bindings.iter().enumerate() {
2343 if idx > 0 {
2344 self.styled("·", Style::new().fg(self.theme.text_dim));
2345 }
2346 self.styled(*key, Style::new().bold().fg(self.theme.primary));
2347 self.styled(*action, Style::new().fg(self.theme.text_dim));
2348 }
2349 self.commands.push(Command::EndContainer);
2350 self.last_text_idx = None;
2351
2352 self
2353 }
2354
2355 pub fn key(&self, c: char) -> bool {
2361 self.events.iter().enumerate().any(|(i, e)| {
2362 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
2363 })
2364 }
2365
2366 pub fn key_code(&self, code: KeyCode) -> bool {
2370 self.events
2371 .iter()
2372 .enumerate()
2373 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
2374 }
2375
2376 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
2380 self.events.iter().enumerate().any(|(i, e)| {
2381 !self.consumed[i]
2382 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
2383 })
2384 }
2385
2386 pub fn mouse_down(&self) -> Option<(u32, u32)> {
2390 self.events.iter().enumerate().find_map(|(i, event)| {
2391 if self.consumed[i] {
2392 return None;
2393 }
2394 if let Event::Mouse(mouse) = event {
2395 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
2396 return Some((mouse.x, mouse.y));
2397 }
2398 }
2399 None
2400 })
2401 }
2402
2403 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
2408 self.mouse_pos
2409 }
2410
2411 pub fn scroll_up(&self) -> bool {
2413 self.events.iter().enumerate().any(|(i, event)| {
2414 !self.consumed[i]
2415 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
2416 })
2417 }
2418
2419 pub fn scroll_down(&self) -> bool {
2421 self.events.iter().enumerate().any(|(i, event)| {
2422 !self.consumed[i]
2423 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
2424 })
2425 }
2426
2427 pub fn quit(&mut self) {
2429 self.should_quit = true;
2430 }
2431
2432 pub fn theme(&self) -> &Theme {
2434 &self.theme
2435 }
2436
2437 pub fn set_theme(&mut self, theme: Theme) {
2441 self.theme = theme;
2442 }
2443
2444 pub fn width(&self) -> u32 {
2448 self.area_width
2449 }
2450
2451 pub fn height(&self) -> u32 {
2453 self.area_height
2454 }
2455
2456 pub fn tick(&self) -> u64 {
2461 self.tick
2462 }
2463
2464 pub fn debug_enabled(&self) -> bool {
2468 self.debug
2469 }
2470}
2471
2472#[inline]
2473fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2474 if char_index == 0 {
2475 return 0;
2476 }
2477 value
2478 .char_indices()
2479 .nth(char_index)
2480 .map_or(value.len(), |(idx, _)| idx)
2481}
2482
2483fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2484 let mut parts: Vec<String> = Vec::new();
2485 for (i, width) in widths.iter().enumerate() {
2486 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2487 let cell_width = UnicodeWidthStr::width(cell) as u32;
2488 let padding = (*width).saturating_sub(cell_width) as usize;
2489 parts.push(format!("{cell}{}", " ".repeat(padding)));
2490 }
2491 parts.join(separator)
2492}
2493
2494fn format_compact_number(value: f64) -> String {
2495 if value.fract().abs() < f64::EPSILON {
2496 return format!("{value:.0}");
2497 }
2498
2499 let mut s = format!("{value:.2}");
2500 while s.contains('.') && s.ends_with('0') {
2501 s.pop();
2502 }
2503 if s.ends_with('.') {
2504 s.pop();
2505 }
2506 s
2507}