1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
3use crate::layout::{Command, Direction};
4use crate::rect::Rect;
5use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
6use crate::widgets::{
7 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
8 ToastLevel, ToastState,
9};
10use unicode_width::UnicodeWidthStr;
11
12#[derive(Debug, Clone, Copy, Default)]
18pub struct Response {
19 pub clicked: bool,
21 pub hovered: bool,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BarDirection {
28 Horizontal,
30 Vertical,
32}
33
34#[derive(Debug, Clone)]
36pub struct Bar {
37 pub label: String,
39 pub value: f64,
41 pub color: Option<Color>,
43}
44
45impl Bar {
46 pub fn new(label: impl Into<String>, value: f64) -> Self {
48 Self {
49 label: label.into(),
50 value,
51 color: None,
52 }
53 }
54
55 pub fn color(mut self, color: Color) -> Self {
57 self.color = Some(color);
58 self
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct BarGroup {
65 pub label: String,
67 pub bars: Vec<Bar>,
69}
70
71impl BarGroup {
72 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
74 Self {
75 label: label.into(),
76 bars,
77 }
78 }
79}
80
81pub trait Widget {
143 type Response;
146
147 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
153}
154
155pub struct Context {
171 pub(crate) commands: Vec<Command>,
172 pub(crate) events: Vec<Event>,
173 pub(crate) consumed: Vec<bool>,
174 pub(crate) should_quit: bool,
175 pub(crate) area_width: u32,
176 pub(crate) area_height: u32,
177 pub(crate) tick: u64,
178 pub(crate) focus_index: usize,
179 pub(crate) focus_count: usize,
180 prev_focus_count: usize,
181 scroll_count: usize,
182 prev_scroll_infos: Vec<(u32, u32)>,
183 interaction_count: usize,
184 pub(crate) prev_hit_map: Vec<Rect>,
185 mouse_pos: Option<(u32, u32)>,
186 click_pos: Option<(u32, u32)>,
187 last_mouse_pos: Option<(u32, u32)>,
188 last_text_idx: Option<usize>,
189 debug: bool,
190 theme: Theme,
191}
192
193#[must_use = "configure and finalize with .col() or .row()"]
214pub struct ContainerBuilder<'a> {
215 ctx: &'a mut Context,
216 gap: u32,
217 align: Align,
218 border: Option<Border>,
219 border_style: Style,
220 padding: Padding,
221 margin: Margin,
222 constraints: Constraints,
223 title: Option<(String, Style)>,
224 grow: u16,
225 scroll_offset: Option<u32>,
226}
227
228#[derive(Debug, Clone, Copy)]
235struct CanvasPixel {
236 bits: u32,
237 color: Color,
238}
239
240#[derive(Debug, Clone)]
242struct CanvasLabel {
243 x: usize,
244 y: usize,
245 text: String,
246 color: Color,
247}
248
249#[derive(Debug, Clone)]
251struct CanvasLayer {
252 grid: Vec<Vec<CanvasPixel>>,
253 labels: Vec<CanvasLabel>,
254}
255
256pub struct CanvasContext {
257 layers: Vec<CanvasLayer>,
258 cols: usize,
259 rows: usize,
260 px_w: usize,
261 px_h: usize,
262 current_color: Color,
263}
264
265impl CanvasContext {
266 fn new(cols: usize, rows: usize) -> Self {
267 Self {
268 layers: vec![Self::new_layer(cols, rows)],
269 cols,
270 rows,
271 px_w: cols * 2,
272 px_h: rows * 4,
273 current_color: Color::Reset,
274 }
275 }
276
277 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
278 CanvasLayer {
279 grid: vec![
280 vec![
281 CanvasPixel {
282 bits: 0,
283 color: Color::Reset,
284 };
285 cols
286 ];
287 rows
288 ],
289 labels: Vec::new(),
290 }
291 }
292
293 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
294 self.layers.last_mut()
295 }
296
297 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
298 if x >= self.px_w || y >= self.px_h {
299 return;
300 }
301
302 let char_col = x / 2;
303 let char_row = y / 4;
304 let sub_col = x % 2;
305 let sub_row = y % 4;
306 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
307 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
308
309 let bit = if sub_col == 0 {
310 LEFT_BITS[sub_row]
311 } else {
312 RIGHT_BITS[sub_row]
313 };
314
315 if let Some(layer) = self.current_layer_mut() {
316 let cell = &mut layer.grid[char_row][char_col];
317 let new_bits = cell.bits | bit;
318 if new_bits != cell.bits {
319 cell.bits = new_bits;
320 cell.color = color;
321 }
322 }
323 }
324
325 fn dot_isize(&mut self, x: isize, y: isize) {
326 if x >= 0 && y >= 0 {
327 self.dot(x as usize, y as usize);
328 }
329 }
330
331 pub fn width(&self) -> usize {
333 self.px_w
334 }
335
336 pub fn height(&self) -> usize {
338 self.px_h
339 }
340
341 pub fn dot(&mut self, x: usize, y: usize) {
343 self.dot_with_color(x, y, self.current_color);
344 }
345
346 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
348 let (mut x, mut y) = (x0 as isize, y0 as isize);
349 let (x1, y1) = (x1 as isize, y1 as isize);
350 let dx = (x1 - x).abs();
351 let dy = -(y1 - y).abs();
352 let sx = if x < x1 { 1 } else { -1 };
353 let sy = if y < y1 { 1 } else { -1 };
354 let mut err = dx + dy;
355
356 loop {
357 self.dot_isize(x, y);
358 if x == x1 && y == y1 {
359 break;
360 }
361 let e2 = 2 * err;
362 if e2 >= dy {
363 err += dy;
364 x += sx;
365 }
366 if e2 <= dx {
367 err += dx;
368 y += sy;
369 }
370 }
371 }
372
373 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
375 if w == 0 || h == 0 {
376 return;
377 }
378
379 self.line(x, y, x + w.saturating_sub(1), y);
380 self.line(
381 x + w.saturating_sub(1),
382 y,
383 x + w.saturating_sub(1),
384 y + h.saturating_sub(1),
385 );
386 self.line(
387 x + w.saturating_sub(1),
388 y + h.saturating_sub(1),
389 x,
390 y + h.saturating_sub(1),
391 );
392 self.line(x, y + h.saturating_sub(1), x, y);
393 }
394
395 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
397 let mut x = r as isize;
398 let mut y: isize = 0;
399 let mut err: isize = 1 - x;
400 let (cx, cy) = (cx as isize, cy as isize);
401
402 while x >= y {
403 for &(dx, dy) in &[
404 (x, y),
405 (y, x),
406 (-x, y),
407 (-y, x),
408 (x, -y),
409 (y, -x),
410 (-x, -y),
411 (-y, -x),
412 ] {
413 let px = cx + dx;
414 let py = cy + dy;
415 self.dot_isize(px, py);
416 }
417
418 y += 1;
419 if err < 0 {
420 err += 2 * y + 1;
421 } else {
422 x -= 1;
423 err += 2 * (y - x) + 1;
424 }
425 }
426 }
427
428 pub fn set_color(&mut self, color: Color) {
430 self.current_color = color;
431 }
432
433 pub fn color(&self) -> Color {
435 self.current_color
436 }
437
438 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
440 if w == 0 || h == 0 {
441 return;
442 }
443
444 let x_end = x.saturating_add(w).min(self.px_w);
445 let y_end = y.saturating_add(h).min(self.px_h);
446 if x >= x_end || y >= y_end {
447 return;
448 }
449
450 for yy in y..y_end {
451 self.line(x, yy, x_end.saturating_sub(1), yy);
452 }
453 }
454
455 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
457 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
458 for y in (cy - r)..=(cy + r) {
459 let dy = y - cy;
460 let span_sq = (r * r - dy * dy).max(0);
461 let dx = (span_sq as f64).sqrt() as isize;
462 for x in (cx - dx)..=(cx + dx) {
463 self.dot_isize(x, y);
464 }
465 }
466 }
467
468 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
470 self.line(x0, y0, x1, y1);
471 self.line(x1, y1, x2, y2);
472 self.line(x2, y2, x0, y0);
473 }
474
475 pub fn filled_triangle(
477 &mut self,
478 x0: usize,
479 y0: usize,
480 x1: usize,
481 y1: usize,
482 x2: usize,
483 y2: usize,
484 ) {
485 let vertices = [
486 (x0 as isize, y0 as isize),
487 (x1 as isize, y1 as isize),
488 (x2 as isize, y2 as isize),
489 ];
490 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
491 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
492
493 for y in min_y..=max_y {
494 let mut intersections: Vec<f64> = Vec::new();
495
496 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
497 let (x_a, y_a) = vertices[edge.0];
498 let (x_b, y_b) = vertices[edge.1];
499 if y_a == y_b {
500 continue;
501 }
502
503 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
504 (x_a, y_a, x_b, y_b)
505 } else {
506 (x_b, y_b, x_a, y_a)
507 };
508
509 if y < y_start || y >= y_end {
510 continue;
511 }
512
513 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
514 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
515 }
516
517 intersections.sort_by(|a, b| a.total_cmp(b));
518 let mut i = 0usize;
519 while i + 1 < intersections.len() {
520 let x_start = intersections[i].ceil() as isize;
521 let x_end = intersections[i + 1].floor() as isize;
522 for x in x_start..=x_end {
523 self.dot_isize(x, y);
524 }
525 i += 2;
526 }
527 }
528
529 self.triangle(x0, y0, x1, y1, x2, y2);
530 }
531
532 pub fn points(&mut self, pts: &[(usize, usize)]) {
534 for &(x, y) in pts {
535 self.dot(x, y);
536 }
537 }
538
539 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
541 for window in pts.windows(2) {
542 if let [(x0, y0), (x1, y1)] = window {
543 self.line(*x0, *y0, *x1, *y1);
544 }
545 }
546 }
547
548 pub fn print(&mut self, x: usize, y: usize, text: &str) {
551 if text.is_empty() {
552 return;
553 }
554
555 let color = self.current_color;
556 if let Some(layer) = self.current_layer_mut() {
557 layer.labels.push(CanvasLabel {
558 x,
559 y,
560 text: text.to_string(),
561 color,
562 });
563 }
564 }
565
566 pub fn layer(&mut self) {
568 self.layers.push(Self::new_layer(self.cols, self.rows));
569 }
570
571 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
572 let mut final_grid = vec![
573 vec![
574 CanvasPixel {
575 bits: 0,
576 color: Color::Reset,
577 };
578 self.cols
579 ];
580 self.rows
581 ];
582 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
583 vec![vec![None; self.cols]; self.rows];
584
585 for layer in &self.layers {
586 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
587 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
588 let src = layer.grid[row][col];
589 if src.bits == 0 {
590 continue;
591 }
592
593 let merged = dst.bits | src.bits;
594 if merged != dst.bits {
595 dst.bits = merged;
596 dst.color = src.color;
597 }
598 }
599 }
600
601 for label in &layer.labels {
602 let row = label.y / 4;
603 if row >= self.rows {
604 continue;
605 }
606 let start_col = label.x / 2;
607 for (offset, ch) in label.text.chars().enumerate() {
608 let col = start_col + offset;
609 if col >= self.cols {
610 break;
611 }
612 labels_overlay[row][col] = Some((ch, label.color));
613 }
614 }
615 }
616
617 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
618 for row in 0..self.rows {
619 let mut segments: Vec<(String, Color)> = Vec::new();
620 let mut current_color: Option<Color> = None;
621 let mut current_text = String::new();
622
623 for col in 0..self.cols {
624 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
625 (label_ch, label_color)
626 } else {
627 let bits = final_grid[row][col].bits;
628 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
629 (ch, final_grid[row][col].color)
630 };
631
632 match current_color {
633 Some(c) if c == color => {
634 current_text.push(ch);
635 }
636 Some(c) => {
637 segments.push((std::mem::take(&mut current_text), c));
638 current_text.push(ch);
639 current_color = Some(color);
640 }
641 None => {
642 current_text.push(ch);
643 current_color = Some(color);
644 }
645 }
646 }
647
648 if let Some(color) = current_color {
649 segments.push((current_text, color));
650 }
651 lines.push(segments);
652 }
653
654 lines
655 }
656}
657
658impl<'a> ContainerBuilder<'a> {
659 pub fn border(mut self, border: Border) -> Self {
663 self.border = Some(border);
664 self
665 }
666
667 pub fn rounded(self) -> Self {
669 self.border(Border::Rounded)
670 }
671
672 pub fn border_style(mut self, style: Style) -> Self {
674 self.border_style = style;
675 self
676 }
677
678 pub fn p(self, value: u32) -> Self {
682 self.pad(value)
683 }
684
685 pub fn pad(mut self, value: u32) -> Self {
687 self.padding = Padding::all(value);
688 self
689 }
690
691 pub fn px(mut self, value: u32) -> Self {
693 self.padding.left = value;
694 self.padding.right = value;
695 self
696 }
697
698 pub fn py(mut self, value: u32) -> Self {
700 self.padding.top = value;
701 self.padding.bottom = value;
702 self
703 }
704
705 pub fn pt(mut self, value: u32) -> Self {
707 self.padding.top = value;
708 self
709 }
710
711 pub fn pr(mut self, value: u32) -> Self {
713 self.padding.right = value;
714 self
715 }
716
717 pub fn pb(mut self, value: u32) -> Self {
719 self.padding.bottom = value;
720 self
721 }
722
723 pub fn pl(mut self, value: u32) -> Self {
725 self.padding.left = value;
726 self
727 }
728
729 pub fn padding(mut self, padding: Padding) -> Self {
731 self.padding = padding;
732 self
733 }
734
735 pub fn m(mut self, value: u32) -> Self {
739 self.margin = Margin::all(value);
740 self
741 }
742
743 pub fn mx(mut self, value: u32) -> Self {
745 self.margin.left = value;
746 self.margin.right = value;
747 self
748 }
749
750 pub fn my(mut self, value: u32) -> Self {
752 self.margin.top = value;
753 self.margin.bottom = value;
754 self
755 }
756
757 pub fn mt(mut self, value: u32) -> Self {
759 self.margin.top = value;
760 self
761 }
762
763 pub fn mr(mut self, value: u32) -> Self {
765 self.margin.right = value;
766 self
767 }
768
769 pub fn mb(mut self, value: u32) -> Self {
771 self.margin.bottom = value;
772 self
773 }
774
775 pub fn ml(mut self, value: u32) -> Self {
777 self.margin.left = value;
778 self
779 }
780
781 pub fn margin(mut self, margin: Margin) -> Self {
783 self.margin = margin;
784 self
785 }
786
787 pub fn w(mut self, value: u32) -> Self {
791 self.constraints.min_width = Some(value);
792 self.constraints.max_width = Some(value);
793 self
794 }
795
796 pub fn h(mut self, value: u32) -> Self {
798 self.constraints.min_height = Some(value);
799 self.constraints.max_height = Some(value);
800 self
801 }
802
803 pub fn min_w(mut self, value: u32) -> Self {
805 self.constraints.min_width = Some(value);
806 self
807 }
808
809 pub fn max_w(mut self, value: u32) -> Self {
811 self.constraints.max_width = Some(value);
812 self
813 }
814
815 pub fn min_h(mut self, value: u32) -> Self {
817 self.constraints.min_height = Some(value);
818 self
819 }
820
821 pub fn max_h(mut self, value: u32) -> Self {
823 self.constraints.max_height = Some(value);
824 self
825 }
826
827 pub fn min_width(mut self, value: u32) -> Self {
829 self.constraints.min_width = Some(value);
830 self
831 }
832
833 pub fn max_width(mut self, value: u32) -> Self {
835 self.constraints.max_width = Some(value);
836 self
837 }
838
839 pub fn min_height(mut self, value: u32) -> Self {
841 self.constraints.min_height = Some(value);
842 self
843 }
844
845 pub fn max_height(mut self, value: u32) -> Self {
847 self.constraints.max_height = Some(value);
848 self
849 }
850
851 pub fn constraints(mut self, constraints: Constraints) -> Self {
853 self.constraints = constraints;
854 self
855 }
856
857 pub fn gap(mut self, gap: u32) -> Self {
861 self.gap = gap;
862 self
863 }
864
865 pub fn grow(mut self, grow: u16) -> Self {
867 self.grow = grow;
868 self
869 }
870
871 pub fn align(mut self, align: Align) -> Self {
875 self.align = align;
876 self
877 }
878
879 pub fn center(self) -> Self {
881 self.align(Align::Center)
882 }
883
884 pub fn title(self, title: impl Into<String>) -> Self {
888 self.title_styled(title, Style::new())
889 }
890
891 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
893 self.title = Some((title.into(), style));
894 self
895 }
896
897 pub fn scroll_offset(mut self, offset: u32) -> Self {
901 self.scroll_offset = Some(offset);
902 self
903 }
904
905 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
910 self.finish(Direction::Column, f)
911 }
912
913 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
918 self.finish(Direction::Row, f)
919 }
920
921 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
922 let interaction_id = self.ctx.interaction_count;
923 self.ctx.interaction_count += 1;
924
925 if let Some(scroll_offset) = self.scroll_offset {
926 self.ctx.commands.push(Command::BeginScrollable {
927 grow: self.grow,
928 border: self.border,
929 border_style: self.border_style,
930 padding: self.padding,
931 margin: self.margin,
932 constraints: self.constraints,
933 title: self.title,
934 scroll_offset,
935 });
936 } else {
937 self.ctx.commands.push(Command::BeginContainer {
938 direction,
939 gap: self.gap,
940 align: self.align,
941 border: self.border,
942 border_style: self.border_style,
943 padding: self.padding,
944 margin: self.margin,
945 constraints: self.constraints,
946 title: self.title,
947 grow: self.grow,
948 });
949 }
950 f(self.ctx);
951 self.ctx.commands.push(Command::EndContainer);
952 self.ctx.last_text_idx = None;
953
954 self.ctx.response_for(interaction_id)
955 }
956}
957
958impl Context {
959 #[allow(clippy::too_many_arguments)]
960 pub(crate) fn new(
961 events: Vec<Event>,
962 width: u32,
963 height: u32,
964 tick: u64,
965 focus_index: usize,
966 prev_focus_count: usize,
967 prev_scroll_infos: Vec<(u32, u32)>,
968 prev_hit_map: Vec<Rect>,
969 debug: bool,
970 theme: Theme,
971 last_mouse_pos: Option<(u32, u32)>,
972 ) -> Self {
973 let consumed = vec![false; events.len()];
974
975 let mut mouse_pos = last_mouse_pos;
976 let mut click_pos = None;
977 for event in &events {
978 if let Event::Mouse(mouse) = event {
979 mouse_pos = Some((mouse.x, mouse.y));
980 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
981 click_pos = Some((mouse.x, mouse.y));
982 }
983 }
984 }
985
986 Self {
987 commands: Vec::new(),
988 events,
989 consumed,
990 should_quit: false,
991 area_width: width,
992 area_height: height,
993 tick,
994 focus_index,
995 focus_count: 0,
996 prev_focus_count,
997 scroll_count: 0,
998 prev_scroll_infos,
999 interaction_count: 0,
1000 prev_hit_map,
1001 mouse_pos,
1002 click_pos,
1003 last_mouse_pos,
1004 last_text_idx: None,
1005 debug,
1006 theme,
1007 }
1008 }
1009
1010 pub(crate) fn process_focus_keys(&mut self) {
1011 for (i, event) in self.events.iter().enumerate() {
1012 if let Event::Key(key) = event {
1013 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1014 if self.prev_focus_count > 0 {
1015 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1016 }
1017 self.consumed[i] = true;
1018 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1019 || key.code == KeyCode::BackTab
1020 {
1021 if self.prev_focus_count > 0 {
1022 self.focus_index = if self.focus_index == 0 {
1023 self.prev_focus_count - 1
1024 } else {
1025 self.focus_index - 1
1026 };
1027 }
1028 self.consumed[i] = true;
1029 }
1030 }
1031 }
1032 }
1033
1034 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1038 w.ui(self)
1039 }
1040
1041 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1056 self.error_boundary_with(f, |ui, msg| {
1057 ui.styled(
1058 format!("⚠ Error: {msg}"),
1059 Style::new().fg(ui.theme.error).bold(),
1060 );
1061 });
1062 }
1063
1064 pub fn error_boundary_with(
1084 &mut self,
1085 f: impl FnOnce(&mut Context),
1086 fallback: impl FnOnce(&mut Context, String),
1087 ) {
1088 let cmd_count = self.commands.len();
1089 let last_text_idx = self.last_text_idx;
1090
1091 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1092 f(self);
1093 }));
1094
1095 match result {
1096 Ok(()) => {}
1097 Err(panic_info) => {
1098 self.commands.truncate(cmd_count);
1099 self.last_text_idx = last_text_idx;
1100
1101 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1102 (*s).to_string()
1103 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1104 s.clone()
1105 } else {
1106 "widget panicked".to_string()
1107 };
1108
1109 fallback(self, msg);
1110 }
1111 }
1112 }
1113
1114 pub fn interaction(&mut self) -> Response {
1120 let id = self.interaction_count;
1121 self.interaction_count += 1;
1122 self.response_for(id)
1123 }
1124
1125 pub fn register_focusable(&mut self) -> bool {
1130 let id = self.focus_count;
1131 self.focus_count += 1;
1132 if self.prev_focus_count == 0 {
1133 return true;
1134 }
1135 self.focus_index % self.prev_focus_count == id
1136 }
1137
1138 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1151 let content = s.into();
1152 self.commands.push(Command::Text {
1153 content,
1154 style: Style::new(),
1155 grow: 0,
1156 align: Align::Start,
1157 wrap: false,
1158 margin: Margin::default(),
1159 constraints: Constraints::default(),
1160 });
1161 self.last_text_idx = Some(self.commands.len() - 1);
1162 self
1163 }
1164
1165 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1170 let content = s.into();
1171 self.commands.push(Command::Text {
1172 content,
1173 style: Style::new(),
1174 grow: 0,
1175 align: Align::Start,
1176 wrap: true,
1177 margin: Margin::default(),
1178 constraints: Constraints::default(),
1179 });
1180 self.last_text_idx = Some(self.commands.len() - 1);
1181 self
1182 }
1183
1184 pub fn bold(&mut self) -> &mut Self {
1188 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1189 self
1190 }
1191
1192 pub fn dim(&mut self) -> &mut Self {
1197 let text_dim = self.theme.text_dim;
1198 self.modify_last_style(|s| {
1199 s.modifiers |= Modifiers::DIM;
1200 if s.fg.is_none() {
1201 s.fg = Some(text_dim);
1202 }
1203 });
1204 self
1205 }
1206
1207 pub fn italic(&mut self) -> &mut Self {
1209 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1210 self
1211 }
1212
1213 pub fn underline(&mut self) -> &mut Self {
1215 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1216 self
1217 }
1218
1219 pub fn reversed(&mut self) -> &mut Self {
1221 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1222 self
1223 }
1224
1225 pub fn strikethrough(&mut self) -> &mut Self {
1227 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1228 self
1229 }
1230
1231 pub fn fg(&mut self, color: Color) -> &mut Self {
1233 self.modify_last_style(|s| s.fg = Some(color));
1234 self
1235 }
1236
1237 pub fn bg(&mut self, color: Color) -> &mut Self {
1239 self.modify_last_style(|s| s.bg = Some(color));
1240 self
1241 }
1242
1243 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
1248 self.commands.push(Command::Text {
1249 content: s.into(),
1250 style,
1251 grow: 0,
1252 align: Align::Start,
1253 wrap: false,
1254 margin: Margin::default(),
1255 constraints: Constraints::default(),
1256 });
1257 self.last_text_idx = Some(self.commands.len() - 1);
1258 self
1259 }
1260
1261 pub fn wrap(&mut self) -> &mut Self {
1263 if let Some(idx) = self.last_text_idx {
1264 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1265 *wrap = true;
1266 }
1267 }
1268 self
1269 }
1270
1271 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1272 if let Some(idx) = self.last_text_idx {
1273 if let Command::Text { style, .. } = &mut self.commands[idx] {
1274 f(style);
1275 }
1276 }
1277 }
1278
1279 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1297 self.push_container(Direction::Column, 0, f)
1298 }
1299
1300 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1304 self.push_container(Direction::Column, gap, f)
1305 }
1306
1307 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1324 self.push_container(Direction::Row, 0, f)
1325 }
1326
1327 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1331 self.push_container(Direction::Row, gap, f)
1332 }
1333
1334 pub fn container(&mut self) -> ContainerBuilder<'_> {
1355 let border = self.theme.border;
1356 ContainerBuilder {
1357 ctx: self,
1358 gap: 0,
1359 align: Align::Start,
1360 border: None,
1361 border_style: Style::new().fg(border),
1362 padding: Padding::default(),
1363 margin: Margin::default(),
1364 constraints: Constraints::default(),
1365 title: None,
1366 grow: 0,
1367 scroll_offset: None,
1368 }
1369 }
1370
1371 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1390 let index = self.scroll_count;
1391 self.scroll_count += 1;
1392 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1393 state.set_bounds(ch, vh);
1394 let max = ch.saturating_sub(vh) as usize;
1395 state.offset = state.offset.min(max);
1396 }
1397
1398 let next_id = self.interaction_count;
1399 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1400 self.auto_scroll(&rect, state);
1401 }
1402
1403 self.container().scroll_offset(state.offset as u32)
1404 }
1405
1406 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1407 let last_y = self.last_mouse_pos.map(|(_, y)| y);
1408 let mut to_consume: Vec<usize> = Vec::new();
1409
1410 for (i, event) in self.events.iter().enumerate() {
1411 if self.consumed[i] {
1412 continue;
1413 }
1414 if let Event::Mouse(mouse) = event {
1415 let in_bounds = mouse.x >= rect.x
1416 && mouse.x < rect.right()
1417 && mouse.y >= rect.y
1418 && mouse.y < rect.bottom();
1419 if !in_bounds {
1420 continue;
1421 }
1422 match mouse.kind {
1423 MouseKind::ScrollUp => {
1424 state.scroll_up(1);
1425 to_consume.push(i);
1426 }
1427 MouseKind::ScrollDown => {
1428 state.scroll_down(1);
1429 to_consume.push(i);
1430 }
1431 MouseKind::Drag(MouseButton::Left) => {
1432 if let Some(prev_y) = last_y {
1433 let delta = mouse.y as i32 - prev_y as i32;
1434 if delta < 0 {
1435 state.scroll_down((-delta) as usize);
1436 } else if delta > 0 {
1437 state.scroll_up(delta as usize);
1438 }
1439 }
1440 to_consume.push(i);
1441 }
1442 _ => {}
1443 }
1444 }
1445 }
1446
1447 for i in to_consume {
1448 self.consumed[i] = true;
1449 }
1450 }
1451
1452 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1456 self.container().border(border)
1457 }
1458
1459 fn push_container(
1460 &mut self,
1461 direction: Direction,
1462 gap: u32,
1463 f: impl FnOnce(&mut Context),
1464 ) -> Response {
1465 let interaction_id = self.interaction_count;
1466 self.interaction_count += 1;
1467 let border = self.theme.border;
1468
1469 self.commands.push(Command::BeginContainer {
1470 direction,
1471 gap,
1472 align: Align::Start,
1473 border: None,
1474 border_style: Style::new().fg(border),
1475 padding: Padding::default(),
1476 margin: Margin::default(),
1477 constraints: Constraints::default(),
1478 title: None,
1479 grow: 0,
1480 });
1481 f(self);
1482 self.commands.push(Command::EndContainer);
1483 self.last_text_idx = None;
1484
1485 self.response_for(interaction_id)
1486 }
1487
1488 fn response_for(&self, interaction_id: usize) -> Response {
1489 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1490 let clicked = self
1491 .click_pos
1492 .map(|(mx, my)| {
1493 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1494 })
1495 .unwrap_or(false);
1496 let hovered = self
1497 .mouse_pos
1498 .map(|(mx, my)| {
1499 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1500 })
1501 .unwrap_or(false);
1502 Response { clicked, hovered }
1503 } else {
1504 Response::default()
1505 }
1506 }
1507
1508 pub fn grow(&mut self, value: u16) -> &mut Self {
1513 if let Some(idx) = self.last_text_idx {
1514 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1515 *grow = value;
1516 }
1517 }
1518 self
1519 }
1520
1521 pub fn align(&mut self, align: Align) -> &mut Self {
1523 if let Some(idx) = self.last_text_idx {
1524 if let Command::Text {
1525 align: text_align, ..
1526 } = &mut self.commands[idx]
1527 {
1528 *text_align = align;
1529 }
1530 }
1531 self
1532 }
1533
1534 pub fn spacer(&mut self) -> &mut Self {
1538 self.commands.push(Command::Spacer { grow: 1 });
1539 self.last_text_idx = None;
1540 self
1541 }
1542
1543 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1559 let focused = self.register_focusable();
1560 state.cursor = state.cursor.min(state.value.chars().count());
1561
1562 if focused {
1563 let mut consumed_indices = Vec::new();
1564 for (i, event) in self.events.iter().enumerate() {
1565 if let Event::Key(key) = event {
1566 match key.code {
1567 KeyCode::Char(ch) => {
1568 if let Some(max) = state.max_length {
1569 if state.value.chars().count() >= max {
1570 continue;
1571 }
1572 }
1573 let index = byte_index_for_char(&state.value, state.cursor);
1574 state.value.insert(index, ch);
1575 state.cursor += 1;
1576 consumed_indices.push(i);
1577 }
1578 KeyCode::Backspace => {
1579 if state.cursor > 0 {
1580 let start = byte_index_for_char(&state.value, state.cursor - 1);
1581 let end = byte_index_for_char(&state.value, state.cursor);
1582 state.value.replace_range(start..end, "");
1583 state.cursor -= 1;
1584 }
1585 consumed_indices.push(i);
1586 }
1587 KeyCode::Left => {
1588 state.cursor = state.cursor.saturating_sub(1);
1589 consumed_indices.push(i);
1590 }
1591 KeyCode::Right => {
1592 state.cursor = (state.cursor + 1).min(state.value.chars().count());
1593 consumed_indices.push(i);
1594 }
1595 KeyCode::Home => {
1596 state.cursor = 0;
1597 consumed_indices.push(i);
1598 }
1599 KeyCode::Delete => {
1600 let len = state.value.chars().count();
1601 if state.cursor < len {
1602 let start = byte_index_for_char(&state.value, state.cursor);
1603 let end = byte_index_for_char(&state.value, state.cursor + 1);
1604 state.value.replace_range(start..end, "");
1605 }
1606 consumed_indices.push(i);
1607 }
1608 KeyCode::End => {
1609 state.cursor = state.value.chars().count();
1610 consumed_indices.push(i);
1611 }
1612 _ => {}
1613 }
1614 }
1615 if let Event::Paste(ref text) = event {
1616 for ch in text.chars() {
1617 if let Some(max) = state.max_length {
1618 if state.value.chars().count() >= max {
1619 break;
1620 }
1621 }
1622 let index = byte_index_for_char(&state.value, state.cursor);
1623 state.value.insert(index, ch);
1624 state.cursor += 1;
1625 }
1626 consumed_indices.push(i);
1627 }
1628 }
1629
1630 for index in consumed_indices {
1631 self.consumed[index] = true;
1632 }
1633 }
1634
1635 if state.value.is_empty() {
1636 self.styled(
1637 state.placeholder.clone(),
1638 Style::new().dim().fg(self.theme.text_dim),
1639 )
1640 } else {
1641 let mut rendered = String::new();
1642 for (idx, ch) in state.value.chars().enumerate() {
1643 if focused && idx == state.cursor {
1644 rendered.push('▎');
1645 }
1646 rendered.push(ch);
1647 }
1648 if focused && state.cursor >= state.value.chars().count() {
1649 rendered.push('▎');
1650 }
1651 self.styled(rendered, Style::new().fg(self.theme.text))
1652 }
1653 }
1654
1655 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1661 self.styled(
1662 state.frame(self.tick).to_string(),
1663 Style::new().fg(self.theme.primary),
1664 )
1665 }
1666
1667 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1672 state.cleanup(self.tick);
1673 if state.messages.is_empty() {
1674 return self;
1675 }
1676
1677 self.interaction_count += 1;
1678 self.commands.push(Command::BeginContainer {
1679 direction: Direction::Column,
1680 gap: 0,
1681 align: Align::Start,
1682 border: None,
1683 border_style: Style::new().fg(self.theme.border),
1684 padding: Padding::default(),
1685 margin: Margin::default(),
1686 constraints: Constraints::default(),
1687 title: None,
1688 grow: 0,
1689 });
1690 for message in state.messages.iter().rev() {
1691 let color = match message.level {
1692 ToastLevel::Info => self.theme.primary,
1693 ToastLevel::Success => self.theme.success,
1694 ToastLevel::Warning => self.theme.warning,
1695 ToastLevel::Error => self.theme.error,
1696 };
1697 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
1698 }
1699 self.commands.push(Command::EndContainer);
1700 self.last_text_idx = None;
1701
1702 self
1703 }
1704
1705 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1710 if state.lines.is_empty() {
1711 state.lines.push(String::new());
1712 }
1713 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1714 state.cursor_col = state
1715 .cursor_col
1716 .min(state.lines[state.cursor_row].chars().count());
1717
1718 let focused = self.register_focusable();
1719
1720 if focused {
1721 let mut consumed_indices = Vec::new();
1722 for (i, event) in self.events.iter().enumerate() {
1723 if let Event::Key(key) = event {
1724 match key.code {
1725 KeyCode::Char(ch) => {
1726 if let Some(max) = state.max_length {
1727 let total: usize =
1728 state.lines.iter().map(|line| line.chars().count()).sum();
1729 if total >= max {
1730 continue;
1731 }
1732 }
1733 let index = byte_index_for_char(
1734 &state.lines[state.cursor_row],
1735 state.cursor_col,
1736 );
1737 state.lines[state.cursor_row].insert(index, ch);
1738 state.cursor_col += 1;
1739 consumed_indices.push(i);
1740 }
1741 KeyCode::Enter => {
1742 let split_index = byte_index_for_char(
1743 &state.lines[state.cursor_row],
1744 state.cursor_col,
1745 );
1746 let remainder = state.lines[state.cursor_row].split_off(split_index);
1747 state.cursor_row += 1;
1748 state.lines.insert(state.cursor_row, remainder);
1749 state.cursor_col = 0;
1750 consumed_indices.push(i);
1751 }
1752 KeyCode::Backspace => {
1753 if state.cursor_col > 0 {
1754 let start = byte_index_for_char(
1755 &state.lines[state.cursor_row],
1756 state.cursor_col - 1,
1757 );
1758 let end = byte_index_for_char(
1759 &state.lines[state.cursor_row],
1760 state.cursor_col,
1761 );
1762 state.lines[state.cursor_row].replace_range(start..end, "");
1763 state.cursor_col -= 1;
1764 } else if state.cursor_row > 0 {
1765 let current = state.lines.remove(state.cursor_row);
1766 state.cursor_row -= 1;
1767 state.cursor_col = state.lines[state.cursor_row].chars().count();
1768 state.lines[state.cursor_row].push_str(¤t);
1769 }
1770 consumed_indices.push(i);
1771 }
1772 KeyCode::Left => {
1773 if state.cursor_col > 0 {
1774 state.cursor_col -= 1;
1775 } else if state.cursor_row > 0 {
1776 state.cursor_row -= 1;
1777 state.cursor_col = state.lines[state.cursor_row].chars().count();
1778 }
1779 consumed_indices.push(i);
1780 }
1781 KeyCode::Right => {
1782 let line_len = state.lines[state.cursor_row].chars().count();
1783 if state.cursor_col < line_len {
1784 state.cursor_col += 1;
1785 } else if state.cursor_row + 1 < state.lines.len() {
1786 state.cursor_row += 1;
1787 state.cursor_col = 0;
1788 }
1789 consumed_indices.push(i);
1790 }
1791 KeyCode::Up => {
1792 if state.cursor_row > 0 {
1793 state.cursor_row -= 1;
1794 state.cursor_col = state
1795 .cursor_col
1796 .min(state.lines[state.cursor_row].chars().count());
1797 }
1798 consumed_indices.push(i);
1799 }
1800 KeyCode::Down => {
1801 if state.cursor_row + 1 < state.lines.len() {
1802 state.cursor_row += 1;
1803 state.cursor_col = state
1804 .cursor_col
1805 .min(state.lines[state.cursor_row].chars().count());
1806 }
1807 consumed_indices.push(i);
1808 }
1809 KeyCode::Home => {
1810 state.cursor_col = 0;
1811 consumed_indices.push(i);
1812 }
1813 KeyCode::Delete => {
1814 let line_len = state.lines[state.cursor_row].chars().count();
1815 if state.cursor_col < line_len {
1816 let start = byte_index_for_char(
1817 &state.lines[state.cursor_row],
1818 state.cursor_col,
1819 );
1820 let end = byte_index_for_char(
1821 &state.lines[state.cursor_row],
1822 state.cursor_col + 1,
1823 );
1824 state.lines[state.cursor_row].replace_range(start..end, "");
1825 } else if state.cursor_row + 1 < state.lines.len() {
1826 let next = state.lines.remove(state.cursor_row + 1);
1827 state.lines[state.cursor_row].push_str(&next);
1828 }
1829 consumed_indices.push(i);
1830 }
1831 KeyCode::End => {
1832 state.cursor_col = state.lines[state.cursor_row].chars().count();
1833 consumed_indices.push(i);
1834 }
1835 _ => {}
1836 }
1837 }
1838 if let Event::Paste(ref text) = event {
1839 for ch in text.chars() {
1840 if ch == '\n' || ch == '\r' {
1841 let split_index = byte_index_for_char(
1842 &state.lines[state.cursor_row],
1843 state.cursor_col,
1844 );
1845 let remainder = state.lines[state.cursor_row].split_off(split_index);
1846 state.cursor_row += 1;
1847 state.lines.insert(state.cursor_row, remainder);
1848 state.cursor_col = 0;
1849 } else {
1850 if let Some(max) = state.max_length {
1851 let total: usize =
1852 state.lines.iter().map(|l| l.chars().count()).sum();
1853 if total >= max {
1854 break;
1855 }
1856 }
1857 let index = byte_index_for_char(
1858 &state.lines[state.cursor_row],
1859 state.cursor_col,
1860 );
1861 state.lines[state.cursor_row].insert(index, ch);
1862 state.cursor_col += 1;
1863 }
1864 }
1865 consumed_indices.push(i);
1866 }
1867 }
1868
1869 for index in consumed_indices {
1870 self.consumed[index] = true;
1871 }
1872 }
1873
1874 self.interaction_count += 1;
1875 self.commands.push(Command::BeginContainer {
1876 direction: Direction::Column,
1877 gap: 0,
1878 align: Align::Start,
1879 border: None,
1880 border_style: Style::new().fg(self.theme.border),
1881 padding: Padding::default(),
1882 margin: Margin::default(),
1883 constraints: Constraints::default(),
1884 title: None,
1885 grow: 0,
1886 });
1887 for row in 0..visible_rows as usize {
1888 let line = state.lines.get(row).cloned().unwrap_or_default();
1889 let mut rendered = line.clone();
1890 let mut style = if line.is_empty() {
1891 Style::new().fg(self.theme.text_dim)
1892 } else {
1893 Style::new().fg(self.theme.text)
1894 };
1895
1896 if focused && row == state.cursor_row {
1897 rendered.clear();
1898 for (idx, ch) in line.chars().enumerate() {
1899 if idx == state.cursor_col {
1900 rendered.push('▎');
1901 }
1902 rendered.push(ch);
1903 }
1904 if state.cursor_col >= line.chars().count() {
1905 rendered.push('▎');
1906 }
1907 style = Style::new().fg(self.theme.text);
1908 }
1909
1910 self.styled(rendered, style);
1911 }
1912 self.commands.push(Command::EndContainer);
1913 self.last_text_idx = None;
1914
1915 self
1916 }
1917
1918 pub fn progress(&mut self, ratio: f64) -> &mut Self {
1923 self.progress_bar(ratio, 20)
1924 }
1925
1926 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1931 let clamped = ratio.clamp(0.0, 1.0);
1932 let filled = (clamped * width as f64).round() as u32;
1933 let empty = width.saturating_sub(filled);
1934 let mut bar = String::new();
1935 for _ in 0..filled {
1936 bar.push('█');
1937 }
1938 for _ in 0..empty {
1939 bar.push('░');
1940 }
1941 self.text(bar)
1942 }
1943
1944 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1965 if data.is_empty() {
1966 return self;
1967 }
1968
1969 let max_label_width = data
1970 .iter()
1971 .map(|(label, _)| UnicodeWidthStr::width(*label))
1972 .max()
1973 .unwrap_or(0);
1974 let max_value = data
1975 .iter()
1976 .map(|(_, value)| *value)
1977 .fold(f64::NEG_INFINITY, f64::max);
1978 let denom = if max_value > 0.0 { max_value } else { 1.0 };
1979
1980 self.interaction_count += 1;
1981 self.commands.push(Command::BeginContainer {
1982 direction: Direction::Column,
1983 gap: 0,
1984 align: Align::Start,
1985 border: None,
1986 border_style: Style::new().fg(self.theme.border),
1987 padding: Padding::default(),
1988 margin: Margin::default(),
1989 constraints: Constraints::default(),
1990 title: None,
1991 grow: 0,
1992 });
1993
1994 for (label, value) in data {
1995 let label_width = UnicodeWidthStr::width(*label);
1996 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
1997 let normalized = (*value / denom).clamp(0.0, 1.0);
1998 let bar_len = (normalized * max_width as f64).round() as usize;
1999 let bar = "█".repeat(bar_len);
2000
2001 self.interaction_count += 1;
2002 self.commands.push(Command::BeginContainer {
2003 direction: Direction::Row,
2004 gap: 1,
2005 align: Align::Start,
2006 border: None,
2007 border_style: Style::new().fg(self.theme.border),
2008 padding: Padding::default(),
2009 margin: Margin::default(),
2010 constraints: Constraints::default(),
2011 title: None,
2012 grow: 0,
2013 });
2014 self.styled(
2015 format!("{label}{label_padding}"),
2016 Style::new().fg(self.theme.text),
2017 );
2018 self.styled(bar, Style::new().fg(self.theme.primary));
2019 self.styled(
2020 format_compact_number(*value),
2021 Style::new().fg(self.theme.text_dim),
2022 );
2023 self.commands.push(Command::EndContainer);
2024 self.last_text_idx = None;
2025 }
2026
2027 self.commands.push(Command::EndContainer);
2028 self.last_text_idx = None;
2029
2030 self
2031 }
2032
2033 pub fn bar_chart_styled(
2049 &mut self,
2050 bars: &[Bar],
2051 max_width: u32,
2052 direction: BarDirection,
2053 ) -> &mut Self {
2054 if bars.is_empty() {
2055 return self;
2056 }
2057
2058 let max_value = bars
2059 .iter()
2060 .map(|bar| bar.value)
2061 .fold(f64::NEG_INFINITY, f64::max);
2062 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2063
2064 match direction {
2065 BarDirection::Horizontal => {
2066 let max_label_width = bars
2067 .iter()
2068 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2069 .max()
2070 .unwrap_or(0);
2071
2072 self.interaction_count += 1;
2073 self.commands.push(Command::BeginContainer {
2074 direction: Direction::Column,
2075 gap: 0,
2076 align: Align::Start,
2077 border: None,
2078 border_style: Style::new().fg(self.theme.border),
2079 padding: Padding::default(),
2080 margin: Margin::default(),
2081 constraints: Constraints::default(),
2082 title: None,
2083 grow: 0,
2084 });
2085
2086 for bar in bars {
2087 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2088 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2089 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2090 let bar_len = (normalized * max_width as f64).round() as usize;
2091 let bar_text = "█".repeat(bar_len);
2092 let color = bar.color.unwrap_or(self.theme.primary);
2093
2094 self.interaction_count += 1;
2095 self.commands.push(Command::BeginContainer {
2096 direction: Direction::Row,
2097 gap: 1,
2098 align: Align::Start,
2099 border: None,
2100 border_style: Style::new().fg(self.theme.border),
2101 padding: Padding::default(),
2102 margin: Margin::default(),
2103 constraints: Constraints::default(),
2104 title: None,
2105 grow: 0,
2106 });
2107 self.styled(
2108 format!("{}{label_padding}", bar.label),
2109 Style::new().fg(self.theme.text),
2110 );
2111 self.styled(bar_text, Style::new().fg(color));
2112 self.styled(
2113 format_compact_number(bar.value),
2114 Style::new().fg(self.theme.text_dim),
2115 );
2116 self.commands.push(Command::EndContainer);
2117 self.last_text_idx = None;
2118 }
2119
2120 self.commands.push(Command::EndContainer);
2121 self.last_text_idx = None;
2122 }
2123 BarDirection::Vertical => {
2124 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
2125
2126 let chart_height = max_width.max(1) as usize;
2127 let value_labels: Vec<String> = bars
2128 .iter()
2129 .map(|bar| format_compact_number(bar.value))
2130 .collect();
2131 let col_width = bars
2132 .iter()
2133 .zip(value_labels.iter())
2134 .map(|(bar, value)| {
2135 UnicodeWidthStr::width(bar.label.as_str())
2136 .max(UnicodeWidthStr::width(value.as_str()))
2137 .max(1)
2138 })
2139 .max()
2140 .unwrap_or(1);
2141
2142 let bar_units: Vec<usize> = bars
2143 .iter()
2144 .map(|bar| {
2145 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2146 (normalized * chart_height as f64 * 8.0).round() as usize
2147 })
2148 .collect();
2149
2150 self.interaction_count += 1;
2151 self.commands.push(Command::BeginContainer {
2152 direction: Direction::Column,
2153 gap: 0,
2154 align: Align::Start,
2155 border: None,
2156 border_style: Style::new().fg(self.theme.border),
2157 padding: Padding::default(),
2158 margin: Margin::default(),
2159 constraints: Constraints::default(),
2160 title: None,
2161 grow: 0,
2162 });
2163
2164 self.interaction_count += 1;
2165 self.commands.push(Command::BeginContainer {
2166 direction: Direction::Row,
2167 gap: 1,
2168 align: Align::Start,
2169 border: None,
2170 border_style: Style::new().fg(self.theme.border),
2171 padding: Padding::default(),
2172 margin: Margin::default(),
2173 constraints: Constraints::default(),
2174 title: None,
2175 grow: 0,
2176 });
2177 for value in &value_labels {
2178 self.styled(
2179 center_text(value, col_width),
2180 Style::new().fg(self.theme.text_dim),
2181 );
2182 }
2183 self.commands.push(Command::EndContainer);
2184 self.last_text_idx = None;
2185
2186 for row in (0..chart_height).rev() {
2187 self.interaction_count += 1;
2188 self.commands.push(Command::BeginContainer {
2189 direction: Direction::Row,
2190 gap: 1,
2191 align: Align::Start,
2192 border: None,
2193 border_style: Style::new().fg(self.theme.border),
2194 padding: Padding::default(),
2195 margin: Margin::default(),
2196 constraints: Constraints::default(),
2197 title: None,
2198 grow: 0,
2199 });
2200
2201 let row_base = row * 8;
2202 for (bar, units) in bars.iter().zip(bar_units.iter()) {
2203 let fill = if *units <= row_base {
2204 ' '
2205 } else {
2206 let delta = *units - row_base;
2207 if delta >= 8 {
2208 '█'
2209 } else {
2210 FRACTION_BLOCKS[delta]
2211 }
2212 };
2213
2214 self.styled(
2215 center_text(&fill.to_string(), col_width),
2216 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2217 );
2218 }
2219
2220 self.commands.push(Command::EndContainer);
2221 self.last_text_idx = None;
2222 }
2223
2224 self.interaction_count += 1;
2225 self.commands.push(Command::BeginContainer {
2226 direction: Direction::Row,
2227 gap: 1,
2228 align: Align::Start,
2229 border: None,
2230 border_style: Style::new().fg(self.theme.border),
2231 padding: Padding::default(),
2232 margin: Margin::default(),
2233 constraints: Constraints::default(),
2234 title: None,
2235 grow: 0,
2236 });
2237 for bar in bars {
2238 self.styled(
2239 center_text(&bar.label, col_width),
2240 Style::new().fg(self.theme.text),
2241 );
2242 }
2243 self.commands.push(Command::EndContainer);
2244 self.last_text_idx = None;
2245
2246 self.commands.push(Command::EndContainer);
2247 self.last_text_idx = None;
2248 }
2249 }
2250
2251 self
2252 }
2253
2254 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
2271 if groups.is_empty() {
2272 return self;
2273 }
2274
2275 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
2276 if all_bars.is_empty() {
2277 return self;
2278 }
2279
2280 let max_label_width = all_bars
2281 .iter()
2282 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2283 .max()
2284 .unwrap_or(0);
2285 let max_value = all_bars
2286 .iter()
2287 .map(|bar| bar.value)
2288 .fold(f64::NEG_INFINITY, f64::max);
2289 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2290
2291 self.interaction_count += 1;
2292 self.commands.push(Command::BeginContainer {
2293 direction: Direction::Column,
2294 gap: 1,
2295 align: Align::Start,
2296 border: None,
2297 border_style: Style::new().fg(self.theme.border),
2298 padding: Padding::default(),
2299 margin: Margin::default(),
2300 constraints: Constraints::default(),
2301 title: None,
2302 grow: 0,
2303 });
2304
2305 for group in groups {
2306 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
2307
2308 for bar in &group.bars {
2309 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2310 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2311 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2312 let bar_len = (normalized * max_width as f64).round() as usize;
2313 let bar_text = "█".repeat(bar_len);
2314
2315 self.interaction_count += 1;
2316 self.commands.push(Command::BeginContainer {
2317 direction: Direction::Row,
2318 gap: 1,
2319 align: Align::Start,
2320 border: None,
2321 border_style: Style::new().fg(self.theme.border),
2322 padding: Padding::default(),
2323 margin: Margin::default(),
2324 constraints: Constraints::default(),
2325 title: None,
2326 grow: 0,
2327 });
2328 self.styled(
2329 format!(" {}{label_padding}", bar.label),
2330 Style::new().fg(self.theme.text),
2331 );
2332 self.styled(
2333 bar_text,
2334 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2335 );
2336 self.styled(
2337 format_compact_number(bar.value),
2338 Style::new().fg(self.theme.text_dim),
2339 );
2340 self.commands.push(Command::EndContainer);
2341 self.last_text_idx = None;
2342 }
2343 }
2344
2345 self.commands.push(Command::EndContainer);
2346 self.last_text_idx = None;
2347
2348 self
2349 }
2350
2351 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
2367 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2368
2369 let w = width as usize;
2370 let window = if data.len() > w {
2371 &data[data.len() - w..]
2372 } else {
2373 data
2374 };
2375
2376 if window.is_empty() {
2377 return self;
2378 }
2379
2380 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
2381 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2382 let range = max - min;
2383
2384 let line: String = window
2385 .iter()
2386 .map(|&value| {
2387 let normalized = if range == 0.0 {
2388 0.5
2389 } else {
2390 (value - min) / range
2391 };
2392 let idx = (normalized * 7.0).round() as usize;
2393 BLOCKS[idx.min(7)]
2394 })
2395 .collect();
2396
2397 self.styled(line, Style::new().fg(self.theme.primary))
2398 }
2399
2400 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
2420 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2421
2422 let w = width as usize;
2423 let window = if data.len() > w {
2424 &data[data.len() - w..]
2425 } else {
2426 data
2427 };
2428
2429 if window.is_empty() {
2430 return self;
2431 }
2432
2433 let mut finite_values = window
2434 .iter()
2435 .map(|(value, _)| *value)
2436 .filter(|value| !value.is_nan());
2437 let Some(first) = finite_values.next() else {
2438 return self.styled(
2439 " ".repeat(window.len()),
2440 Style::new().fg(self.theme.text_dim),
2441 );
2442 };
2443
2444 let mut min = first;
2445 let mut max = first;
2446 for value in finite_values {
2447 min = f64::min(min, value);
2448 max = f64::max(max, value);
2449 }
2450 let range = max - min;
2451
2452 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
2453 for (value, color) in window {
2454 if value.is_nan() {
2455 cells.push((' ', self.theme.text_dim));
2456 continue;
2457 }
2458
2459 let normalized = if range == 0.0 {
2460 0.5
2461 } else {
2462 ((*value - min) / range).clamp(0.0, 1.0)
2463 };
2464 let idx = (normalized * 7.0).round() as usize;
2465 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
2466 }
2467
2468 self.interaction_count += 1;
2469 self.commands.push(Command::BeginContainer {
2470 direction: Direction::Row,
2471 gap: 0,
2472 align: Align::Start,
2473 border: None,
2474 border_style: Style::new().fg(self.theme.border),
2475 padding: Padding::default(),
2476 margin: Margin::default(),
2477 constraints: Constraints::default(),
2478 title: None,
2479 grow: 0,
2480 });
2481
2482 let mut seg = String::new();
2483 let mut seg_color = cells[0].1;
2484 for (ch, color) in cells {
2485 if color != seg_color {
2486 self.styled(seg, Style::new().fg(seg_color));
2487 seg = String::new();
2488 seg_color = color;
2489 }
2490 seg.push(ch);
2491 }
2492 if !seg.is_empty() {
2493 self.styled(seg, Style::new().fg(seg_color));
2494 }
2495
2496 self.commands.push(Command::EndContainer);
2497 self.last_text_idx = None;
2498
2499 self
2500 }
2501
2502 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2516 if data.is_empty() || width == 0 || height == 0 {
2517 return self;
2518 }
2519
2520 let cols = width as usize;
2521 let rows = height as usize;
2522 let px_w = cols * 2;
2523 let px_h = rows * 4;
2524
2525 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
2526 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2527 let range = if (max - min).abs() < f64::EPSILON {
2528 1.0
2529 } else {
2530 max - min
2531 };
2532
2533 let points: Vec<usize> = (0..px_w)
2534 .map(|px| {
2535 let data_idx = if px_w <= 1 {
2536 0.0
2537 } else {
2538 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
2539 };
2540 let idx = data_idx.floor() as usize;
2541 let frac = data_idx - idx as f64;
2542 let value = if idx + 1 < data.len() {
2543 data[idx] * (1.0 - frac) + data[idx + 1] * frac
2544 } else {
2545 data[idx.min(data.len() - 1)]
2546 };
2547
2548 let normalized = (value - min) / range;
2549 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
2550 py.min(px_h - 1)
2551 })
2552 .collect();
2553
2554 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
2555 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
2556
2557 let mut grid = vec![vec![0u32; cols]; rows];
2558
2559 for i in 0..points.len() {
2560 let px = i;
2561 let py = points[i];
2562 let char_col = px / 2;
2563 let char_row = py / 4;
2564 let sub_col = px % 2;
2565 let sub_row = py % 4;
2566
2567 if char_col < cols && char_row < rows {
2568 grid[char_row][char_col] |= if sub_col == 0 {
2569 LEFT_BITS[sub_row]
2570 } else {
2571 RIGHT_BITS[sub_row]
2572 };
2573 }
2574
2575 if i + 1 < points.len() {
2576 let py_next = points[i + 1];
2577 let (y_start, y_end) = if py <= py_next {
2578 (py, py_next)
2579 } else {
2580 (py_next, py)
2581 };
2582 for y in y_start..=y_end {
2583 let cell_row = y / 4;
2584 let sub_y = y % 4;
2585 if char_col < cols && cell_row < rows {
2586 grid[cell_row][char_col] |= if sub_col == 0 {
2587 LEFT_BITS[sub_y]
2588 } else {
2589 RIGHT_BITS[sub_y]
2590 };
2591 }
2592 }
2593 }
2594 }
2595
2596 let style = Style::new().fg(self.theme.primary);
2597 for row in grid {
2598 let line: String = row
2599 .iter()
2600 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
2601 .collect();
2602 self.styled(line, style);
2603 }
2604
2605 self
2606 }
2607
2608 pub fn canvas(
2625 &mut self,
2626 width: u32,
2627 height: u32,
2628 draw: impl FnOnce(&mut CanvasContext),
2629 ) -> &mut Self {
2630 if width == 0 || height == 0 {
2631 return self;
2632 }
2633
2634 let mut canvas = CanvasContext::new(width as usize, height as usize);
2635 draw(&mut canvas);
2636
2637 for segments in canvas.render() {
2638 self.interaction_count += 1;
2639 self.commands.push(Command::BeginContainer {
2640 direction: Direction::Row,
2641 gap: 0,
2642 align: Align::Start,
2643 border: None,
2644 border_style: Style::new(),
2645 padding: Padding::default(),
2646 margin: Margin::default(),
2647 constraints: Constraints::default(),
2648 title: None,
2649 grow: 0,
2650 });
2651 for (text, color) in segments {
2652 let c = if color == Color::Reset {
2653 self.theme.primary
2654 } else {
2655 color
2656 };
2657 self.styled(text, Style::new().fg(c));
2658 }
2659 self.commands.push(Command::EndContainer);
2660 self.last_text_idx = None;
2661 }
2662
2663 self
2664 }
2665
2666 pub fn chart(
2668 &mut self,
2669 configure: impl FnOnce(&mut ChartBuilder),
2670 width: u32,
2671 height: u32,
2672 ) -> &mut Self {
2673 if width == 0 || height == 0 {
2674 return self;
2675 }
2676
2677 let axis_style = Style::new().fg(self.theme.text_dim);
2678 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
2679 configure(&mut builder);
2680
2681 let config = builder.build();
2682 let rows = render_chart(&config);
2683
2684 for row in rows {
2685 self.interaction_count += 1;
2686 self.commands.push(Command::BeginContainer {
2687 direction: Direction::Row,
2688 gap: 0,
2689 align: Align::Start,
2690 border: None,
2691 border_style: Style::new().fg(self.theme.border),
2692 padding: Padding::default(),
2693 margin: Margin::default(),
2694 constraints: Constraints::default(),
2695 title: None,
2696 grow: 0,
2697 });
2698 for (text, style) in row.segments {
2699 self.styled(text, style);
2700 }
2701 self.commands.push(Command::EndContainer);
2702 self.last_text_idx = None;
2703 }
2704
2705 self
2706 }
2707
2708 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2710 self.histogram_with(data, |_| {}, width, height)
2711 }
2712
2713 pub fn histogram_with(
2715 &mut self,
2716 data: &[f64],
2717 configure: impl FnOnce(&mut HistogramBuilder),
2718 width: u32,
2719 height: u32,
2720 ) -> &mut Self {
2721 if width == 0 || height == 0 {
2722 return self;
2723 }
2724
2725 let mut options = HistogramBuilder::default();
2726 configure(&mut options);
2727 let axis_style = Style::new().fg(self.theme.text_dim);
2728 let config = build_histogram_config(data, &options, width, height, axis_style);
2729 let rows = render_chart(&config);
2730
2731 for row in rows {
2732 self.interaction_count += 1;
2733 self.commands.push(Command::BeginContainer {
2734 direction: Direction::Row,
2735 gap: 0,
2736 align: Align::Start,
2737 border: None,
2738 border_style: Style::new().fg(self.theme.border),
2739 padding: Padding::default(),
2740 margin: Margin::default(),
2741 constraints: Constraints::default(),
2742 title: None,
2743 grow: 0,
2744 });
2745 for (text, style) in row.segments {
2746 self.styled(text, style);
2747 }
2748 self.commands.push(Command::EndContainer);
2749 self.last_text_idx = None;
2750 }
2751
2752 self
2753 }
2754
2755 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
2772 let interaction_id = self.interaction_count;
2773 self.interaction_count += 1;
2774 let border = self.theme.border;
2775
2776 self.commands.push(Command::BeginContainer {
2777 direction: Direction::Column,
2778 gap: 0,
2779 align: Align::Start,
2780 border: None,
2781 border_style: Style::new().fg(border),
2782 padding: Padding::default(),
2783 margin: Margin::default(),
2784 constraints: Constraints::default(),
2785 title: None,
2786 grow: 0,
2787 });
2788
2789 let children_start = self.commands.len();
2790 f(self);
2791 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
2792
2793 let mut elements: Vec<Vec<Command>> = Vec::new();
2794 let mut iter = child_commands.into_iter().peekable();
2795 while let Some(cmd) = iter.next() {
2796 match cmd {
2797 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2798 let mut depth = 1_u32;
2799 let mut element = vec![cmd];
2800 for next in iter.by_ref() {
2801 match next {
2802 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2803 depth += 1;
2804 }
2805 Command::EndContainer => {
2806 depth = depth.saturating_sub(1);
2807 }
2808 _ => {}
2809 }
2810 let at_end = matches!(next, Command::EndContainer) && depth == 0;
2811 element.push(next);
2812 if at_end {
2813 break;
2814 }
2815 }
2816 elements.push(element);
2817 }
2818 Command::EndContainer => {}
2819 _ => elements.push(vec![cmd]),
2820 }
2821 }
2822
2823 let cols = cols.max(1) as usize;
2824 for row in elements.chunks(cols) {
2825 self.interaction_count += 1;
2826 self.commands.push(Command::BeginContainer {
2827 direction: Direction::Row,
2828 gap: 0,
2829 align: Align::Start,
2830 border: None,
2831 border_style: Style::new().fg(border),
2832 padding: Padding::default(),
2833 margin: Margin::default(),
2834 constraints: Constraints::default(),
2835 title: None,
2836 grow: 0,
2837 });
2838
2839 for element in row {
2840 self.interaction_count += 1;
2841 self.commands.push(Command::BeginContainer {
2842 direction: Direction::Column,
2843 gap: 0,
2844 align: Align::Start,
2845 border: None,
2846 border_style: Style::new().fg(border),
2847 padding: Padding::default(),
2848 margin: Margin::default(),
2849 constraints: Constraints::default(),
2850 title: None,
2851 grow: 1,
2852 });
2853 self.commands.extend(element.iter().cloned());
2854 self.commands.push(Command::EndContainer);
2855 }
2856
2857 self.commands.push(Command::EndContainer);
2858 }
2859
2860 self.commands.push(Command::EndContainer);
2861 self.last_text_idx = None;
2862
2863 self.response_for(interaction_id)
2864 }
2865
2866 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
2871 if state.items.is_empty() {
2872 state.selected = 0;
2873 return self;
2874 }
2875
2876 state.selected = state.selected.min(state.items.len().saturating_sub(1));
2877
2878 let focused = self.register_focusable();
2879
2880 if focused {
2881 let mut consumed_indices = Vec::new();
2882 for (i, event) in self.events.iter().enumerate() {
2883 if let Event::Key(key) = event {
2884 match key.code {
2885 KeyCode::Up | KeyCode::Char('k') => {
2886 state.selected = state.selected.saturating_sub(1);
2887 consumed_indices.push(i);
2888 }
2889 KeyCode::Down | KeyCode::Char('j') => {
2890 state.selected =
2891 (state.selected + 1).min(state.items.len().saturating_sub(1));
2892 consumed_indices.push(i);
2893 }
2894 _ => {}
2895 }
2896 }
2897 }
2898
2899 for index in consumed_indices {
2900 self.consumed[index] = true;
2901 }
2902 }
2903
2904 for (idx, item) in state.items.iter().enumerate() {
2905 if idx == state.selected {
2906 if focused {
2907 self.styled(
2908 format!("▸ {item}"),
2909 Style::new().bold().fg(self.theme.primary),
2910 );
2911 } else {
2912 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
2913 }
2914 } else {
2915 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
2916 }
2917 }
2918
2919 self
2920 }
2921
2922 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
2927 if state.is_dirty() {
2928 state.recompute_widths();
2929 }
2930
2931 let focused = self.register_focusable();
2932
2933 if focused && !state.rows.is_empty() {
2934 let mut consumed_indices = Vec::new();
2935 for (i, event) in self.events.iter().enumerate() {
2936 if let Event::Key(key) = event {
2937 match key.code {
2938 KeyCode::Up | KeyCode::Char('k') => {
2939 state.selected = state.selected.saturating_sub(1);
2940 consumed_indices.push(i);
2941 }
2942 KeyCode::Down | KeyCode::Char('j') => {
2943 state.selected =
2944 (state.selected + 1).min(state.rows.len().saturating_sub(1));
2945 consumed_indices.push(i);
2946 }
2947 _ => {}
2948 }
2949 }
2950 }
2951 for index in consumed_indices {
2952 self.consumed[index] = true;
2953 }
2954 }
2955
2956 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2957
2958 let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2959 self.styled(header_line, Style::new().bold().fg(self.theme.text));
2960
2961 let separator = state
2962 .column_widths()
2963 .iter()
2964 .map(|w| "─".repeat(*w as usize))
2965 .collect::<Vec<_>>()
2966 .join("─┼─");
2967 self.text(separator);
2968
2969 for (idx, row) in state.rows.iter().enumerate() {
2970 let line = format_table_row(row, state.column_widths(), " │ ");
2971 if idx == state.selected {
2972 let mut style = Style::new()
2973 .bg(self.theme.selected_bg)
2974 .fg(self.theme.selected_fg);
2975 if focused {
2976 style = style.bold();
2977 }
2978 self.styled(line, style);
2979 } else {
2980 self.styled(line, Style::new().fg(self.theme.text));
2981 }
2982 }
2983
2984 self
2985 }
2986
2987 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
2992 if state.labels.is_empty() {
2993 state.selected = 0;
2994 return self;
2995 }
2996
2997 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
2998 let focused = self.register_focusable();
2999
3000 if focused {
3001 let mut consumed_indices = Vec::new();
3002 for (i, event) in self.events.iter().enumerate() {
3003 if let Event::Key(key) = event {
3004 match key.code {
3005 KeyCode::Left => {
3006 state.selected = if state.selected == 0 {
3007 state.labels.len().saturating_sub(1)
3008 } else {
3009 state.selected - 1
3010 };
3011 consumed_indices.push(i);
3012 }
3013 KeyCode::Right => {
3014 state.selected = (state.selected + 1) % state.labels.len();
3015 consumed_indices.push(i);
3016 }
3017 _ => {}
3018 }
3019 }
3020 }
3021
3022 for index in consumed_indices {
3023 self.consumed[index] = true;
3024 }
3025 }
3026
3027 self.interaction_count += 1;
3028 self.commands.push(Command::BeginContainer {
3029 direction: Direction::Row,
3030 gap: 1,
3031 align: Align::Start,
3032 border: None,
3033 border_style: Style::new().fg(self.theme.border),
3034 padding: Padding::default(),
3035 margin: Margin::default(),
3036 constraints: Constraints::default(),
3037 title: None,
3038 grow: 0,
3039 });
3040 for (idx, label) in state.labels.iter().enumerate() {
3041 let style = if idx == state.selected {
3042 let s = Style::new().fg(self.theme.primary).bold();
3043 if focused {
3044 s.underline()
3045 } else {
3046 s
3047 }
3048 } else {
3049 Style::new().fg(self.theme.text_dim)
3050 };
3051 self.styled(format!("[ {label} ]"), style);
3052 }
3053 self.commands.push(Command::EndContainer);
3054 self.last_text_idx = None;
3055
3056 self
3057 }
3058
3059 pub fn button(&mut self, label: impl Into<String>) -> bool {
3064 let focused = self.register_focusable();
3065 let interaction_id = self.interaction_count;
3066 self.interaction_count += 1;
3067 let response = self.response_for(interaction_id);
3068
3069 let mut activated = response.clicked;
3070 if focused {
3071 let mut consumed_indices = Vec::new();
3072 for (i, event) in self.events.iter().enumerate() {
3073 if let Event::Key(key) = event {
3074 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3075 activated = true;
3076 consumed_indices.push(i);
3077 }
3078 }
3079 }
3080
3081 for index in consumed_indices {
3082 self.consumed[index] = true;
3083 }
3084 }
3085
3086 let style = if focused {
3087 Style::new().fg(self.theme.primary).bold()
3088 } else if response.hovered {
3089 Style::new().fg(self.theme.accent)
3090 } else {
3091 Style::new().fg(self.theme.text)
3092 };
3093
3094 self.commands.push(Command::BeginContainer {
3095 direction: Direction::Row,
3096 gap: 0,
3097 align: Align::Start,
3098 border: None,
3099 border_style: Style::new().fg(self.theme.border),
3100 padding: Padding::default(),
3101 margin: Margin::default(),
3102 constraints: Constraints::default(),
3103 title: None,
3104 grow: 0,
3105 });
3106 self.styled(format!("[ {} ]", label.into()), style);
3107 self.commands.push(Command::EndContainer);
3108 self.last_text_idx = None;
3109
3110 activated
3111 }
3112
3113 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
3118 let focused = self.register_focusable();
3119 let interaction_id = self.interaction_count;
3120 self.interaction_count += 1;
3121 let response = self.response_for(interaction_id);
3122 let mut should_toggle = response.clicked;
3123
3124 if focused {
3125 let mut consumed_indices = Vec::new();
3126 for (i, event) in self.events.iter().enumerate() {
3127 if let Event::Key(key) = event {
3128 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3129 should_toggle = true;
3130 consumed_indices.push(i);
3131 }
3132 }
3133 }
3134
3135 for index in consumed_indices {
3136 self.consumed[index] = true;
3137 }
3138 }
3139
3140 if should_toggle {
3141 *checked = !*checked;
3142 }
3143
3144 self.commands.push(Command::BeginContainer {
3145 direction: Direction::Row,
3146 gap: 1,
3147 align: Align::Start,
3148 border: None,
3149 border_style: Style::new().fg(self.theme.border),
3150 padding: Padding::default(),
3151 margin: Margin::default(),
3152 constraints: Constraints::default(),
3153 title: None,
3154 grow: 0,
3155 });
3156 let marker_style = if *checked {
3157 Style::new().fg(self.theme.success)
3158 } else {
3159 Style::new().fg(self.theme.text_dim)
3160 };
3161 let marker = if *checked { "[x]" } else { "[ ]" };
3162 let label_text = label.into();
3163 if focused {
3164 self.styled(format!("▸ {marker}"), marker_style.bold());
3165 self.styled(label_text, Style::new().fg(self.theme.text).bold());
3166 } else {
3167 self.styled(marker, marker_style);
3168 self.styled(label_text, Style::new().fg(self.theme.text));
3169 }
3170 self.commands.push(Command::EndContainer);
3171 self.last_text_idx = None;
3172
3173 self
3174 }
3175
3176 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
3182 let focused = self.register_focusable();
3183 let interaction_id = self.interaction_count;
3184 self.interaction_count += 1;
3185 let response = self.response_for(interaction_id);
3186 let mut should_toggle = response.clicked;
3187
3188 if focused {
3189 let mut consumed_indices = Vec::new();
3190 for (i, event) in self.events.iter().enumerate() {
3191 if let Event::Key(key) = event {
3192 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3193 should_toggle = true;
3194 consumed_indices.push(i);
3195 }
3196 }
3197 }
3198
3199 for index in consumed_indices {
3200 self.consumed[index] = true;
3201 }
3202 }
3203
3204 if should_toggle {
3205 *on = !*on;
3206 }
3207
3208 self.commands.push(Command::BeginContainer {
3209 direction: Direction::Row,
3210 gap: 2,
3211 align: Align::Start,
3212 border: None,
3213 border_style: Style::new().fg(self.theme.border),
3214 padding: Padding::default(),
3215 margin: Margin::default(),
3216 constraints: Constraints::default(),
3217 title: None,
3218 grow: 0,
3219 });
3220 let label_text = label.into();
3221 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
3222 let switch_style = if *on {
3223 Style::new().fg(self.theme.success)
3224 } else {
3225 Style::new().fg(self.theme.text_dim)
3226 };
3227 if focused {
3228 self.styled(
3229 format!("▸ {label_text}"),
3230 Style::new().fg(self.theme.text).bold(),
3231 );
3232 self.styled(switch, switch_style.bold());
3233 } else {
3234 self.styled(label_text, Style::new().fg(self.theme.text));
3235 self.styled(switch, switch_style);
3236 }
3237 self.commands.push(Command::EndContainer);
3238 self.last_text_idx = None;
3239
3240 self
3241 }
3242
3243 pub fn separator(&mut self) -> &mut Self {
3248 self.commands.push(Command::Text {
3249 content: "─".repeat(200),
3250 style: Style::new().fg(self.theme.border).dim(),
3251 grow: 0,
3252 align: Align::Start,
3253 wrap: false,
3254 margin: Margin::default(),
3255 constraints: Constraints::default(),
3256 });
3257 self.last_text_idx = Some(self.commands.len() - 1);
3258 self
3259 }
3260
3261 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
3267 if bindings.is_empty() {
3268 return self;
3269 }
3270
3271 self.interaction_count += 1;
3272 self.commands.push(Command::BeginContainer {
3273 direction: Direction::Row,
3274 gap: 2,
3275 align: Align::Start,
3276 border: None,
3277 border_style: Style::new().fg(self.theme.border),
3278 padding: Padding::default(),
3279 margin: Margin::default(),
3280 constraints: Constraints::default(),
3281 title: None,
3282 grow: 0,
3283 });
3284 for (idx, (key, action)) in bindings.iter().enumerate() {
3285 if idx > 0 {
3286 self.styled("·", Style::new().fg(self.theme.text_dim));
3287 }
3288 self.styled(*key, Style::new().bold().fg(self.theme.primary));
3289 self.styled(*action, Style::new().fg(self.theme.text_dim));
3290 }
3291 self.commands.push(Command::EndContainer);
3292 self.last_text_idx = None;
3293
3294 self
3295 }
3296
3297 pub fn key(&self, c: char) -> bool {
3303 self.events.iter().enumerate().any(|(i, e)| {
3304 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
3305 })
3306 }
3307
3308 pub fn key_code(&self, code: KeyCode) -> bool {
3312 self.events
3313 .iter()
3314 .enumerate()
3315 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
3316 }
3317
3318 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3322 self.events.iter().enumerate().any(|(i, e)| {
3323 !self.consumed[i]
3324 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3325 })
3326 }
3327
3328 pub fn mouse_down(&self) -> Option<(u32, u32)> {
3332 self.events.iter().enumerate().find_map(|(i, event)| {
3333 if self.consumed[i] {
3334 return None;
3335 }
3336 if let Event::Mouse(mouse) = event {
3337 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3338 return Some((mouse.x, mouse.y));
3339 }
3340 }
3341 None
3342 })
3343 }
3344
3345 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3350 self.mouse_pos
3351 }
3352
3353 pub fn paste(&self) -> Option<&str> {
3355 self.events.iter().enumerate().find_map(|(i, event)| {
3356 if self.consumed[i] {
3357 return None;
3358 }
3359 if let Event::Paste(ref text) = event {
3360 return Some(text.as_str());
3361 }
3362 None
3363 })
3364 }
3365
3366 pub fn scroll_up(&self) -> bool {
3368 self.events.iter().enumerate().any(|(i, event)| {
3369 !self.consumed[i]
3370 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3371 })
3372 }
3373
3374 pub fn scroll_down(&self) -> bool {
3376 self.events.iter().enumerate().any(|(i, event)| {
3377 !self.consumed[i]
3378 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3379 })
3380 }
3381
3382 pub fn quit(&mut self) {
3384 self.should_quit = true;
3385 }
3386
3387 pub fn theme(&self) -> &Theme {
3389 &self.theme
3390 }
3391
3392 pub fn set_theme(&mut self, theme: Theme) {
3396 self.theme = theme;
3397 }
3398
3399 pub fn width(&self) -> u32 {
3403 self.area_width
3404 }
3405
3406 pub fn height(&self) -> u32 {
3408 self.area_height
3409 }
3410
3411 pub fn tick(&self) -> u64 {
3416 self.tick
3417 }
3418
3419 pub fn debug_enabled(&self) -> bool {
3423 self.debug
3424 }
3425}
3426
3427#[inline]
3428fn byte_index_for_char(value: &str, char_index: usize) -> usize {
3429 if char_index == 0 {
3430 return 0;
3431 }
3432 value
3433 .char_indices()
3434 .nth(char_index)
3435 .map_or(value.len(), |(idx, _)| idx)
3436}
3437
3438fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
3439 let mut parts: Vec<String> = Vec::new();
3440 for (i, width) in widths.iter().enumerate() {
3441 let cell = cells.get(i).map(String::as_str).unwrap_or("");
3442 let cell_width = UnicodeWidthStr::width(cell) as u32;
3443 let padding = (*width).saturating_sub(cell_width) as usize;
3444 parts.push(format!("{cell}{}", " ".repeat(padding)));
3445 }
3446 parts.join(separator)
3447}
3448
3449fn format_compact_number(value: f64) -> String {
3450 if value.fract().abs() < f64::EPSILON {
3451 return format!("{value:.0}");
3452 }
3453
3454 let mut s = format!("{value:.2}");
3455 while s.contains('.') && s.ends_with('0') {
3456 s.pop();
3457 }
3458 if s.ends_with('.') {
3459 s.pop();
3460 }
3461 s
3462}
3463
3464fn center_text(text: &str, width: usize) -> String {
3465 let text_width = UnicodeWidthStr::width(text);
3466 if text_width >= width {
3467 return text.to_string();
3468 }
3469
3470 let total = width - text_width;
3471 let left = total / 2;
3472 let right = total - left;
3473 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
3474}