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
136pub struct ContainerBuilder<'a> {
157 ctx: &'a mut Context,
158 gap: u32,
159 align: Align,
160 border: Option<Border>,
161 border_style: Style,
162 padding: Padding,
163 margin: Margin,
164 constraints: Constraints,
165 title: Option<(String, Style)>,
166 grow: u16,
167 scroll_offset: Option<u32>,
168}
169
170impl<'a> ContainerBuilder<'a> {
171 pub fn border(mut self, border: Border) -> Self {
175 self.border = Some(border);
176 self
177 }
178
179 pub fn rounded(self) -> Self {
181 self.border(Border::Rounded)
182 }
183
184 pub fn border_style(mut self, style: Style) -> Self {
186 self.border_style = style;
187 self
188 }
189
190 pub fn p(self, value: u32) -> Self {
194 self.pad(value)
195 }
196
197 pub fn pad(mut self, value: u32) -> Self {
199 self.padding = Padding::all(value);
200 self
201 }
202
203 pub fn px(mut self, value: u32) -> Self {
205 self.padding.left = value;
206 self.padding.right = value;
207 self
208 }
209
210 pub fn py(mut self, value: u32) -> Self {
212 self.padding.top = value;
213 self.padding.bottom = value;
214 self
215 }
216
217 pub fn pt(mut self, value: u32) -> Self {
219 self.padding.top = value;
220 self
221 }
222
223 pub fn pr(mut self, value: u32) -> Self {
225 self.padding.right = value;
226 self
227 }
228
229 pub fn pb(mut self, value: u32) -> Self {
231 self.padding.bottom = value;
232 self
233 }
234
235 pub fn pl(mut self, value: u32) -> Self {
237 self.padding.left = value;
238 self
239 }
240
241 pub fn padding(mut self, padding: Padding) -> Self {
243 self.padding = padding;
244 self
245 }
246
247 pub fn m(mut self, value: u32) -> Self {
251 self.margin = Margin::all(value);
252 self
253 }
254
255 pub fn mx(mut self, value: u32) -> Self {
257 self.margin.left = value;
258 self.margin.right = value;
259 self
260 }
261
262 pub fn my(mut self, value: u32) -> Self {
264 self.margin.top = value;
265 self.margin.bottom = value;
266 self
267 }
268
269 pub fn mt(mut self, value: u32) -> Self {
271 self.margin.top = value;
272 self
273 }
274
275 pub fn mr(mut self, value: u32) -> Self {
277 self.margin.right = value;
278 self
279 }
280
281 pub fn mb(mut self, value: u32) -> Self {
283 self.margin.bottom = value;
284 self
285 }
286
287 pub fn ml(mut self, value: u32) -> Self {
289 self.margin.left = value;
290 self
291 }
292
293 pub fn margin(mut self, margin: Margin) -> Self {
295 self.margin = margin;
296 self
297 }
298
299 pub fn w(mut self, value: u32) -> Self {
303 self.constraints.min_width = Some(value);
304 self.constraints.max_width = Some(value);
305 self
306 }
307
308 pub fn h(mut self, value: u32) -> Self {
310 self.constraints.min_height = Some(value);
311 self.constraints.max_height = Some(value);
312 self
313 }
314
315 pub fn min_w(mut self, value: u32) -> Self {
317 self.constraints.min_width = Some(value);
318 self
319 }
320
321 pub fn max_w(mut self, value: u32) -> Self {
323 self.constraints.max_width = Some(value);
324 self
325 }
326
327 pub fn min_h(mut self, value: u32) -> Self {
329 self.constraints.min_height = Some(value);
330 self
331 }
332
333 pub fn max_h(mut self, value: u32) -> Self {
335 self.constraints.max_height = Some(value);
336 self
337 }
338
339 pub fn min_width(mut self, value: u32) -> Self {
341 self.constraints.min_width = Some(value);
342 self
343 }
344
345 pub fn max_width(mut self, value: u32) -> Self {
347 self.constraints.max_width = Some(value);
348 self
349 }
350
351 pub fn min_height(mut self, value: u32) -> Self {
353 self.constraints.min_height = Some(value);
354 self
355 }
356
357 pub fn max_height(mut self, value: u32) -> Self {
359 self.constraints.max_height = Some(value);
360 self
361 }
362
363 pub fn constraints(mut self, constraints: Constraints) -> Self {
365 self.constraints = constraints;
366 self
367 }
368
369 pub fn gap(mut self, gap: u32) -> Self {
373 self.gap = gap;
374 self
375 }
376
377 pub fn grow(mut self, grow: u16) -> Self {
379 self.grow = grow;
380 self
381 }
382
383 pub fn align(mut self, align: Align) -> Self {
387 self.align = align;
388 self
389 }
390
391 pub fn center(self) -> Self {
393 self.align(Align::Center)
394 }
395
396 pub fn title(self, title: impl Into<String>) -> Self {
400 self.title_styled(title, Style::new())
401 }
402
403 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
405 self.title = Some((title.into(), style));
406 self
407 }
408
409 pub fn scroll_offset(mut self, offset: u32) -> Self {
413 self.scroll_offset = Some(offset);
414 self
415 }
416
417 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
422 self.finish(Direction::Column, f)
423 }
424
425 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
430 self.finish(Direction::Row, f)
431 }
432
433 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
434 let interaction_id = self.ctx.interaction_count;
435 self.ctx.interaction_count += 1;
436
437 if let Some(scroll_offset) = self.scroll_offset {
438 self.ctx.commands.push(Command::BeginScrollable {
439 grow: self.grow,
440 border: self.border,
441 border_style: self.border_style,
442 padding: self.padding,
443 margin: self.margin,
444 constraints: self.constraints,
445 title: self.title,
446 scroll_offset,
447 });
448 } else {
449 self.ctx.commands.push(Command::BeginContainer {
450 direction,
451 gap: self.gap,
452 align: self.align,
453 border: self.border,
454 border_style: self.border_style,
455 padding: self.padding,
456 margin: self.margin,
457 constraints: self.constraints,
458 title: self.title,
459 grow: self.grow,
460 });
461 }
462 f(self.ctx);
463 self.ctx.commands.push(Command::EndContainer);
464 self.ctx.last_text_idx = None;
465
466 self.ctx.response_for(interaction_id)
467 }
468}
469
470impl Context {
471 #[allow(clippy::too_many_arguments)]
472 pub(crate) fn new(
473 events: Vec<Event>,
474 width: u32,
475 height: u32,
476 tick: u64,
477 focus_index: usize,
478 prev_focus_count: usize,
479 prev_scroll_infos: Vec<(u32, u32)>,
480 prev_hit_map: Vec<Rect>,
481 debug: bool,
482 theme: Theme,
483 last_mouse_pos: Option<(u32, u32)>,
484 ) -> Self {
485 let consumed = vec![false; events.len()];
486
487 let mut mouse_pos = last_mouse_pos;
488 let mut click_pos = None;
489 for event in &events {
490 if let Event::Mouse(mouse) = event {
491 mouse_pos = Some((mouse.x, mouse.y));
492 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
493 click_pos = Some((mouse.x, mouse.y));
494 }
495 }
496 }
497
498 Self {
499 commands: Vec::new(),
500 events,
501 consumed,
502 should_quit: false,
503 area_width: width,
504 area_height: height,
505 tick,
506 focus_index,
507 focus_count: 0,
508 prev_focus_count,
509 scroll_count: 0,
510 prev_scroll_infos,
511 interaction_count: 0,
512 prev_hit_map,
513 mouse_pos,
514 click_pos,
515 last_mouse_pos,
516 last_text_idx: None,
517 debug,
518 theme,
519 }
520 }
521
522 pub(crate) fn process_focus_keys(&mut self) {
523 for (i, event) in self.events.iter().enumerate() {
524 if let Event::Key(key) = event {
525 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
526 if self.prev_focus_count > 0 {
527 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
528 }
529 self.consumed[i] = true;
530 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
531 || key.code == KeyCode::BackTab
532 {
533 if self.prev_focus_count > 0 {
534 self.focus_index = if self.focus_index == 0 {
535 self.prev_focus_count - 1
536 } else {
537 self.focus_index - 1
538 };
539 }
540 self.consumed[i] = true;
541 }
542 }
543 }
544 }
545
546 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
550 w.ui(self)
551 }
552
553 pub fn interaction(&mut self) -> Response {
559 let id = self.interaction_count;
560 self.interaction_count += 1;
561 self.response_for(id)
562 }
563
564 pub fn register_focusable(&mut self) -> bool {
569 let id = self.focus_count;
570 self.focus_count += 1;
571 if self.prev_focus_count == 0 {
572 return true;
573 }
574 self.focus_index % self.prev_focus_count == id
575 }
576
577 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
590 let content = s.into();
591 self.commands.push(Command::Text {
592 content,
593 style: Style::new(),
594 grow: 0,
595 align: Align::Start,
596 wrap: false,
597 margin: Margin::default(),
598 constraints: Constraints::default(),
599 });
600 self.last_text_idx = Some(self.commands.len() - 1);
601 self
602 }
603
604 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
609 let content = s.into();
610 self.commands.push(Command::Text {
611 content,
612 style: Style::new(),
613 grow: 0,
614 align: Align::Start,
615 wrap: true,
616 margin: Margin::default(),
617 constraints: Constraints::default(),
618 });
619 self.last_text_idx = Some(self.commands.len() - 1);
620 self
621 }
622
623 pub fn bold(&mut self) -> &mut Self {
627 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
628 self
629 }
630
631 pub fn dim(&mut self) -> &mut Self {
636 let text_dim = self.theme.text_dim;
637 self.modify_last_style(|s| {
638 s.modifiers |= Modifiers::DIM;
639 if s.fg.is_none() {
640 s.fg = Some(text_dim);
641 }
642 });
643 self
644 }
645
646 pub fn italic(&mut self) -> &mut Self {
648 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
649 self
650 }
651
652 pub fn underline(&mut self) -> &mut Self {
654 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
655 self
656 }
657
658 pub fn reversed(&mut self) -> &mut Self {
660 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
661 self
662 }
663
664 pub fn strikethrough(&mut self) -> &mut Self {
666 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
667 self
668 }
669
670 pub fn fg(&mut self, color: Color) -> &mut Self {
672 self.modify_last_style(|s| s.fg = Some(color));
673 self
674 }
675
676 pub fn bg(&mut self, color: Color) -> &mut Self {
678 self.modify_last_style(|s| s.bg = Some(color));
679 self
680 }
681
682 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
687 self.commands.push(Command::Text {
688 content: s.into(),
689 style,
690 grow: 0,
691 align: Align::Start,
692 wrap: false,
693 margin: Margin::default(),
694 constraints: Constraints::default(),
695 });
696 self.last_text_idx = Some(self.commands.len() - 1);
697 self
698 }
699
700 pub fn wrap(&mut self) -> &mut Self {
702 if let Some(idx) = self.last_text_idx {
703 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
704 *wrap = true;
705 }
706 }
707 self
708 }
709
710 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
711 if let Some(idx) = self.last_text_idx {
712 if let Command::Text { style, .. } = &mut self.commands[idx] {
713 f(style);
714 }
715 }
716 }
717
718 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
736 self.push_container(Direction::Column, 0, f)
737 }
738
739 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
743 self.push_container(Direction::Column, gap, f)
744 }
745
746 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
763 self.push_container(Direction::Row, 0, f)
764 }
765
766 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
770 self.push_container(Direction::Row, gap, f)
771 }
772
773 pub fn container(&mut self) -> ContainerBuilder<'_> {
794 let border = self.theme.border;
795 ContainerBuilder {
796 ctx: self,
797 gap: 0,
798 align: Align::Start,
799 border: None,
800 border_style: Style::new().fg(border),
801 padding: Padding::default(),
802 margin: Margin::default(),
803 constraints: Constraints::default(),
804 title: None,
805 grow: 0,
806 scroll_offset: None,
807 }
808 }
809
810 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
829 let index = self.scroll_count;
830 self.scroll_count += 1;
831 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
832 state.set_bounds(ch, vh);
833 let max = ch.saturating_sub(vh) as usize;
834 state.offset = state.offset.min(max);
835 }
836
837 let next_id = self.interaction_count;
838 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
839 self.auto_scroll(&rect, state);
840 }
841
842 self.container().scroll_offset(state.offset as u32)
843 }
844
845 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
846 let last_y = self.last_mouse_pos.map(|(_, y)| y);
847 let mut to_consume: Vec<usize> = Vec::new();
848
849 for (i, event) in self.events.iter().enumerate() {
850 if self.consumed[i] {
851 continue;
852 }
853 if let Event::Mouse(mouse) = event {
854 let in_bounds = mouse.x >= rect.x
855 && mouse.x < rect.right()
856 && mouse.y >= rect.y
857 && mouse.y < rect.bottom();
858 if !in_bounds {
859 continue;
860 }
861 match mouse.kind {
862 MouseKind::ScrollUp => {
863 state.scroll_up(1);
864 to_consume.push(i);
865 }
866 MouseKind::ScrollDown => {
867 state.scroll_down(1);
868 to_consume.push(i);
869 }
870 MouseKind::Drag(MouseButton::Left) => {
871 if let Some(prev_y) = last_y {
872 let delta = mouse.y as i32 - prev_y as i32;
873 if delta < 0 {
874 state.scroll_down((-delta) as usize);
875 } else if delta > 0 {
876 state.scroll_up(delta as usize);
877 }
878 }
879 to_consume.push(i);
880 }
881 _ => {}
882 }
883 }
884 }
885
886 for i in to_consume {
887 self.consumed[i] = true;
888 }
889 }
890
891 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
895 self.container().border(border)
896 }
897
898 fn push_container(
899 &mut self,
900 direction: Direction,
901 gap: u32,
902 f: impl FnOnce(&mut Context),
903 ) -> Response {
904 let interaction_id = self.interaction_count;
905 self.interaction_count += 1;
906 let border = self.theme.border;
907
908 self.commands.push(Command::BeginContainer {
909 direction,
910 gap,
911 align: Align::Start,
912 border: None,
913 border_style: Style::new().fg(border),
914 padding: Padding::default(),
915 margin: Margin::default(),
916 constraints: Constraints::default(),
917 title: None,
918 grow: 0,
919 });
920 f(self);
921 self.commands.push(Command::EndContainer);
922 self.last_text_idx = None;
923
924 self.response_for(interaction_id)
925 }
926
927 fn response_for(&self, interaction_id: usize) -> Response {
928 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
929 let clicked = self
930 .click_pos
931 .map(|(mx, my)| {
932 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
933 })
934 .unwrap_or(false);
935 let hovered = self
936 .mouse_pos
937 .map(|(mx, my)| {
938 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
939 })
940 .unwrap_or(false);
941 Response { clicked, hovered }
942 } else {
943 Response::default()
944 }
945 }
946
947 pub fn grow(&mut self, value: u16) -> &mut Self {
952 if let Some(idx) = self.last_text_idx {
953 if let Command::Text { grow, .. } = &mut self.commands[idx] {
954 *grow = value;
955 }
956 }
957 self
958 }
959
960 pub fn align(&mut self, align: Align) -> &mut Self {
962 if let Some(idx) = self.last_text_idx {
963 if let Command::Text {
964 align: text_align, ..
965 } = &mut self.commands[idx]
966 {
967 *text_align = align;
968 }
969 }
970 self
971 }
972
973 pub fn spacer(&mut self) -> &mut Self {
977 self.commands.push(Command::Spacer { grow: 1 });
978 self.last_text_idx = None;
979 self
980 }
981
982 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
998 let focused = self.register_focusable();
999 state.cursor = state.cursor.min(state.value.chars().count());
1000
1001 if focused {
1002 let mut consumed_indices = Vec::new();
1003 for (i, event) in self.events.iter().enumerate() {
1004 if let Event::Key(key) = event {
1005 match key.code {
1006 KeyCode::Char(ch) => {
1007 if let Some(max) = state.max_length {
1008 if state.value.chars().count() >= max {
1009 continue;
1010 }
1011 }
1012 let index = byte_index_for_char(&state.value, state.cursor);
1013 state.value.insert(index, ch);
1014 state.cursor += 1;
1015 consumed_indices.push(i);
1016 }
1017 KeyCode::Backspace => {
1018 if state.cursor > 0 {
1019 let start = byte_index_for_char(&state.value, state.cursor - 1);
1020 let end = byte_index_for_char(&state.value, state.cursor);
1021 state.value.replace_range(start..end, "");
1022 state.cursor -= 1;
1023 }
1024 consumed_indices.push(i);
1025 }
1026 KeyCode::Left => {
1027 state.cursor = state.cursor.saturating_sub(1);
1028 consumed_indices.push(i);
1029 }
1030 KeyCode::Right => {
1031 state.cursor = (state.cursor + 1).min(state.value.chars().count());
1032 consumed_indices.push(i);
1033 }
1034 KeyCode::Home => {
1035 state.cursor = 0;
1036 consumed_indices.push(i);
1037 }
1038 KeyCode::End => {
1039 state.cursor = state.value.chars().count();
1040 consumed_indices.push(i);
1041 }
1042 _ => {}
1043 }
1044 }
1045 }
1046
1047 for index in consumed_indices {
1048 self.consumed[index] = true;
1049 }
1050 }
1051
1052 if state.value.is_empty() {
1053 self.styled(
1054 state.placeholder.clone(),
1055 Style::new().dim().fg(self.theme.text_dim),
1056 )
1057 } else {
1058 let mut rendered = String::new();
1059 for (idx, ch) in state.value.chars().enumerate() {
1060 if focused && idx == state.cursor {
1061 rendered.push('▎');
1062 }
1063 rendered.push(ch);
1064 }
1065 if focused && state.cursor >= state.value.chars().count() {
1066 rendered.push('▎');
1067 }
1068 self.styled(rendered, Style::new().fg(self.theme.text))
1069 }
1070 }
1071
1072 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1078 self.styled(
1079 state.frame(self.tick).to_string(),
1080 Style::new().fg(self.theme.primary),
1081 )
1082 }
1083
1084 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1089 state.cleanup(self.tick);
1090 if state.messages.is_empty() {
1091 return self;
1092 }
1093
1094 self.interaction_count += 1;
1095 self.commands.push(Command::BeginContainer {
1096 direction: Direction::Column,
1097 gap: 0,
1098 align: Align::Start,
1099 border: None,
1100 border_style: Style::new().fg(self.theme.border),
1101 padding: Padding::default(),
1102 margin: Margin::default(),
1103 constraints: Constraints::default(),
1104 title: None,
1105 grow: 0,
1106 });
1107 for message in state.messages.iter().rev() {
1108 let color = match message.level {
1109 ToastLevel::Info => self.theme.primary,
1110 ToastLevel::Success => self.theme.success,
1111 ToastLevel::Warning => self.theme.warning,
1112 ToastLevel::Error => self.theme.error,
1113 };
1114 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
1115 }
1116 self.commands.push(Command::EndContainer);
1117 self.last_text_idx = None;
1118
1119 self
1120 }
1121
1122 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1127 if state.lines.is_empty() {
1128 state.lines.push(String::new());
1129 }
1130 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1131 state.cursor_col = state
1132 .cursor_col
1133 .min(state.lines[state.cursor_row].chars().count());
1134
1135 let focused = self.register_focusable();
1136
1137 if focused {
1138 let mut consumed_indices = Vec::new();
1139 for (i, event) in self.events.iter().enumerate() {
1140 if let Event::Key(key) = event {
1141 match key.code {
1142 KeyCode::Char(ch) => {
1143 if let Some(max) = state.max_length {
1144 let total: usize =
1145 state.lines.iter().map(|line| line.chars().count()).sum();
1146 if total >= max {
1147 continue;
1148 }
1149 }
1150 let index = byte_index_for_char(
1151 &state.lines[state.cursor_row],
1152 state.cursor_col,
1153 );
1154 state.lines[state.cursor_row].insert(index, ch);
1155 state.cursor_col += 1;
1156 consumed_indices.push(i);
1157 }
1158 KeyCode::Enter => {
1159 let split_index = byte_index_for_char(
1160 &state.lines[state.cursor_row],
1161 state.cursor_col,
1162 );
1163 let remainder = state.lines[state.cursor_row].split_off(split_index);
1164 state.cursor_row += 1;
1165 state.lines.insert(state.cursor_row, remainder);
1166 state.cursor_col = 0;
1167 consumed_indices.push(i);
1168 }
1169 KeyCode::Backspace => {
1170 if state.cursor_col > 0 {
1171 let start = byte_index_for_char(
1172 &state.lines[state.cursor_row],
1173 state.cursor_col - 1,
1174 );
1175 let end = byte_index_for_char(
1176 &state.lines[state.cursor_row],
1177 state.cursor_col,
1178 );
1179 state.lines[state.cursor_row].replace_range(start..end, "");
1180 state.cursor_col -= 1;
1181 } else if state.cursor_row > 0 {
1182 let current = state.lines.remove(state.cursor_row);
1183 state.cursor_row -= 1;
1184 state.cursor_col = state.lines[state.cursor_row].chars().count();
1185 state.lines[state.cursor_row].push_str(¤t);
1186 }
1187 consumed_indices.push(i);
1188 }
1189 KeyCode::Left => {
1190 if state.cursor_col > 0 {
1191 state.cursor_col -= 1;
1192 } else if state.cursor_row > 0 {
1193 state.cursor_row -= 1;
1194 state.cursor_col = state.lines[state.cursor_row].chars().count();
1195 }
1196 consumed_indices.push(i);
1197 }
1198 KeyCode::Right => {
1199 let line_len = state.lines[state.cursor_row].chars().count();
1200 if state.cursor_col < line_len {
1201 state.cursor_col += 1;
1202 } else if state.cursor_row + 1 < state.lines.len() {
1203 state.cursor_row += 1;
1204 state.cursor_col = 0;
1205 }
1206 consumed_indices.push(i);
1207 }
1208 KeyCode::Up => {
1209 if state.cursor_row > 0 {
1210 state.cursor_row -= 1;
1211 state.cursor_col = state
1212 .cursor_col
1213 .min(state.lines[state.cursor_row].chars().count());
1214 }
1215 consumed_indices.push(i);
1216 }
1217 KeyCode::Down => {
1218 if state.cursor_row + 1 < state.lines.len() {
1219 state.cursor_row += 1;
1220 state.cursor_col = state
1221 .cursor_col
1222 .min(state.lines[state.cursor_row].chars().count());
1223 }
1224 consumed_indices.push(i);
1225 }
1226 KeyCode::Home => {
1227 state.cursor_col = 0;
1228 consumed_indices.push(i);
1229 }
1230 KeyCode::End => {
1231 state.cursor_col = state.lines[state.cursor_row].chars().count();
1232 consumed_indices.push(i);
1233 }
1234 _ => {}
1235 }
1236 }
1237 }
1238
1239 for index in consumed_indices {
1240 self.consumed[index] = true;
1241 }
1242 }
1243
1244 self.interaction_count += 1;
1245 self.commands.push(Command::BeginContainer {
1246 direction: Direction::Column,
1247 gap: 0,
1248 align: Align::Start,
1249 border: None,
1250 border_style: Style::new().fg(self.theme.border),
1251 padding: Padding::default(),
1252 margin: Margin::default(),
1253 constraints: Constraints::default(),
1254 title: None,
1255 grow: 0,
1256 });
1257 for row in 0..visible_rows as usize {
1258 let line = state.lines.get(row).cloned().unwrap_or_default();
1259 let mut rendered = line.clone();
1260 let mut style = if line.is_empty() {
1261 Style::new().fg(self.theme.text_dim)
1262 } else {
1263 Style::new().fg(self.theme.text)
1264 };
1265
1266 if focused && row == state.cursor_row {
1267 rendered.clear();
1268 for (idx, ch) in line.chars().enumerate() {
1269 if idx == state.cursor_col {
1270 rendered.push('▎');
1271 }
1272 rendered.push(ch);
1273 }
1274 if state.cursor_col >= line.chars().count() {
1275 rendered.push('▎');
1276 }
1277 style = Style::new().fg(self.theme.text);
1278 }
1279
1280 self.styled(rendered, style);
1281 }
1282 self.commands.push(Command::EndContainer);
1283 self.last_text_idx = None;
1284
1285 self
1286 }
1287
1288 pub fn progress(&mut self, ratio: f64) -> &mut Self {
1293 self.progress_bar(ratio, 20)
1294 }
1295
1296 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1301 let clamped = ratio.clamp(0.0, 1.0);
1302 let filled = (clamped * width as f64).round() as u32;
1303 let empty = width.saturating_sub(filled);
1304 let mut bar = String::new();
1305 for _ in 0..filled {
1306 bar.push('█');
1307 }
1308 for _ in 0..empty {
1309 bar.push('░');
1310 }
1311 self.text(bar)
1312 }
1313
1314 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
1319 if state.items.is_empty() {
1320 state.selected = 0;
1321 return self;
1322 }
1323
1324 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1325
1326 let focused = self.register_focusable();
1327
1328 if focused {
1329 let mut consumed_indices = Vec::new();
1330 for (i, event) in self.events.iter().enumerate() {
1331 if let Event::Key(key) = event {
1332 match key.code {
1333 KeyCode::Up | KeyCode::Char('k') => {
1334 state.selected = state.selected.saturating_sub(1);
1335 consumed_indices.push(i);
1336 }
1337 KeyCode::Down | KeyCode::Char('j') => {
1338 state.selected =
1339 (state.selected + 1).min(state.items.len().saturating_sub(1));
1340 consumed_indices.push(i);
1341 }
1342 _ => {}
1343 }
1344 }
1345 }
1346
1347 for index in consumed_indices {
1348 self.consumed[index] = true;
1349 }
1350 }
1351
1352 for (idx, item) in state.items.iter().enumerate() {
1353 if idx == state.selected {
1354 if focused {
1355 self.styled(
1356 format!("▸ {item}"),
1357 Style::new().bold().fg(self.theme.primary),
1358 );
1359 } else {
1360 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
1361 }
1362 } else {
1363 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
1364 }
1365 }
1366
1367 self
1368 }
1369
1370 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
1375 if state.is_dirty() {
1376 state.recompute_widths();
1377 }
1378
1379 let focused = self.register_focusable();
1380
1381 if focused && !state.rows.is_empty() {
1382 let mut consumed_indices = Vec::new();
1383 for (i, event) in self.events.iter().enumerate() {
1384 if let Event::Key(key) = event {
1385 match key.code {
1386 KeyCode::Up | KeyCode::Char('k') => {
1387 state.selected = state.selected.saturating_sub(1);
1388 consumed_indices.push(i);
1389 }
1390 KeyCode::Down | KeyCode::Char('j') => {
1391 state.selected =
1392 (state.selected + 1).min(state.rows.len().saturating_sub(1));
1393 consumed_indices.push(i);
1394 }
1395 _ => {}
1396 }
1397 }
1398 }
1399 for index in consumed_indices {
1400 self.consumed[index] = true;
1401 }
1402 }
1403
1404 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
1405
1406 let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
1407 self.styled(header_line, Style::new().bold().fg(self.theme.text));
1408
1409 let separator = state
1410 .column_widths()
1411 .iter()
1412 .map(|w| "─".repeat(*w as usize))
1413 .collect::<Vec<_>>()
1414 .join("─┼─");
1415 self.text(separator);
1416
1417 for (idx, row) in state.rows.iter().enumerate() {
1418 let line = format_table_row(row, state.column_widths(), " │ ");
1419 if idx == state.selected {
1420 let mut style = Style::new()
1421 .bg(self.theme.selected_bg)
1422 .fg(self.theme.selected_fg);
1423 if focused {
1424 style = style.bold();
1425 }
1426 self.styled(line, style);
1427 } else {
1428 self.styled(line, Style::new().fg(self.theme.text));
1429 }
1430 }
1431
1432 self
1433 }
1434
1435 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
1440 if state.labels.is_empty() {
1441 state.selected = 0;
1442 return self;
1443 }
1444
1445 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
1446 let focused = self.register_focusable();
1447
1448 if focused {
1449 let mut consumed_indices = Vec::new();
1450 for (i, event) in self.events.iter().enumerate() {
1451 if let Event::Key(key) = event {
1452 match key.code {
1453 KeyCode::Left => {
1454 state.selected = if state.selected == 0 {
1455 state.labels.len().saturating_sub(1)
1456 } else {
1457 state.selected - 1
1458 };
1459 consumed_indices.push(i);
1460 }
1461 KeyCode::Right => {
1462 state.selected = (state.selected + 1) % state.labels.len();
1463 consumed_indices.push(i);
1464 }
1465 _ => {}
1466 }
1467 }
1468 }
1469
1470 for index in consumed_indices {
1471 self.consumed[index] = true;
1472 }
1473 }
1474
1475 self.interaction_count += 1;
1476 self.commands.push(Command::BeginContainer {
1477 direction: Direction::Row,
1478 gap: 1,
1479 align: Align::Start,
1480 border: None,
1481 border_style: Style::new().fg(self.theme.border),
1482 padding: Padding::default(),
1483 margin: Margin::default(),
1484 constraints: Constraints::default(),
1485 title: None,
1486 grow: 0,
1487 });
1488 for (idx, label) in state.labels.iter().enumerate() {
1489 let style = if idx == state.selected {
1490 let s = Style::new().fg(self.theme.primary).bold();
1491 if focused {
1492 s.underline()
1493 } else {
1494 s
1495 }
1496 } else {
1497 Style::new().fg(self.theme.text_dim)
1498 };
1499 self.styled(format!("[ {label} ]"), style);
1500 }
1501 self.commands.push(Command::EndContainer);
1502 self.last_text_idx = None;
1503
1504 self
1505 }
1506
1507 pub fn button(&mut self, label: impl Into<String>) -> bool {
1512 let focused = self.register_focusable();
1513 let interaction_id = self.interaction_count;
1514 self.interaction_count += 1;
1515 let response = self.response_for(interaction_id);
1516
1517 let mut activated = response.clicked;
1518 if focused {
1519 let mut consumed_indices = Vec::new();
1520 for (i, event) in self.events.iter().enumerate() {
1521 if let Event::Key(key) = event {
1522 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1523 activated = true;
1524 consumed_indices.push(i);
1525 }
1526 }
1527 }
1528
1529 for index in consumed_indices {
1530 self.consumed[index] = true;
1531 }
1532 }
1533
1534 let style = if focused {
1535 Style::new().fg(self.theme.primary).bold()
1536 } else if response.hovered {
1537 Style::new().fg(self.theme.accent)
1538 } else {
1539 Style::new().fg(self.theme.text)
1540 };
1541
1542 self.commands.push(Command::BeginContainer {
1543 direction: Direction::Row,
1544 gap: 0,
1545 align: Align::Start,
1546 border: None,
1547 border_style: Style::new().fg(self.theme.border),
1548 padding: Padding::default(),
1549 margin: Margin::default(),
1550 constraints: Constraints::default(),
1551 title: None,
1552 grow: 0,
1553 });
1554 self.styled(format!("[ {} ]", label.into()), style);
1555 self.commands.push(Command::EndContainer);
1556 self.last_text_idx = None;
1557
1558 activated
1559 }
1560
1561 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
1566 let focused = self.register_focusable();
1567 let interaction_id = self.interaction_count;
1568 self.interaction_count += 1;
1569 let response = self.response_for(interaction_id);
1570 let mut should_toggle = response.clicked;
1571
1572 if focused {
1573 let mut consumed_indices = Vec::new();
1574 for (i, event) in self.events.iter().enumerate() {
1575 if let Event::Key(key) = event {
1576 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1577 should_toggle = true;
1578 consumed_indices.push(i);
1579 }
1580 }
1581 }
1582
1583 for index in consumed_indices {
1584 self.consumed[index] = true;
1585 }
1586 }
1587
1588 if should_toggle {
1589 *checked = !*checked;
1590 }
1591
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 let marker_style = if *checked {
1605 Style::new().fg(self.theme.success)
1606 } else {
1607 Style::new().fg(self.theme.text_dim)
1608 };
1609 let marker = if *checked { "[x]" } else { "[ ]" };
1610 let label_text = label.into();
1611 if focused {
1612 self.styled(format!("▸ {marker}"), marker_style.bold());
1613 self.styled(label_text, Style::new().fg(self.theme.text).bold());
1614 } else {
1615 self.styled(marker, marker_style);
1616 self.styled(label_text, Style::new().fg(self.theme.text));
1617 }
1618 self.commands.push(Command::EndContainer);
1619 self.last_text_idx = None;
1620
1621 self
1622 }
1623
1624 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
1630 let focused = self.register_focusable();
1631 let interaction_id = self.interaction_count;
1632 self.interaction_count += 1;
1633 let response = self.response_for(interaction_id);
1634 let mut should_toggle = response.clicked;
1635
1636 if focused {
1637 let mut consumed_indices = Vec::new();
1638 for (i, event) in self.events.iter().enumerate() {
1639 if let Event::Key(key) = event {
1640 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1641 should_toggle = true;
1642 consumed_indices.push(i);
1643 }
1644 }
1645 }
1646
1647 for index in consumed_indices {
1648 self.consumed[index] = true;
1649 }
1650 }
1651
1652 if should_toggle {
1653 *on = !*on;
1654 }
1655
1656 self.commands.push(Command::BeginContainer {
1657 direction: Direction::Row,
1658 gap: 2,
1659 align: Align::Start,
1660 border: None,
1661 border_style: Style::new().fg(self.theme.border),
1662 padding: Padding::default(),
1663 margin: Margin::default(),
1664 constraints: Constraints::default(),
1665 title: None,
1666 grow: 0,
1667 });
1668 let label_text = label.into();
1669 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1670 let switch_style = if *on {
1671 Style::new().fg(self.theme.success)
1672 } else {
1673 Style::new().fg(self.theme.text_dim)
1674 };
1675 if focused {
1676 self.styled(
1677 format!("▸ {label_text}"),
1678 Style::new().fg(self.theme.text).bold(),
1679 );
1680 self.styled(switch, switch_style.bold());
1681 } else {
1682 self.styled(label_text, Style::new().fg(self.theme.text));
1683 self.styled(switch, switch_style);
1684 }
1685 self.commands.push(Command::EndContainer);
1686 self.last_text_idx = None;
1687
1688 self
1689 }
1690
1691 pub fn separator(&mut self) -> &mut Self {
1696 self.commands.push(Command::Text {
1697 content: "─".repeat(200),
1698 style: Style::new().fg(self.theme.border).dim(),
1699 grow: 0,
1700 align: Align::Start,
1701 wrap: false,
1702 margin: Margin::default(),
1703 constraints: Constraints::default(),
1704 });
1705 self.last_text_idx = Some(self.commands.len() - 1);
1706 self
1707 }
1708
1709 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
1715 if bindings.is_empty() {
1716 return self;
1717 }
1718
1719 self.interaction_count += 1;
1720 self.commands.push(Command::BeginContainer {
1721 direction: Direction::Row,
1722 gap: 2,
1723 align: Align::Start,
1724 border: None,
1725 border_style: Style::new().fg(self.theme.border),
1726 padding: Padding::default(),
1727 margin: Margin::default(),
1728 constraints: Constraints::default(),
1729 title: None,
1730 grow: 0,
1731 });
1732 for (idx, (key, action)) in bindings.iter().enumerate() {
1733 if idx > 0 {
1734 self.styled("·", Style::new().fg(self.theme.text_dim));
1735 }
1736 self.styled(*key, Style::new().bold().fg(self.theme.primary));
1737 self.styled(*action, Style::new().fg(self.theme.text_dim));
1738 }
1739 self.commands.push(Command::EndContainer);
1740 self.last_text_idx = None;
1741
1742 self
1743 }
1744
1745 pub fn key(&self, c: char) -> bool {
1751 self.events.iter().enumerate().any(|(i, e)| {
1752 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
1753 })
1754 }
1755
1756 pub fn key_code(&self, code: KeyCode) -> bool {
1760 self.events
1761 .iter()
1762 .enumerate()
1763 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
1764 }
1765
1766 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
1770 self.events.iter().enumerate().any(|(i, e)| {
1771 !self.consumed[i]
1772 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
1773 })
1774 }
1775
1776 pub fn mouse_down(&self) -> Option<(u32, u32)> {
1780 self.events.iter().enumerate().find_map(|(i, event)| {
1781 if self.consumed[i] {
1782 return None;
1783 }
1784 if let Event::Mouse(mouse) = event {
1785 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1786 return Some((mouse.x, mouse.y));
1787 }
1788 }
1789 None
1790 })
1791 }
1792
1793 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
1798 self.mouse_pos
1799 }
1800
1801 pub fn scroll_up(&self) -> bool {
1803 self.events.iter().enumerate().any(|(i, event)| {
1804 !self.consumed[i]
1805 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
1806 })
1807 }
1808
1809 pub fn scroll_down(&self) -> bool {
1811 self.events.iter().enumerate().any(|(i, event)| {
1812 !self.consumed[i]
1813 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
1814 })
1815 }
1816
1817 pub fn quit(&mut self) {
1819 self.should_quit = true;
1820 }
1821
1822 pub fn theme(&self) -> &Theme {
1824 &self.theme
1825 }
1826
1827 pub fn set_theme(&mut self, theme: Theme) {
1831 self.theme = theme;
1832 }
1833
1834 pub fn width(&self) -> u32 {
1838 self.area_width
1839 }
1840
1841 pub fn height(&self) -> u32 {
1843 self.area_height
1844 }
1845
1846 pub fn tick(&self) -> u64 {
1851 self.tick
1852 }
1853
1854 pub fn debug_enabled(&self) -> bool {
1858 self.debug
1859 }
1860}
1861
1862#[inline]
1863fn byte_index_for_char(value: &str, char_index: usize) -> usize {
1864 if char_index == 0 {
1865 return 0;
1866 }
1867 value
1868 .char_indices()
1869 .nth(char_index)
1870 .map_or(value.len(), |(idx, _)| idx)
1871}
1872
1873fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
1874 let mut parts: Vec<String> = Vec::new();
1875 for (i, width) in widths.iter().enumerate() {
1876 let cell = cells.get(i).map(String::as_str).unwrap_or("");
1877 let cell_width = UnicodeWidthStr::width(cell) as u32;
1878 let padding = (*width).saturating_sub(cell_width) as usize;
1879 parts.push(format!("{cell}{}", " ".repeat(padding)));
1880 }
1881 parts.join(separator)
1882}