1use super::*;
2use crate::{DirectoryTreeState, RichLogState, TreeNode};
3
4enum MdInline {
6 Text(String),
7 Link { text: String, url: String },
8 Image { alt: String, url: String },
9}
10
11impl Context {
12 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
29 slt_assert(cols > 0, "grid() requires at least 1 column");
30 let interaction_id = self.next_interaction_id();
31 let border = self.theme.border;
32
33 self.commands.push(Command::BeginContainer {
34 direction: Direction::Column,
35 gap: 0,
36 align: Align::Start,
37 align_self: None,
38 justify: Justify::Start,
39 border: None,
40 border_sides: BorderSides::all(),
41 border_style: Style::new().fg(border),
42 bg_color: None,
43 padding: Padding::default(),
44 margin: Margin::default(),
45 constraints: Constraints::default(),
46 title: None,
47 grow: 0,
48 group_name: None,
49 });
50
51 let children_start = self.commands.len();
52 f(self);
53 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
54
55 let mut elements: Vec<Vec<Command>> = Vec::new();
56 let mut iter = child_commands.into_iter().peekable();
57 while let Some(cmd) = iter.next() {
58 match cmd {
59 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
60 let mut depth = 1_u32;
61 let mut element = vec![cmd];
62 for next in iter.by_ref() {
63 match next {
64 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
65 depth += 1;
66 }
67 Command::EndContainer => {
68 depth = depth.saturating_sub(1);
69 }
70 _ => {}
71 }
72 let at_end = matches!(next, Command::EndContainer) && depth == 0;
73 element.push(next);
74 if at_end {
75 break;
76 }
77 }
78 elements.push(element);
79 }
80 Command::EndContainer => {}
81 _ => elements.push(vec![cmd]),
82 }
83 }
84
85 let cols = cols.max(1) as usize;
86 for row in elements.chunks(cols) {
87 self.interaction_count += 1;
88 self.commands.push(Command::BeginContainer {
89 direction: Direction::Row,
90 gap: 0,
91 align: Align::Start,
92 align_self: None,
93 justify: Justify::Start,
94 border: None,
95 border_sides: BorderSides::all(),
96 border_style: Style::new().fg(border),
97 bg_color: None,
98 padding: Padding::default(),
99 margin: Margin::default(),
100 constraints: Constraints::default(),
101 title: None,
102 grow: 0,
103 group_name: None,
104 });
105
106 for element in row {
107 self.interaction_count += 1;
108 self.commands.push(Command::BeginContainer {
109 direction: Direction::Column,
110 gap: 0,
111 align: Align::Start,
112 align_self: None,
113 justify: Justify::Start,
114 border: None,
115 border_sides: BorderSides::all(),
116 border_style: Style::new().fg(border),
117 bg_color: None,
118 padding: Padding::default(),
119 margin: Margin::default(),
120 constraints: Constraints::default(),
121 title: None,
122 grow: 1,
123 group_name: None,
124 });
125 self.commands.extend(element.iter().cloned());
126 self.commands.push(Command::EndContainer);
127 }
128
129 self.commands.push(Command::EndContainer);
130 }
131
132 self.commands.push(Command::EndContainer);
133 self.last_text_idx = None;
134
135 self.response_for(interaction_id)
136 }
137
138 pub fn list(&mut self, state: &mut ListState) -> Response {
144 self.list_colored(state, &WidgetColors::new())
145 }
146
147 pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
149 let visible = state.visible_indices().to_vec();
150 if visible.is_empty() && state.items.is_empty() {
151 state.selected = 0;
152 return Response::none();
153 }
154
155 if !visible.is_empty() {
156 state.selected = state.selected.min(visible.len().saturating_sub(1));
157 }
158
159 let old_selected = state.selected;
160 let focused = self.register_focusable();
161 let interaction_id = self.next_interaction_id();
162 let mut response = self.response_for(interaction_id);
163 response.focused = focused;
164
165 if focused {
166 let mut consumed_indices = Vec::new();
167 for (i, event) in self.events.iter().enumerate() {
168 if let Event::Key(key) = event {
169 if key.kind != KeyEventKind::Press {
170 continue;
171 }
172 match key.code {
173 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
174 let _ = handle_vertical_nav(
175 &mut state.selected,
176 visible.len().saturating_sub(1),
177 key.code.clone(),
178 );
179 consumed_indices.push(i);
180 }
181 _ => {}
182 }
183 }
184 }
185
186 for index in consumed_indices {
187 self.consumed[index] = true;
188 }
189 }
190
191 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
192 for (i, event) in self.events.iter().enumerate() {
193 if self.consumed[i] {
194 continue;
195 }
196 if let Event::Mouse(mouse) = event {
197 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
198 continue;
199 }
200 let in_bounds = mouse.x >= rect.x
201 && mouse.x < rect.right()
202 && mouse.y >= rect.y
203 && mouse.y < rect.bottom();
204 if !in_bounds {
205 continue;
206 }
207 let clicked_idx = (mouse.y - rect.y) as usize;
208 if clicked_idx < visible.len() {
209 state.selected = clicked_idx;
210 self.consumed[i] = true;
211 }
212 }
213 }
214 }
215
216 self.commands.push(Command::BeginContainer {
217 direction: Direction::Column,
218 gap: 0,
219 align: Align::Start,
220 align_self: None,
221 justify: Justify::Start,
222 border: None,
223 border_sides: BorderSides::all(),
224 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
225 bg_color: None,
226 padding: Padding::default(),
227 margin: Margin::default(),
228 constraints: Constraints::default(),
229 title: None,
230 grow: 0,
231 group_name: None,
232 });
233
234 for (view_idx, &item_idx) in visible.iter().enumerate() {
235 let item = &state.items[item_idx];
236 if view_idx == state.selected {
237 let mut selected_style = Style::new()
238 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
239 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
240 if focused {
241 selected_style = selected_style.bold();
242 }
243 let mut row = String::with_capacity(2 + item.len());
244 row.push_str("▸ ");
245 row.push_str(item);
246 self.styled(row, selected_style);
247 } else {
248 let mut row = String::with_capacity(2 + item.len());
249 row.push_str(" ");
250 row.push_str(item);
251 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
252 }
253 }
254
255 self.commands.push(Command::EndContainer);
256 self.last_text_idx = None;
257
258 response.changed = state.selected != old_selected;
259 response
260 }
261
262 pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
264 let focused = self.register_focusable();
265 let interaction_id = self.next_interaction_id();
266 let mut response = self.response_for(interaction_id);
267 response.focused = focused;
268
269 let month_days = CalendarState::days_in_month(state.year, state.month);
270 state.cursor_day = state.cursor_day.clamp(1, month_days);
271 if let Some(day) = state.selected_day {
272 state.selected_day = Some(day.min(month_days));
273 }
274 let old_selected = state.selected_day;
275
276 if focused {
277 let mut consumed_indices = Vec::new();
278 for (i, event) in self.events.iter().enumerate() {
279 if self.consumed[i] {
280 continue;
281 }
282 if let Event::Key(key) = event {
283 if key.kind != KeyEventKind::Press {
284 continue;
285 }
286 match key.code {
287 KeyCode::Left => {
288 calendar_move_cursor_by_days(state, -1);
289 consumed_indices.push(i);
290 }
291 KeyCode::Right => {
292 calendar_move_cursor_by_days(state, 1);
293 consumed_indices.push(i);
294 }
295 KeyCode::Up => {
296 calendar_move_cursor_by_days(state, -7);
297 consumed_indices.push(i);
298 }
299 KeyCode::Down => {
300 calendar_move_cursor_by_days(state, 7);
301 consumed_indices.push(i);
302 }
303 KeyCode::Char('h') => {
304 state.prev_month();
305 consumed_indices.push(i);
306 }
307 KeyCode::Char('l') => {
308 state.next_month();
309 consumed_indices.push(i);
310 }
311 KeyCode::Enter | KeyCode::Char(' ') => {
312 state.selected_day = Some(state.cursor_day);
313 consumed_indices.push(i);
314 }
315 _ => {}
316 }
317 }
318 }
319
320 for index in consumed_indices {
321 self.consumed[index] = true;
322 }
323 }
324
325 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
326 for (i, event) in self.events.iter().enumerate() {
327 if self.consumed[i] {
328 continue;
329 }
330 if let Event::Mouse(mouse) = event {
331 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
332 continue;
333 }
334 let in_bounds = mouse.x >= rect.x
335 && mouse.x < rect.right()
336 && mouse.y >= rect.y
337 && mouse.y < rect.bottom();
338 if !in_bounds {
339 continue;
340 }
341
342 let rel_x = mouse.x.saturating_sub(rect.x);
343 let rel_y = mouse.y.saturating_sub(rect.y);
344 if rel_y == 0 {
345 if rel_x <= 2 {
346 state.prev_month();
347 self.consumed[i] = true;
348 continue;
349 }
350 if rel_x + 3 >= rect.width {
351 state.next_month();
352 self.consumed[i] = true;
353 continue;
354 }
355 }
356
357 if !(2..8).contains(&rel_y) {
358 continue;
359 }
360 if rel_x >= 21 {
361 continue;
362 }
363
364 let week = rel_y - 2;
365 let col = rel_x / 3;
366 let day_index = week * 7 + col;
367 let first = CalendarState::first_weekday(state.year, state.month);
368 let days = CalendarState::days_in_month(state.year, state.month);
369 if day_index < first {
370 continue;
371 }
372 let day = day_index - first + 1;
373 if day == 0 || day > days {
374 continue;
375 }
376 state.cursor_day = day;
377 state.selected_day = Some(day);
378 self.consumed[i] = true;
379 }
380 }
381 }
382
383 let title = {
384 let month_name = calendar_month_name(state.month);
385 let mut s = String::with_capacity(16);
386 s.push_str(&state.year.to_string());
387 s.push(' ');
388 s.push_str(month_name);
389 s
390 };
391
392 self.commands.push(Command::BeginContainer {
393 direction: Direction::Column,
394 gap: 0,
395 align: Align::Start,
396 align_self: None,
397 justify: Justify::Start,
398 border: None,
399 border_sides: BorderSides::all(),
400 border_style: Style::new().fg(self.theme.border),
401 bg_color: None,
402 padding: Padding::default(),
403 margin: Margin::default(),
404 constraints: Constraints::default(),
405 title: None,
406 grow: 0,
407 group_name: None,
408 });
409
410 self.commands.push(Command::BeginContainer {
411 direction: Direction::Row,
412 gap: 1,
413 align: Align::Start,
414 align_self: None,
415 justify: Justify::Start,
416 border: None,
417 border_sides: BorderSides::all(),
418 border_style: Style::new().fg(self.theme.border),
419 bg_color: None,
420 padding: Padding::default(),
421 margin: Margin::default(),
422 constraints: Constraints::default(),
423 title: None,
424 grow: 0,
425 group_name: None,
426 });
427 self.styled("◀", Style::new().fg(self.theme.text));
428 self.styled(title, Style::new().bold().fg(self.theme.text));
429 self.styled("▶", Style::new().fg(self.theme.text));
430 self.commands.push(Command::EndContainer);
431
432 self.commands.push(Command::BeginContainer {
433 direction: Direction::Row,
434 gap: 0,
435 align: Align::Start,
436 align_self: None,
437 justify: Justify::Start,
438 border: None,
439 border_sides: BorderSides::all(),
440 border_style: Style::new().fg(self.theme.border),
441 bg_color: None,
442 padding: Padding::default(),
443 margin: Margin::default(),
444 constraints: Constraints::default(),
445 title: None,
446 grow: 0,
447 group_name: None,
448 });
449 for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
450 self.styled(
451 format!("{wd:>2} "),
452 Style::new().fg(self.theme.text_dim).bold(),
453 );
454 }
455 self.commands.push(Command::EndContainer);
456
457 let first = CalendarState::first_weekday(state.year, state.month);
458 let days = CalendarState::days_in_month(state.year, state.month);
459 for week in 0..6_u32 {
460 self.commands.push(Command::BeginContainer {
461 direction: Direction::Row,
462 gap: 0,
463 align: Align::Start,
464 align_self: None,
465 justify: Justify::Start,
466 border: None,
467 border_sides: BorderSides::all(),
468 border_style: Style::new().fg(self.theme.border),
469 bg_color: None,
470 padding: Padding::default(),
471 margin: Margin::default(),
472 constraints: Constraints::default(),
473 title: None,
474 grow: 0,
475 group_name: None,
476 });
477
478 for col in 0..7_u32 {
479 let idx = week * 7 + col;
480 if idx < first || idx >= first + days {
481 self.styled(" ", Style::new().fg(self.theme.text_dim));
482 continue;
483 }
484 let day = idx - first + 1;
485 let text = format!("{day:>2} ");
486 let style = if state.selected_day == Some(day) {
487 Style::new()
488 .bg(self.theme.selected_bg)
489 .fg(self.theme.selected_fg)
490 } else if state.cursor_day == day {
491 Style::new().fg(self.theme.primary).bold()
492 } else {
493 Style::new().fg(self.theme.text)
494 };
495 self.styled(text, style);
496 }
497
498 self.commands.push(Command::EndContainer);
499 }
500
501 self.commands.push(Command::EndContainer);
502 self.last_text_idx = None;
503 response.changed = state.selected_day != old_selected;
504 response
505 }
506
507 pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
509 if state.dirty {
510 state.refresh();
511 }
512 if !state.entries.is_empty() {
513 state.selected = state.selected.min(state.entries.len().saturating_sub(1));
514 }
515
516 let focused = self.register_focusable();
517 let interaction_id = self.next_interaction_id();
518 let mut response = self.response_for(interaction_id);
519 response.focused = focused;
520 let mut file_selected = false;
521
522 if focused {
523 let mut consumed_indices = Vec::new();
524 for (i, event) in self.events.iter().enumerate() {
525 if self.consumed[i] {
526 continue;
527 }
528 if let Event::Key(key) = event {
529 if key.kind != KeyEventKind::Press {
530 continue;
531 }
532 match key.code {
533 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
534 if !state.entries.is_empty() {
535 let _ = handle_vertical_nav(
536 &mut state.selected,
537 state.entries.len().saturating_sub(1),
538 key.code.clone(),
539 );
540 }
541 consumed_indices.push(i);
542 }
543 KeyCode::Enter => {
544 if let Some(entry) = state.entries.get(state.selected).cloned() {
545 if entry.is_dir {
546 state.current_dir = entry.path;
547 state.selected = 0;
548 state.selected_file = None;
549 state.dirty = true;
550 } else {
551 state.selected_file = Some(entry.path);
552 file_selected = true;
553 }
554 }
555 consumed_indices.push(i);
556 }
557 KeyCode::Backspace => {
558 if let Some(parent) =
559 state.current_dir.parent().map(|p| p.to_path_buf())
560 {
561 state.current_dir = parent;
562 state.selected = 0;
563 state.selected_file = None;
564 state.dirty = true;
565 }
566 consumed_indices.push(i);
567 }
568 KeyCode::Char('h') => {
569 state.show_hidden = !state.show_hidden;
570 state.selected = 0;
571 state.dirty = true;
572 consumed_indices.push(i);
573 }
574 KeyCode::Esc => {
575 state.selected_file = None;
576 consumed_indices.push(i);
577 }
578 _ => {}
579 }
580 }
581 }
582
583 for index in consumed_indices {
584 self.consumed[index] = true;
585 }
586 }
587
588 if state.dirty {
589 state.refresh();
590 }
591
592 self.commands.push(Command::BeginContainer {
593 direction: Direction::Column,
594 gap: 0,
595 align: Align::Start,
596 align_self: None,
597 justify: Justify::Start,
598 border: None,
599 border_sides: BorderSides::all(),
600 border_style: Style::new().fg(self.theme.border),
601 bg_color: None,
602 padding: Padding::default(),
603 margin: Margin::default(),
604 constraints: Constraints::default(),
605 title: None,
606 grow: 0,
607 group_name: None,
608 });
609
610 let dir_text = {
611 let dir = state.current_dir.display().to_string();
612 let mut text = String::with_capacity(5 + dir.len());
613 text.push_str("Dir: ");
614 text.push_str(&dir);
615 text
616 };
617 self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
618
619 if state.entries.is_empty() {
620 self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
621 } else {
622 for (idx, entry) in state.entries.iter().enumerate() {
623 let icon = if entry.is_dir { "▸ " } else { " " };
624 let row = if entry.is_dir {
625 let mut row = String::with_capacity(icon.len() + entry.name.len());
626 row.push_str(icon);
627 row.push_str(&entry.name);
628 row
629 } else {
630 let size_text = entry.size.to_string();
631 let mut row =
632 String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
633 row.push_str(icon);
634 row.push_str(&entry.name);
635 row.push_str(" ");
636 row.push_str(&size_text);
637 row.push_str(" B");
638 row
639 };
640
641 let style = if idx == state.selected {
642 if focused {
643 Style::new().bold().fg(self.theme.primary)
644 } else {
645 Style::new().fg(self.theme.primary)
646 }
647 } else {
648 Style::new().fg(self.theme.text)
649 };
650 self.styled(row, style);
651 }
652 }
653
654 self.commands.push(Command::EndContainer);
655 self.last_text_idx = None;
656
657 response.changed = file_selected;
658 response
659 }
660
661 pub fn table(&mut self, state: &mut TableState) -> Response {
667 self.table_colored(state, &WidgetColors::new())
668 }
669
670 pub fn table_colored(&mut self, state: &mut TableState, colors: &WidgetColors) -> Response {
672 if state.is_dirty() {
673 state.recompute_widths();
674 }
675
676 let old_selected = state.selected;
677 let old_sort_column = state.sort_column;
678 let old_sort_ascending = state.sort_ascending;
679 let old_page = state.page;
680 let old_filter = state.filter.clone();
681
682 let focused = self.register_focusable();
683 let interaction_id = self.next_interaction_id();
684 let mut response = self.response_for(interaction_id);
685 response.focused = focused;
686
687 self.table_handle_events(state, focused, interaction_id);
688
689 if state.is_dirty() {
690 state.recompute_widths();
691 }
692
693 self.table_render(state, focused, colors);
694
695 response.changed = state.selected != old_selected
696 || state.sort_column != old_sort_column
697 || state.sort_ascending != old_sort_ascending
698 || state.page != old_page
699 || state.filter != old_filter;
700 response
701 }
702
703 fn table_handle_events(
704 &mut self,
705 state: &mut TableState,
706 focused: bool,
707 interaction_id: usize,
708 ) {
709 self.handle_table_keys(state, focused);
710
711 if state.visible_indices().is_empty() && state.headers.is_empty() {
712 return;
713 }
714
715 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
716 for (i, event) in self.events.iter().enumerate() {
717 if self.consumed[i] {
718 continue;
719 }
720 if let Event::Mouse(mouse) = event {
721 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
722 continue;
723 }
724 let in_bounds = mouse.x >= rect.x
725 && mouse.x < rect.right()
726 && mouse.y >= rect.y
727 && mouse.y < rect.bottom();
728 if !in_bounds {
729 continue;
730 }
731
732 if mouse.y == rect.y {
733 let rel_x = mouse.x.saturating_sub(rect.x);
734 let mut x_offset = 0u32;
735 for (col_idx, width) in state.column_widths().iter().enumerate() {
736 if rel_x >= x_offset && rel_x < x_offset + *width {
737 state.toggle_sort(col_idx);
738 state.selected = 0;
739 self.consumed[i] = true;
740 break;
741 }
742 x_offset += *width;
743 if col_idx + 1 < state.column_widths().len() {
744 x_offset += 3;
745 }
746 }
747 continue;
748 }
749
750 if mouse.y < rect.y + 2 {
751 continue;
752 }
753
754 let visible_len = if state.page_size > 0 {
755 let start = state
756 .page
757 .saturating_mul(state.page_size)
758 .min(state.visible_indices().len());
759 let end = (start + state.page_size).min(state.visible_indices().len());
760 end.saturating_sub(start)
761 } else {
762 state.visible_indices().len()
763 };
764 let clicked_idx = (mouse.y - rect.y - 2) as usize;
765 if clicked_idx < visible_len {
766 state.selected = clicked_idx;
767 self.consumed[i] = true;
768 }
769 }
770 }
771 }
772 }
773
774 fn table_render(&mut self, state: &mut TableState, focused: bool, colors: &WidgetColors) {
775 let total_visible = state.visible_indices().len();
776 let page_start = if state.page_size > 0 {
777 state
778 .page
779 .saturating_mul(state.page_size)
780 .min(total_visible)
781 } else {
782 0
783 };
784 let page_end = if state.page_size > 0 {
785 (page_start + state.page_size).min(total_visible)
786 } else {
787 total_visible
788 };
789 let visible_len = page_end.saturating_sub(page_start);
790 state.selected = state.selected.min(visible_len.saturating_sub(1));
791
792 self.commands.push(Command::BeginContainer {
793 direction: Direction::Column,
794 gap: 0,
795 align: Align::Start,
796 align_self: None,
797 justify: Justify::Start,
798 border: None,
799 border_sides: BorderSides::all(),
800 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
801 bg_color: None,
802 padding: Padding::default(),
803 margin: Margin::default(),
804 constraints: Constraints::default(),
805 title: None,
806 grow: 0,
807 group_name: None,
808 });
809
810 self.render_table_header(state, colors);
811 self.render_table_rows(state, focused, page_start, visible_len, colors);
812
813 if state.page_size > 0 && state.total_pages() > 1 {
814 let current_page = (state.page + 1).to_string();
815 let total_pages = state.total_pages().to_string();
816 let mut page_text = String::with_capacity(current_page.len() + total_pages.len() + 6);
817 page_text.push_str("Page ");
818 page_text.push_str(¤t_page);
819 page_text.push('/');
820 page_text.push_str(&total_pages);
821 self.styled(
822 page_text,
823 Style::new()
824 .dim()
825 .fg(colors.fg.unwrap_or(self.theme.text_dim)),
826 );
827 }
828
829 self.commands.push(Command::EndContainer);
830 self.last_text_idx = None;
831 }
832
833 fn handle_table_keys(&mut self, state: &mut TableState, focused: bool) {
834 if !focused || state.visible_indices().is_empty() {
835 return;
836 }
837
838 let mut consumed_indices = Vec::new();
839 for (i, event) in self.events.iter().enumerate() {
840 if let Event::Key(key) = event {
841 if key.kind != KeyEventKind::Press {
842 continue;
843 }
844 match key.code {
845 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
846 let visible_len = table_visible_len(state);
847 state.selected = state.selected.min(visible_len.saturating_sub(1));
848 let _ = handle_vertical_nav(
849 &mut state.selected,
850 visible_len.saturating_sub(1),
851 key.code.clone(),
852 );
853 consumed_indices.push(i);
854 }
855 KeyCode::PageUp => {
856 let old_page = state.page;
857 state.prev_page();
858 if state.page != old_page {
859 state.selected = 0;
860 }
861 consumed_indices.push(i);
862 }
863 KeyCode::PageDown => {
864 let old_page = state.page;
865 state.next_page();
866 if state.page != old_page {
867 state.selected = 0;
868 }
869 consumed_indices.push(i);
870 }
871 _ => {}
872 }
873 }
874 }
875 for index in consumed_indices {
876 self.consumed[index] = true;
877 }
878 }
879
880 fn render_table_header(&mut self, state: &TableState, colors: &WidgetColors) {
881 let header_cells = state
882 .headers
883 .iter()
884 .enumerate()
885 .map(|(i, header)| {
886 if state.sort_column == Some(i) {
887 if state.sort_ascending {
888 let mut sorted_header = String::with_capacity(header.len() + 2);
889 sorted_header.push_str(header);
890 sorted_header.push_str(" ▲");
891 sorted_header
892 } else {
893 let mut sorted_header = String::with_capacity(header.len() + 2);
894 sorted_header.push_str(header);
895 sorted_header.push_str(" ▼");
896 sorted_header
897 }
898 } else {
899 header.clone()
900 }
901 })
902 .collect::<Vec<_>>();
903 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
904 self.styled(
905 header_line,
906 Style::new().bold().fg(colors.fg.unwrap_or(self.theme.text)),
907 );
908
909 let separator = state
910 .column_widths()
911 .iter()
912 .map(|w| "─".repeat(*w as usize))
913 .collect::<Vec<_>>()
914 .join("─┼─");
915 self.text(separator);
916 }
917
918 fn render_table_rows(
919 &mut self,
920 state: &TableState,
921 focused: bool,
922 page_start: usize,
923 visible_len: usize,
924 colors: &WidgetColors,
925 ) {
926 for idx in 0..visible_len {
927 let data_idx = state.visible_indices()[page_start + idx];
928 let Some(row) = state.rows.get(data_idx) else {
929 continue;
930 };
931 let line = format_table_row(row, state.column_widths(), " │ ");
932 if idx == state.selected {
933 let mut style = Style::new()
934 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
935 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
936 if focused {
937 style = style.bold();
938 }
939 self.styled(line, style);
940 } else {
941 let mut style = Style::new().fg(colors.fg.unwrap_or(self.theme.text));
942 if state.zebra {
943 let zebra_bg = colors.bg.unwrap_or({
944 if idx % 2 == 0 {
945 self.theme.surface
946 } else {
947 self.theme.surface_hover
948 }
949 });
950 style = style.bg(zebra_bg);
951 }
952 self.styled(line, style);
953 }
954 }
955 }
956
957 pub fn tabs(&mut self, state: &mut TabsState) -> Response {
963 self.tabs_colored(state, &WidgetColors::new())
964 }
965
966 pub fn tabs_colored(&mut self, state: &mut TabsState, colors: &WidgetColors) -> Response {
968 if state.labels.is_empty() {
969 state.selected = 0;
970 return Response::none();
971 }
972
973 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
974 let old_selected = state.selected;
975 let focused = self.register_focusable();
976 let interaction_id = self.next_interaction_id();
977 let mut response = self.response_for(interaction_id);
978 response.focused = focused;
979
980 if focused {
981 let mut consumed_indices = Vec::new();
982 for (i, event) in self.events.iter().enumerate() {
983 if let Event::Key(key) = event {
984 if key.kind != KeyEventKind::Press {
985 continue;
986 }
987 match key.code {
988 KeyCode::Left => {
989 state.selected = if state.selected == 0 {
990 state.labels.len().saturating_sub(1)
991 } else {
992 state.selected - 1
993 };
994 consumed_indices.push(i);
995 }
996 KeyCode::Right => {
997 if !state.labels.is_empty() {
998 state.selected = (state.selected + 1) % state.labels.len();
999 }
1000 consumed_indices.push(i);
1001 }
1002 _ => {}
1003 }
1004 }
1005 }
1006
1007 for index in consumed_indices {
1008 self.consumed[index] = true;
1009 }
1010 }
1011
1012 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1013 for (i, event) in self.events.iter().enumerate() {
1014 if self.consumed[i] {
1015 continue;
1016 }
1017 if let Event::Mouse(mouse) = event {
1018 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1019 continue;
1020 }
1021 let in_bounds = mouse.x >= rect.x
1022 && mouse.x < rect.right()
1023 && mouse.y >= rect.y
1024 && mouse.y < rect.bottom();
1025 if !in_bounds {
1026 continue;
1027 }
1028
1029 let mut x_offset = 0u32;
1030 let rel_x = mouse.x - rect.x;
1031 for (idx, label) in state.labels.iter().enumerate() {
1032 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
1033 if rel_x >= x_offset && rel_x < x_offset + tab_width {
1034 state.selected = idx;
1035 self.consumed[i] = true;
1036 break;
1037 }
1038 x_offset += tab_width + 1;
1039 }
1040 }
1041 }
1042 }
1043
1044 self.commands.push(Command::BeginContainer {
1045 direction: Direction::Row,
1046 gap: 1,
1047 align: Align::Start,
1048 align_self: None,
1049 justify: Justify::Start,
1050 border: None,
1051 border_sides: BorderSides::all(),
1052 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1053 bg_color: None,
1054 padding: Padding::default(),
1055 margin: Margin::default(),
1056 constraints: Constraints::default(),
1057 title: None,
1058 grow: 0,
1059 group_name: None,
1060 });
1061 for (idx, label) in state.labels.iter().enumerate() {
1062 let style = if idx == state.selected {
1063 let s = Style::new()
1064 .fg(colors.accent.unwrap_or(self.theme.primary))
1065 .bold();
1066 if focused {
1067 s.underline()
1068 } else {
1069 s
1070 }
1071 } else {
1072 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1073 };
1074 let mut tab = String::with_capacity(label.len() + 4);
1075 tab.push_str("[ ");
1076 tab.push_str(label);
1077 tab.push_str(" ]");
1078 self.styled(tab, style);
1079 }
1080 self.commands.push(Command::EndContainer);
1081 self.last_text_idx = None;
1082
1083 response.changed = state.selected != old_selected;
1084 response
1085 }
1086
1087 pub fn button(&mut self, label: impl Into<String>) -> Response {
1093 self.button_colored(label, &WidgetColors::new())
1094 }
1095
1096 pub fn button_colored(&mut self, label: impl Into<String>, colors: &WidgetColors) -> Response {
1098 let focused = self.register_focusable();
1099 let interaction_id = self.next_interaction_id();
1100 let mut response = self.response_for(interaction_id);
1101 response.focused = focused;
1102
1103 let mut activated = response.clicked;
1104 if focused {
1105 let mut consumed_indices = Vec::new();
1106 for (i, event) in self.events.iter().enumerate() {
1107 if let Event::Key(key) = event {
1108 if key.kind != KeyEventKind::Press {
1109 continue;
1110 }
1111 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1112 activated = true;
1113 consumed_indices.push(i);
1114 }
1115 }
1116 }
1117
1118 for index in consumed_indices {
1119 self.consumed[index] = true;
1120 }
1121 }
1122
1123 let hovered = response.hovered;
1124 let base_fg = colors.fg.unwrap_or(self.theme.text);
1125 let accent = colors.accent.unwrap_or(self.theme.accent);
1126 let base_bg = colors.bg.unwrap_or(self.theme.surface_hover);
1127 let style = if focused {
1128 Style::new().fg(accent).bold()
1129 } else if hovered {
1130 Style::new().fg(accent)
1131 } else {
1132 Style::new().fg(base_fg)
1133 };
1134 let has_custom_bg = colors.bg.is_some();
1135 let bg_color = if has_custom_bg || hovered || focused {
1136 Some(base_bg)
1137 } else {
1138 None
1139 };
1140
1141 self.commands.push(Command::BeginContainer {
1142 direction: Direction::Row,
1143 gap: 0,
1144 align: Align::Start,
1145 align_self: None,
1146 justify: Justify::Start,
1147 border: None,
1148 border_sides: BorderSides::all(),
1149 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1150 bg_color,
1151 padding: Padding::default(),
1152 margin: Margin::default(),
1153 constraints: Constraints::default(),
1154 title: None,
1155 grow: 0,
1156 group_name: None,
1157 });
1158 let raw_label = label.into();
1159 let mut label_text = String::with_capacity(raw_label.len() + 4);
1160 label_text.push_str("[ ");
1161 label_text.push_str(&raw_label);
1162 label_text.push_str(" ]");
1163 self.styled(label_text, style);
1164 self.commands.push(Command::EndContainer);
1165 self.last_text_idx = None;
1166
1167 response.clicked = activated;
1168 response
1169 }
1170
1171 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> Response {
1176 let focused = self.register_focusable();
1177 let interaction_id = self.next_interaction_id();
1178 let mut response = self.response_for(interaction_id);
1179 response.focused = focused;
1180
1181 let mut activated = response.clicked;
1182 if focused {
1183 let mut consumed_indices = Vec::new();
1184 for (i, event) in self.events.iter().enumerate() {
1185 if let Event::Key(key) = event {
1186 if key.kind != KeyEventKind::Press {
1187 continue;
1188 }
1189 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1190 activated = true;
1191 consumed_indices.push(i);
1192 }
1193 }
1194 }
1195 for index in consumed_indices {
1196 self.consumed[index] = true;
1197 }
1198 }
1199
1200 let label = label.into();
1201 let hover_bg = if response.hovered || focused {
1202 Some(self.theme.surface_hover)
1203 } else {
1204 None
1205 };
1206 let (text, style, bg_color, border) = match variant {
1207 ButtonVariant::Default => {
1208 let style = if focused {
1209 Style::new().fg(self.theme.primary).bold()
1210 } else if response.hovered {
1211 Style::new().fg(self.theme.accent)
1212 } else {
1213 Style::new().fg(self.theme.text)
1214 };
1215 let mut text = String::with_capacity(label.len() + 4);
1216 text.push_str("[ ");
1217 text.push_str(&label);
1218 text.push_str(" ]");
1219 (text, style, hover_bg, None)
1220 }
1221 ButtonVariant::Primary => {
1222 let style = if focused {
1223 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
1224 } else if response.hovered {
1225 Style::new().fg(self.theme.bg).bg(self.theme.accent)
1226 } else {
1227 Style::new().fg(self.theme.bg).bg(self.theme.primary)
1228 };
1229 let mut text = String::with_capacity(label.len() + 2);
1230 text.push(' ');
1231 text.push_str(&label);
1232 text.push(' ');
1233 (text, style, hover_bg, None)
1234 }
1235 ButtonVariant::Danger => {
1236 let style = if focused {
1237 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
1238 } else if response.hovered {
1239 Style::new().fg(self.theme.bg).bg(self.theme.warning)
1240 } else {
1241 Style::new().fg(self.theme.bg).bg(self.theme.error)
1242 };
1243 let mut text = String::with_capacity(label.len() + 2);
1244 text.push(' ');
1245 text.push_str(&label);
1246 text.push(' ');
1247 (text, style, hover_bg, None)
1248 }
1249 ButtonVariant::Outline => {
1250 let border_color = if focused {
1251 self.theme.primary
1252 } else if response.hovered {
1253 self.theme.accent
1254 } else {
1255 self.theme.border
1256 };
1257 let style = if focused {
1258 Style::new().fg(self.theme.primary).bold()
1259 } else if response.hovered {
1260 Style::new().fg(self.theme.accent)
1261 } else {
1262 Style::new().fg(self.theme.text)
1263 };
1264 (
1265 {
1266 let mut text = String::with_capacity(label.len() + 2);
1267 text.push(' ');
1268 text.push_str(&label);
1269 text.push(' ');
1270 text
1271 },
1272 style,
1273 hover_bg,
1274 Some((Border::Rounded, Style::new().fg(border_color))),
1275 )
1276 }
1277 };
1278
1279 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
1280 self.commands.push(Command::BeginContainer {
1281 direction: Direction::Row,
1282 gap: 0,
1283 align: Align::Center,
1284 align_self: None,
1285 justify: Justify::Center,
1286 border: if border.is_some() {
1287 Some(btn_border)
1288 } else {
1289 None
1290 },
1291 border_sides: BorderSides::all(),
1292 border_style: btn_border_style,
1293 bg_color,
1294 padding: Padding::default(),
1295 margin: Margin::default(),
1296 constraints: Constraints::default(),
1297 title: None,
1298 grow: 0,
1299 group_name: None,
1300 });
1301 self.styled(text, style);
1302 self.commands.push(Command::EndContainer);
1303 self.last_text_idx = None;
1304
1305 response.clicked = activated;
1306 response
1307 }
1308
1309 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> Response {
1315 self.checkbox_colored(label, checked, &WidgetColors::new())
1316 }
1317
1318 pub fn checkbox_colored(
1320 &mut self,
1321 label: impl Into<String>,
1322 checked: &mut bool,
1323 colors: &WidgetColors,
1324 ) -> Response {
1325 let focused = self.register_focusable();
1326 let interaction_id = self.next_interaction_id();
1327 let mut response = self.response_for(interaction_id);
1328 response.focused = focused;
1329 let mut should_toggle = response.clicked;
1330 let old_checked = *checked;
1331
1332 if focused {
1333 let mut consumed_indices = Vec::new();
1334 for (i, event) in self.events.iter().enumerate() {
1335 if let Event::Key(key) = event {
1336 if key.kind != KeyEventKind::Press {
1337 continue;
1338 }
1339 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1340 should_toggle = true;
1341 consumed_indices.push(i);
1342 }
1343 }
1344 }
1345
1346 for index in consumed_indices {
1347 self.consumed[index] = true;
1348 }
1349 }
1350
1351 if should_toggle {
1352 *checked = !*checked;
1353 }
1354
1355 let hover_bg = if response.hovered || focused {
1356 Some(self.theme.surface_hover)
1357 } else {
1358 None
1359 };
1360 self.commands.push(Command::BeginContainer {
1361 direction: Direction::Row,
1362 gap: 1,
1363 align: Align::Start,
1364 align_self: None,
1365 justify: Justify::Start,
1366 border: None,
1367 border_sides: BorderSides::all(),
1368 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1369 bg_color: hover_bg,
1370 padding: Padding::default(),
1371 margin: Margin::default(),
1372 constraints: Constraints::default(),
1373 title: None,
1374 grow: 0,
1375 group_name: None,
1376 });
1377 let marker_style = if *checked {
1378 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1379 } else {
1380 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1381 };
1382 let marker = if *checked { "[x]" } else { "[ ]" };
1383 let label_text = label.into();
1384 if focused {
1385 let mut marker_text = String::with_capacity(2 + marker.len());
1386 marker_text.push_str("▸ ");
1387 marker_text.push_str(marker);
1388 self.styled(marker_text, marker_style.bold());
1389 self.styled(
1390 label_text,
1391 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1392 );
1393 } else {
1394 self.styled(marker, marker_style);
1395 self.styled(
1396 label_text,
1397 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1398 );
1399 }
1400 self.commands.push(Command::EndContainer);
1401 self.last_text_idx = None;
1402
1403 response.changed = *checked != old_checked;
1404 response
1405 }
1406
1407 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> Response {
1414 self.toggle_colored(label, on, &WidgetColors::new())
1415 }
1416
1417 pub fn toggle_colored(
1419 &mut self,
1420 label: impl Into<String>,
1421 on: &mut bool,
1422 colors: &WidgetColors,
1423 ) -> Response {
1424 let focused = self.register_focusable();
1425 let interaction_id = self.next_interaction_id();
1426 let mut response = self.response_for(interaction_id);
1427 response.focused = focused;
1428 let mut should_toggle = response.clicked;
1429 let old_on = *on;
1430
1431 if focused {
1432 let mut consumed_indices = Vec::new();
1433 for (i, event) in self.events.iter().enumerate() {
1434 if let Event::Key(key) = event {
1435 if key.kind != KeyEventKind::Press {
1436 continue;
1437 }
1438 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1439 should_toggle = true;
1440 consumed_indices.push(i);
1441 }
1442 }
1443 }
1444
1445 for index in consumed_indices {
1446 self.consumed[index] = true;
1447 }
1448 }
1449
1450 if should_toggle {
1451 *on = !*on;
1452 }
1453
1454 let hover_bg = if response.hovered || focused {
1455 Some(self.theme.surface_hover)
1456 } else {
1457 None
1458 };
1459 self.commands.push(Command::BeginContainer {
1460 direction: Direction::Row,
1461 gap: 2,
1462 align: Align::Start,
1463 align_self: None,
1464 justify: Justify::Start,
1465 border: None,
1466 border_sides: BorderSides::all(),
1467 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1468 bg_color: hover_bg,
1469 padding: Padding::default(),
1470 margin: Margin::default(),
1471 constraints: Constraints::default(),
1472 title: None,
1473 grow: 0,
1474 group_name: None,
1475 });
1476 let label_text = label.into();
1477 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1478 let switch_style = if *on {
1479 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1480 } else {
1481 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1482 };
1483 if focused {
1484 let mut focused_label = String::with_capacity(2 + label_text.len());
1485 focused_label.push_str("▸ ");
1486 focused_label.push_str(&label_text);
1487 self.styled(
1488 focused_label,
1489 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1490 );
1491 self.styled(switch, switch_style.bold());
1492 } else {
1493 self.styled(
1494 label_text,
1495 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1496 );
1497 self.styled(switch, switch_style);
1498 }
1499 self.commands.push(Command::EndContainer);
1500 self.last_text_idx = None;
1501
1502 response.changed = *on != old_on;
1503 response
1504 }
1505
1506 pub fn select(&mut self, state: &mut SelectState) -> Response {
1513 self.select_colored(state, &WidgetColors::new())
1514 }
1515
1516 pub fn select_colored(&mut self, state: &mut SelectState, colors: &WidgetColors) -> Response {
1518 if state.items.is_empty() {
1519 return Response::none();
1520 }
1521 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1522
1523 let focused = self.register_focusable();
1524 let interaction_id = self.next_interaction_id();
1525 let mut response = self.response_for(interaction_id);
1526 response.focused = focused;
1527 let old_selected = state.selected;
1528
1529 self.select_handle_events(state, focused, response.clicked);
1530 self.select_render(state, focused, colors);
1531 response.changed = state.selected != old_selected;
1532 response
1533 }
1534
1535 fn select_handle_events(&mut self, state: &mut SelectState, focused: bool, clicked: bool) {
1536 if clicked {
1537 state.open = !state.open;
1538 if state.open {
1539 state.set_cursor(state.selected);
1540 }
1541 }
1542
1543 if !focused {
1544 return;
1545 }
1546
1547 let mut consumed_indices = Vec::new();
1548 for (i, event) in self.events.iter().enumerate() {
1549 if self.consumed[i] {
1550 continue;
1551 }
1552 if let Event::Key(key) = event {
1553 if key.kind != KeyEventKind::Press {
1554 continue;
1555 }
1556 if state.open {
1557 match key.code {
1558 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1559 let mut cursor = state.cursor();
1560 let _ = handle_vertical_nav(
1561 &mut cursor,
1562 state.items.len().saturating_sub(1),
1563 key.code.clone(),
1564 );
1565 state.set_cursor(cursor);
1566 consumed_indices.push(i);
1567 }
1568 KeyCode::Enter | KeyCode::Char(' ') => {
1569 state.selected = state.cursor();
1570 state.open = false;
1571 consumed_indices.push(i);
1572 }
1573 KeyCode::Esc => {
1574 state.open = false;
1575 consumed_indices.push(i);
1576 }
1577 _ => {}
1578 }
1579 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1580 state.open = true;
1581 state.set_cursor(state.selected);
1582 consumed_indices.push(i);
1583 }
1584 }
1585 }
1586 for idx in consumed_indices {
1587 self.consumed[idx] = true;
1588 }
1589 }
1590
1591 fn select_render(&mut self, state: &SelectState, focused: bool, colors: &WidgetColors) {
1592 let border_color = if focused {
1593 colors.accent.unwrap_or(self.theme.primary)
1594 } else {
1595 colors.border.unwrap_or(self.theme.border)
1596 };
1597 let display_text = state
1598 .items
1599 .get(state.selected)
1600 .cloned()
1601 .unwrap_or_else(|| state.placeholder.clone());
1602 let arrow = if state.open { "▲" } else { "▼" };
1603
1604 self.commands.push(Command::BeginContainer {
1605 direction: Direction::Column,
1606 gap: 0,
1607 align: Align::Start,
1608 align_self: None,
1609 justify: Justify::Start,
1610 border: None,
1611 border_sides: BorderSides::all(),
1612 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1613 bg_color: None,
1614 padding: Padding::default(),
1615 margin: Margin::default(),
1616 constraints: Constraints::default(),
1617 title: None,
1618 grow: 0,
1619 group_name: None,
1620 });
1621
1622 self.render_select_trigger(&display_text, arrow, border_color, colors);
1623
1624 if state.open {
1625 self.render_select_dropdown(state, colors);
1626 }
1627
1628 self.commands.push(Command::EndContainer);
1629 self.last_text_idx = None;
1630 }
1631
1632 fn render_select_trigger(
1633 &mut self,
1634 display_text: &str,
1635 arrow: &str,
1636 border_color: Color,
1637 colors: &WidgetColors,
1638 ) {
1639 self.commands.push(Command::BeginContainer {
1640 direction: Direction::Row,
1641 gap: 1,
1642 align: Align::Start,
1643 align_self: None,
1644 justify: Justify::Start,
1645 border: Some(Border::Rounded),
1646 border_sides: BorderSides::all(),
1647 border_style: Style::new().fg(border_color),
1648 bg_color: None,
1649 padding: Padding {
1650 left: 1,
1651 right: 1,
1652 top: 0,
1653 bottom: 0,
1654 },
1655 margin: Margin::default(),
1656 constraints: Constraints::default(),
1657 title: None,
1658 grow: 0,
1659 group_name: None,
1660 });
1661 self.interaction_count += 1;
1662 self.styled(
1663 display_text,
1664 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1665 );
1666 self.styled(
1667 arrow,
1668 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim)),
1669 );
1670 self.commands.push(Command::EndContainer);
1671 self.last_text_idx = None;
1672 }
1673
1674 fn render_select_dropdown(&mut self, state: &SelectState, colors: &WidgetColors) {
1675 for (idx, item) in state.items.iter().enumerate() {
1676 let is_cursor = idx == state.cursor();
1677 let style = if is_cursor {
1678 Style::new()
1679 .bold()
1680 .fg(colors.accent.unwrap_or(self.theme.primary))
1681 } else {
1682 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1683 };
1684 let prefix = if is_cursor { "▸ " } else { " " };
1685 let mut row = String::with_capacity(prefix.len() + item.len());
1686 row.push_str(prefix);
1687 row.push_str(item);
1688 self.styled(row, style);
1689 }
1690 }
1691
1692 pub fn radio(&mut self, state: &mut RadioState) -> Response {
1697 self.radio_colored(state, &WidgetColors::new())
1698 }
1699
1700 pub fn radio_colored(&mut self, state: &mut RadioState, colors: &WidgetColors) -> Response {
1702 if state.items.is_empty() {
1703 return Response::none();
1704 }
1705 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1706 let focused = self.register_focusable();
1707 let old_selected = state.selected;
1708
1709 if focused {
1710 let mut consumed_indices = Vec::new();
1711 for (i, event) in self.events.iter().enumerate() {
1712 if self.consumed[i] {
1713 continue;
1714 }
1715 if let Event::Key(key) = event {
1716 if key.kind != KeyEventKind::Press {
1717 continue;
1718 }
1719 match key.code {
1720 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1721 let _ = handle_vertical_nav(
1722 &mut state.selected,
1723 state.items.len().saturating_sub(1),
1724 key.code.clone(),
1725 );
1726 consumed_indices.push(i);
1727 }
1728 KeyCode::Enter | KeyCode::Char(' ') => {
1729 consumed_indices.push(i);
1730 }
1731 _ => {}
1732 }
1733 }
1734 }
1735 for idx in consumed_indices {
1736 self.consumed[idx] = true;
1737 }
1738 }
1739
1740 let interaction_id = self.next_interaction_id();
1741 let mut response = self.response_for(interaction_id);
1742 response.focused = focused;
1743
1744 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1745 for (i, event) in self.events.iter().enumerate() {
1746 if self.consumed[i] {
1747 continue;
1748 }
1749 if let Event::Mouse(mouse) = event {
1750 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1751 continue;
1752 }
1753 let in_bounds = mouse.x >= rect.x
1754 && mouse.x < rect.right()
1755 && mouse.y >= rect.y
1756 && mouse.y < rect.bottom();
1757 if !in_bounds {
1758 continue;
1759 }
1760 let clicked_idx = (mouse.y - rect.y) as usize;
1761 if clicked_idx < state.items.len() {
1762 state.selected = clicked_idx;
1763 self.consumed[i] = true;
1764 }
1765 }
1766 }
1767 }
1768
1769 self.commands.push(Command::BeginContainer {
1770 direction: Direction::Column,
1771 gap: 0,
1772 align: Align::Start,
1773 align_self: None,
1774 justify: Justify::Start,
1775 border: None,
1776 border_sides: BorderSides::all(),
1777 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1778 bg_color: None,
1779 padding: Padding::default(),
1780 margin: Margin::default(),
1781 constraints: Constraints::default(),
1782 title: None,
1783 grow: 0,
1784 group_name: None,
1785 });
1786
1787 for (idx, item) in state.items.iter().enumerate() {
1788 let is_selected = idx == state.selected;
1789 let marker = if is_selected { "●" } else { "○" };
1790 let style = if is_selected {
1791 if focused {
1792 Style::new()
1793 .bold()
1794 .fg(colors.accent.unwrap_or(self.theme.primary))
1795 } else {
1796 Style::new().fg(colors.accent.unwrap_or(self.theme.primary))
1797 }
1798 } else {
1799 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1800 };
1801 let prefix = if focused && idx == state.selected {
1802 "▸ "
1803 } else {
1804 " "
1805 };
1806 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1807 row.push_str(prefix);
1808 row.push_str(marker);
1809 row.push(' ');
1810 row.push_str(item);
1811 self.styled(row, style);
1812 }
1813
1814 self.commands.push(Command::EndContainer);
1815 self.last_text_idx = None;
1816 response.changed = state.selected != old_selected;
1817 response
1818 }
1819
1820 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> Response {
1824 if state.items.is_empty() {
1825 return Response::none();
1826 }
1827 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
1828 let focused = self.register_focusable();
1829 let old_selected = state.selected.clone();
1830
1831 if focused {
1832 let mut consumed_indices = Vec::new();
1833 for (i, event) in self.events.iter().enumerate() {
1834 if self.consumed[i] {
1835 continue;
1836 }
1837 if let Event::Key(key) = event {
1838 if key.kind != KeyEventKind::Press {
1839 continue;
1840 }
1841 match key.code {
1842 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1843 let _ = handle_vertical_nav(
1844 &mut state.cursor,
1845 state.items.len().saturating_sub(1),
1846 key.code.clone(),
1847 );
1848 consumed_indices.push(i);
1849 }
1850 KeyCode::Char(' ') | KeyCode::Enter => {
1851 state.toggle(state.cursor);
1852 consumed_indices.push(i);
1853 }
1854 _ => {}
1855 }
1856 }
1857 }
1858 for idx in consumed_indices {
1859 self.consumed[idx] = true;
1860 }
1861 }
1862
1863 let interaction_id = self.next_interaction_id();
1864 let mut response = self.response_for(interaction_id);
1865 response.focused = focused;
1866
1867 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1868 for (i, event) in self.events.iter().enumerate() {
1869 if self.consumed[i] {
1870 continue;
1871 }
1872 if let Event::Mouse(mouse) = event {
1873 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1874 continue;
1875 }
1876 let in_bounds = mouse.x >= rect.x
1877 && mouse.x < rect.right()
1878 && mouse.y >= rect.y
1879 && mouse.y < rect.bottom();
1880 if !in_bounds {
1881 continue;
1882 }
1883 let clicked_idx = (mouse.y - rect.y) as usize;
1884 if clicked_idx < state.items.len() {
1885 state.toggle(clicked_idx);
1886 state.cursor = clicked_idx;
1887 self.consumed[i] = true;
1888 }
1889 }
1890 }
1891 }
1892
1893 self.commands.push(Command::BeginContainer {
1894 direction: Direction::Column,
1895 gap: 0,
1896 align: Align::Start,
1897 align_self: None,
1898 justify: Justify::Start,
1899 border: None,
1900 border_sides: BorderSides::all(),
1901 border_style: Style::new().fg(self.theme.border),
1902 bg_color: None,
1903 padding: Padding::default(),
1904 margin: Margin::default(),
1905 constraints: Constraints::default(),
1906 title: None,
1907 grow: 0,
1908 group_name: None,
1909 });
1910
1911 for (idx, item) in state.items.iter().enumerate() {
1912 let checked = state.selected.contains(&idx);
1913 let marker = if checked { "[x]" } else { "[ ]" };
1914 let is_cursor = idx == state.cursor;
1915 let style = if is_cursor && focused {
1916 Style::new().bold().fg(self.theme.primary)
1917 } else if checked {
1918 Style::new().fg(self.theme.success)
1919 } else {
1920 Style::new().fg(self.theme.text)
1921 };
1922 let prefix = if is_cursor && focused { "▸ " } else { " " };
1923 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1924 row.push_str(prefix);
1925 row.push_str(marker);
1926 row.push(' ');
1927 row.push_str(item);
1928 self.styled(row, style);
1929 }
1930
1931 self.commands.push(Command::EndContainer);
1932 self.last_text_idx = None;
1933 response.changed = state.selected != old_selected;
1934 response
1935 }
1936
1937 pub fn rich_log(&mut self, state: &mut RichLogState) -> Response {
1941 let focused = self.register_focusable();
1942 let interaction_id = self.next_interaction_id();
1943 let mut response = self.response_for(interaction_id);
1944 response.focused = focused;
1945
1946 let widget_height = if response.rect.height > 0 {
1947 response.rect.height as usize
1948 } else {
1949 self.area_height as usize
1950 };
1951 let viewport_height = widget_height.saturating_sub(2);
1952 let effective_height = if viewport_height == 0 {
1953 state.entries.len().max(1)
1954 } else {
1955 viewport_height
1956 };
1957 let show_indicator = state.entries.len() > effective_height;
1958 let visible_rows = if show_indicator {
1959 effective_height.saturating_sub(1).max(1)
1960 } else {
1961 effective_height
1962 };
1963 let max_offset = state.entries.len().saturating_sub(visible_rows);
1964 if state.auto_scroll && state.scroll_offset == usize::MAX {
1965 state.scroll_offset = max_offset;
1966 } else {
1967 state.scroll_offset = state.scroll_offset.min(max_offset);
1968 }
1969 let old_offset = state.scroll_offset;
1970
1971 if focused {
1972 let mut consumed_indices = Vec::new();
1973 for (i, event) in self.events.iter().enumerate() {
1974 if self.consumed[i] {
1975 continue;
1976 }
1977 if let Event::Key(key) = event {
1978 if key.kind != KeyEventKind::Press {
1979 continue;
1980 }
1981 match key.code {
1982 KeyCode::Up | KeyCode::Char('k') => {
1983 state.scroll_offset = state.scroll_offset.saturating_sub(1);
1984 consumed_indices.push(i);
1985 }
1986 KeyCode::Down | KeyCode::Char('j') => {
1987 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
1988 consumed_indices.push(i);
1989 }
1990 KeyCode::PageUp => {
1991 state.scroll_offset = state.scroll_offset.saturating_sub(10);
1992 consumed_indices.push(i);
1993 }
1994 KeyCode::PageDown => {
1995 state.scroll_offset = (state.scroll_offset + 10).min(max_offset);
1996 consumed_indices.push(i);
1997 }
1998 KeyCode::Home => {
1999 state.scroll_offset = 0;
2000 consumed_indices.push(i);
2001 }
2002 KeyCode::End => {
2003 state.scroll_offset = max_offset;
2004 consumed_indices.push(i);
2005 }
2006 _ => {}
2007 }
2008 }
2009 }
2010 for idx in consumed_indices {
2011 self.consumed[idx] = true;
2012 }
2013 }
2014
2015 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
2016 for (i, event) in self.events.iter().enumerate() {
2017 if self.consumed[i] {
2018 continue;
2019 }
2020 if let Event::Mouse(mouse) = event {
2021 let in_bounds = mouse.x >= rect.x
2022 && mouse.x < rect.right()
2023 && mouse.y >= rect.y
2024 && mouse.y < rect.bottom();
2025 if !in_bounds {
2026 continue;
2027 }
2028 let delta = self.scroll_lines_per_event as usize;
2029 match mouse.kind {
2030 MouseKind::ScrollUp => {
2031 state.scroll_offset = state.scroll_offset.saturating_sub(delta);
2032 self.consumed[i] = true;
2033 }
2034 MouseKind::ScrollDown => {
2035 state.scroll_offset = (state.scroll_offset + delta).min(max_offset);
2036 self.consumed[i] = true;
2037 }
2038 _ => {}
2039 }
2040 }
2041 }
2042 }
2043
2044 state.scroll_offset = state.scroll_offset.min(max_offset);
2045 let start = state
2046 .scroll_offset
2047 .min(state.entries.len().saturating_sub(visible_rows));
2048 let end = (start + visible_rows).min(state.entries.len());
2049
2050 self.commands.push(Command::BeginContainer {
2051 direction: Direction::Column,
2052 gap: 0,
2053 align: Align::Start,
2054 align_self: None,
2055 justify: Justify::Start,
2056 border: Some(Border::Single),
2057 border_sides: BorderSides::all(),
2058 border_style: Style::new().fg(self.theme.border),
2059 bg_color: None,
2060 padding: Padding::default(),
2061 margin: Margin::default(),
2062 constraints: Constraints::default(),
2063 title: None,
2064 grow: 0,
2065 group_name: None,
2066 });
2067
2068 for entry in state
2069 .entries
2070 .iter()
2071 .skip(start)
2072 .take(end.saturating_sub(start))
2073 {
2074 self.commands.push(Command::RichText {
2075 segments: entry.segments.clone(),
2076 wrap: false,
2077 align: Align::Start,
2078 margin: Margin::default(),
2079 constraints: Constraints::default(),
2080 });
2081 }
2082
2083 if show_indicator {
2084 let end_pos = end.min(state.entries.len());
2085 let line = format!(
2086 "{}-{} / {}",
2087 start.saturating_add(1),
2088 end_pos,
2089 state.entries.len()
2090 );
2091 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
2092 }
2093
2094 self.commands.push(Command::EndContainer);
2095 self.last_text_idx = None;
2096 response.changed = state.scroll_offset != old_offset;
2097 response
2098 }
2099
2100 pub fn tree(&mut self, state: &mut TreeState) -> Response {
2102 let entries = state.flatten();
2103 if entries.is_empty() {
2104 return Response::none();
2105 }
2106 state.selected = state.selected.min(entries.len().saturating_sub(1));
2107 let old_selected = state.selected;
2108 let focused = self.register_focusable();
2109 let interaction_id = self.next_interaction_id();
2110 let mut response = self.response_for(interaction_id);
2111 response.focused = focused;
2112 let mut changed = false;
2113
2114 if focused {
2115 let mut consumed_indices = Vec::new();
2116 for (i, event) in self.events.iter().enumerate() {
2117 if self.consumed[i] {
2118 continue;
2119 }
2120 if let Event::Key(key) = event {
2121 if key.kind != KeyEventKind::Press {
2122 continue;
2123 }
2124 match key.code {
2125 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2126 let max_index = state.flatten().len().saturating_sub(1);
2127 let _ = handle_vertical_nav(
2128 &mut state.selected,
2129 max_index,
2130 key.code.clone(),
2131 );
2132 changed = changed || state.selected != old_selected;
2133 consumed_indices.push(i);
2134 }
2135 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
2136 state.toggle_at(state.selected);
2137 changed = true;
2138 consumed_indices.push(i);
2139 }
2140 KeyCode::Left => {
2141 let entry = &entries[state.selected.min(entries.len() - 1)];
2142 if entry.expanded {
2143 state.toggle_at(state.selected);
2144 changed = true;
2145 }
2146 consumed_indices.push(i);
2147 }
2148 _ => {}
2149 }
2150 }
2151 }
2152 for idx in consumed_indices {
2153 self.consumed[idx] = true;
2154 }
2155 }
2156
2157 self.commands.push(Command::BeginContainer {
2158 direction: Direction::Column,
2159 gap: 0,
2160 align: Align::Start,
2161 align_self: None,
2162 justify: Justify::Start,
2163 border: None,
2164 border_sides: BorderSides::all(),
2165 border_style: Style::new().fg(self.theme.border),
2166 bg_color: None,
2167 padding: Padding::default(),
2168 margin: Margin::default(),
2169 constraints: Constraints::default(),
2170 title: None,
2171 grow: 0,
2172 group_name: None,
2173 });
2174
2175 let entries = state.flatten();
2176 for (idx, entry) in entries.iter().enumerate() {
2177 let indent = " ".repeat(entry.depth);
2178 let icon = if entry.is_leaf {
2179 " "
2180 } else if entry.expanded {
2181 "▾ "
2182 } else {
2183 "▸ "
2184 };
2185 let is_selected = idx == state.selected;
2186 let style = if is_selected && focused {
2187 Style::new().bold().fg(self.theme.primary)
2188 } else if is_selected {
2189 Style::new().fg(self.theme.primary)
2190 } else {
2191 Style::new().fg(self.theme.text)
2192 };
2193 let cursor = if is_selected && focused { "▸" } else { " " };
2194 let mut row =
2195 String::with_capacity(cursor.len() + indent.len() + icon.len() + entry.label.len());
2196 row.push_str(cursor);
2197 row.push_str(&indent);
2198 row.push_str(icon);
2199 row.push_str(&entry.label);
2200 self.styled(row, style);
2201 }
2202
2203 self.commands.push(Command::EndContainer);
2204 self.last_text_idx = None;
2205 response.changed = changed || state.selected != old_selected;
2206 response
2207 }
2208
2209 pub fn directory_tree(&mut self, state: &mut DirectoryTreeState) -> Response {
2211 let entries = state.tree.flatten();
2212 if entries.is_empty() {
2213 return Response::none();
2214 }
2215 state.tree.selected = state.tree.selected.min(entries.len().saturating_sub(1));
2216 let old_selected = state.tree.selected;
2217 let focused = self.register_focusable();
2218 let interaction_id = self.next_interaction_id();
2219 let mut response = self.response_for(interaction_id);
2220 response.focused = focused;
2221 let mut changed = false;
2222
2223 if focused {
2224 let mut consumed_indices = Vec::new();
2225 for (i, event) in self.events.iter().enumerate() {
2226 if self.consumed[i] {
2227 continue;
2228 }
2229 if let Event::Key(key) = event {
2230 if key.kind != KeyEventKind::Press {
2231 continue;
2232 }
2233 match key.code {
2234 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2235 let max_index = state.tree.flatten().len().saturating_sub(1);
2236 let _ = handle_vertical_nav(
2237 &mut state.tree.selected,
2238 max_index,
2239 key.code.clone(),
2240 );
2241 changed = changed || state.tree.selected != old_selected;
2242 consumed_indices.push(i);
2243 }
2244 KeyCode::Right => {
2245 let current_entries = state.tree.flatten();
2246 let entry = ¤t_entries
2247 [state.tree.selected.min(current_entries.len() - 1)];
2248 if !entry.is_leaf && !entry.expanded {
2249 state.tree.toggle_at(state.tree.selected);
2250 changed = true;
2251 }
2252 consumed_indices.push(i);
2253 }
2254 KeyCode::Enter | KeyCode::Char(' ') => {
2255 state.tree.toggle_at(state.tree.selected);
2256 changed = true;
2257 consumed_indices.push(i);
2258 }
2259 KeyCode::Left => {
2260 let current_entries = state.tree.flatten();
2261 let entry = ¤t_entries
2262 [state.tree.selected.min(current_entries.len() - 1)];
2263 if entry.expanded {
2264 state.tree.toggle_at(state.tree.selected);
2265 changed = true;
2266 }
2267 consumed_indices.push(i);
2268 }
2269 _ => {}
2270 }
2271 }
2272 }
2273 for idx in consumed_indices {
2274 self.consumed[idx] = true;
2275 }
2276 }
2277
2278 self.commands.push(Command::BeginContainer {
2279 direction: Direction::Column,
2280 gap: 0,
2281 align: Align::Start,
2282 align_self: None,
2283 justify: Justify::Start,
2284 border: None,
2285 border_sides: BorderSides::all(),
2286 border_style: Style::new().fg(self.theme.border),
2287 bg_color: None,
2288 padding: Padding::default(),
2289 margin: Margin::default(),
2290 constraints: Constraints::default(),
2291 title: None,
2292 grow: 0,
2293 group_name: None,
2294 });
2295
2296 let mut rows = Vec::new();
2297 flatten_directory_rows(&state.tree.nodes, Vec::new(), &mut rows);
2298 for (idx, row_entry) in rows.iter().enumerate() {
2299 let mut row = String::new();
2300 let cursor = if idx == state.tree.selected && focused {
2301 "▸"
2302 } else {
2303 " "
2304 };
2305 row.push_str(cursor);
2306 row.push(' ');
2307
2308 if row_entry.depth > 0 {
2309 for has_more in &row_entry.branch_mask {
2310 if *has_more {
2311 row.push_str("│ ");
2312 } else {
2313 row.push_str(" ");
2314 }
2315 }
2316 if row_entry.is_last {
2317 row.push_str("└── ");
2318 } else {
2319 row.push_str("├── ");
2320 }
2321 }
2322
2323 let icon = if row_entry.is_leaf {
2324 " "
2325 } else if row_entry.expanded {
2326 "▾ "
2327 } else {
2328 "▸ "
2329 };
2330 if state.show_icons {
2331 row.push_str(icon);
2332 }
2333 row.push_str(&row_entry.label);
2334
2335 let style = if idx == state.tree.selected && focused {
2336 Style::new().bold().fg(self.theme.primary)
2337 } else if idx == state.tree.selected {
2338 Style::new().fg(self.theme.primary)
2339 } else {
2340 Style::new().fg(self.theme.text)
2341 };
2342 self.styled(row, style);
2343 }
2344
2345 self.commands.push(Command::EndContainer);
2346 self.last_text_idx = None;
2347 response.changed = changed || state.tree.selected != old_selected;
2348 response
2349 }
2350
2351 pub fn virtual_list(
2358 &mut self,
2359 state: &mut ListState,
2360 visible_height: usize,
2361 f: impl Fn(&mut Context, usize),
2362 ) -> Response {
2363 if state.items.is_empty() {
2364 return Response::none();
2365 }
2366 state.selected = state.selected.min(state.items.len().saturating_sub(1));
2367 let interaction_id = self.next_interaction_id();
2368 let focused = self.register_focusable();
2369 let old_selected = state.selected;
2370
2371 if focused {
2372 let mut consumed_indices = Vec::new();
2373 for (i, event) in self.events.iter().enumerate() {
2374 if self.consumed[i] {
2375 continue;
2376 }
2377 if let Event::Key(key) = event {
2378 if key.kind != KeyEventKind::Press {
2379 continue;
2380 }
2381 match key.code {
2382 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2383 let _ = handle_vertical_nav(
2384 &mut state.selected,
2385 state.items.len().saturating_sub(1),
2386 key.code.clone(),
2387 );
2388 consumed_indices.push(i);
2389 }
2390 KeyCode::PageUp => {
2391 state.selected = state.selected.saturating_sub(visible_height);
2392 consumed_indices.push(i);
2393 }
2394 KeyCode::PageDown => {
2395 state.selected = (state.selected + visible_height)
2396 .min(state.items.len().saturating_sub(1));
2397 consumed_indices.push(i);
2398 }
2399 KeyCode::Home => {
2400 state.selected = 0;
2401 consumed_indices.push(i);
2402 }
2403 KeyCode::End => {
2404 state.selected = state.items.len().saturating_sub(1);
2405 consumed_indices.push(i);
2406 }
2407 _ => {}
2408 }
2409 }
2410 }
2411 for idx in consumed_indices {
2412 self.consumed[idx] = true;
2413 }
2414 }
2415
2416 let start = if state.selected >= visible_height {
2417 state.selected - visible_height + 1
2418 } else {
2419 0
2420 };
2421 let end = (start + visible_height).min(state.items.len());
2422
2423 self.commands.push(Command::BeginContainer {
2424 direction: Direction::Column,
2425 gap: 0,
2426 align: Align::Start,
2427 align_self: None,
2428 justify: Justify::Start,
2429 border: None,
2430 border_sides: BorderSides::all(),
2431 border_style: Style::new().fg(self.theme.border),
2432 bg_color: None,
2433 padding: Padding::default(),
2434 margin: Margin::default(),
2435 constraints: Constraints::default(),
2436 title: None,
2437 grow: 0,
2438 group_name: None,
2439 });
2440
2441 if start > 0 {
2442 let hidden = start.to_string();
2443 let mut line = String::with_capacity(hidden.len() + 10);
2444 line.push_str(" ↑ ");
2445 line.push_str(&hidden);
2446 line.push_str(" more");
2447 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2448 }
2449
2450 for idx in start..end {
2451 f(self, idx);
2452 }
2453
2454 let remaining = state.items.len().saturating_sub(end);
2455 if remaining > 0 {
2456 let hidden = remaining.to_string();
2457 let mut line = String::with_capacity(hidden.len() + 10);
2458 line.push_str(" ↓ ");
2459 line.push_str(&hidden);
2460 line.push_str(" more");
2461 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2462 }
2463
2464 self.commands.push(Command::EndContainer);
2465 self.last_text_idx = None;
2466 let mut response = self.response_for(interaction_id);
2467 response.focused = focused;
2468 response.changed = state.selected != old_selected;
2469 response
2470 }
2471
2472 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
2476 if !state.open {
2477 return Response::none();
2478 }
2479
2480 state.last_selected = None;
2481 let interaction_id = self.next_interaction_id();
2482
2483 let filtered = state.filtered_indices();
2484 let sel = state.selected().min(filtered.len().saturating_sub(1));
2485 state.set_selected(sel);
2486
2487 let mut consumed_indices = Vec::new();
2488
2489 for (i, event) in self.events.iter().enumerate() {
2490 if self.consumed[i] {
2491 continue;
2492 }
2493 if let Event::Key(key) = event {
2494 if key.kind != KeyEventKind::Press {
2495 continue;
2496 }
2497 match key.code {
2498 KeyCode::Esc => {
2499 state.open = false;
2500 consumed_indices.push(i);
2501 }
2502 KeyCode::Up => {
2503 let s = state.selected();
2504 state.set_selected(s.saturating_sub(1));
2505 consumed_indices.push(i);
2506 }
2507 KeyCode::Down => {
2508 let s = state.selected();
2509 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
2510 consumed_indices.push(i);
2511 }
2512 KeyCode::Enter => {
2513 if let Some(&cmd_idx) = filtered.get(state.selected()) {
2514 state.last_selected = Some(cmd_idx);
2515 state.open = false;
2516 }
2517 consumed_indices.push(i);
2518 }
2519 KeyCode::Backspace => {
2520 if state.cursor > 0 {
2521 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
2522 let end_idx = byte_index_for_char(&state.input, state.cursor);
2523 state.input.replace_range(byte_idx..end_idx, "");
2524 state.cursor -= 1;
2525 state.set_selected(0);
2526 }
2527 consumed_indices.push(i);
2528 }
2529 KeyCode::Char(ch) => {
2530 let byte_idx = byte_index_for_char(&state.input, state.cursor);
2531 state.input.insert(byte_idx, ch);
2532 state.cursor += 1;
2533 state.set_selected(0);
2534 consumed_indices.push(i);
2535 }
2536 _ => {}
2537 }
2538 }
2539 }
2540 for idx in consumed_indices {
2541 self.consumed[idx] = true;
2542 }
2543
2544 let filtered = state.filtered_indices();
2545
2546 let _ = self.modal(|ui| {
2547 let primary = ui.theme.primary;
2548 let _ = ui
2549 .container()
2550 .border(Border::Rounded)
2551 .border_style(Style::new().fg(primary))
2552 .pad(1)
2553 .max_w(60)
2554 .col(|ui| {
2555 let border_color = ui.theme.primary;
2556 let _ = ui
2557 .bordered(Border::Rounded)
2558 .border_style(Style::new().fg(border_color))
2559 .px(1)
2560 .col(|ui| {
2561 let display = if state.input.is_empty() {
2562 "Type to search...".to_string()
2563 } else {
2564 state.input.clone()
2565 };
2566 let style = if state.input.is_empty() {
2567 Style::new().dim().fg(ui.theme.text_dim)
2568 } else {
2569 Style::new().fg(ui.theme.text)
2570 };
2571 ui.styled(display, style);
2572 });
2573
2574 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
2575 let cmd = &state.commands[cmd_idx];
2576 let is_selected = list_idx == state.selected();
2577 let style = if is_selected {
2578 Style::new().bold().fg(ui.theme.primary)
2579 } else {
2580 Style::new().fg(ui.theme.text)
2581 };
2582 let prefix = if is_selected { "▸ " } else { " " };
2583 let shortcut_text = cmd
2584 .shortcut
2585 .as_deref()
2586 .map(|s| {
2587 let mut text = String::with_capacity(s.len() + 4);
2588 text.push_str(" (");
2589 text.push_str(s);
2590 text.push(')');
2591 text
2592 })
2593 .unwrap_or_default();
2594 let mut line = String::with_capacity(
2595 prefix.len() + cmd.label.len() + shortcut_text.len(),
2596 );
2597 line.push_str(prefix);
2598 line.push_str(&cmd.label);
2599 line.push_str(&shortcut_text);
2600 ui.styled(line, style);
2601 if is_selected && !cmd.description.is_empty() {
2602 let mut desc = String::with_capacity(4 + cmd.description.len());
2603 desc.push_str(" ");
2604 desc.push_str(&cmd.description);
2605 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
2606 }
2607 }
2608
2609 if filtered.is_empty() {
2610 ui.styled(
2611 " No matching commands",
2612 Style::new().dim().fg(ui.theme.text_dim),
2613 );
2614 }
2615 });
2616 });
2617
2618 let mut response = self.response_for(interaction_id);
2619 response.changed = state.last_selected.is_some();
2620 response
2621 }
2622
2623 pub fn markdown(&mut self, text: &str) -> Response {
2633 self.commands.push(Command::BeginContainer {
2634 direction: Direction::Column,
2635 gap: 0,
2636 align: Align::Start,
2637 align_self: None,
2638 justify: Justify::Start,
2639 border: None,
2640 border_sides: BorderSides::all(),
2641 border_style: Style::new().fg(self.theme.border),
2642 bg_color: None,
2643 padding: Padding::default(),
2644 margin: Margin::default(),
2645 constraints: Constraints::default(),
2646 title: None,
2647 grow: 0,
2648 group_name: None,
2649 });
2650 self.interaction_count += 1;
2651
2652 let text_style = Style::new().fg(self.theme.text);
2653 let bold_style = Style::new().fg(self.theme.text).bold();
2654 let code_style = Style::new().fg(self.theme.accent);
2655 let border_style = Style::new().fg(self.theme.border).dim();
2656
2657 let mut in_code_block = false;
2658 let mut code_block_lang = String::new();
2659 let mut code_block_lines: Vec<String> = Vec::new();
2660 let mut table_lines: Vec<String> = Vec::new();
2661
2662 for line in text.lines() {
2663 let trimmed = line.trim();
2664
2665 if in_code_block {
2666 if trimmed.starts_with("```") {
2667 in_code_block = false;
2668 let code_content = code_block_lines.join("\n");
2669 let theme = self.theme;
2670 let highlighted: Option<Vec<Vec<(String, Style)>>> =
2671 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
2672 let _ = self.container().bg(theme.surface).p(1).col(|ui| {
2673 if let Some(ref hl_lines) = highlighted {
2674 for segs in hl_lines {
2675 if segs.is_empty() {
2676 ui.text(" ");
2677 } else {
2678 ui.line(|ui| {
2679 for (t, s) in segs {
2680 ui.styled(t, *s);
2681 }
2682 });
2683 }
2684 }
2685 } else {
2686 for cl in &code_block_lines {
2687 ui.styled(cl, code_style);
2688 }
2689 }
2690 });
2691 code_block_lang.clear();
2692 code_block_lines.clear();
2693 } else {
2694 code_block_lines.push(line.to_string());
2695 }
2696 continue;
2697 }
2698
2699 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
2701 table_lines.push(trimmed.to_string());
2702 continue;
2703 }
2704 if !table_lines.is_empty() {
2706 self.render_markdown_table(&table_lines, text_style, bold_style, border_style);
2707 table_lines.clear();
2708 }
2709
2710 if trimmed.is_empty() {
2711 self.text(" ");
2712 continue;
2713 }
2714 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
2715 self.styled("─".repeat(40), border_style);
2716 continue;
2717 }
2718 if let Some(quote) = trimmed.strip_prefix("> ") {
2719 let quote_style = Style::new().fg(self.theme.text_dim).italic();
2720 let bar_style = Style::new().fg(self.theme.border);
2721 self.line(|ui| {
2722 ui.styled("│ ", bar_style);
2723 ui.styled(quote, quote_style);
2724 });
2725 } else if let Some(heading) = trimmed.strip_prefix("### ") {
2726 self.styled(heading, Style::new().bold().fg(self.theme.accent));
2727 } else if let Some(heading) = trimmed.strip_prefix("## ") {
2728 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
2729 } else if let Some(heading) = trimmed.strip_prefix("# ") {
2730 self.styled(heading, Style::new().bold().fg(self.theme.primary));
2731 } else if let Some(item) = trimmed
2732 .strip_prefix("- ")
2733 .or_else(|| trimmed.strip_prefix("* "))
2734 {
2735 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
2736 if segs.len() <= 1 {
2737 let mut line = String::with_capacity(4 + item.len());
2738 line.push_str(" • ");
2739 line.push_str(item);
2740 self.styled(line, text_style);
2741 } else {
2742 self.line(|ui| {
2743 ui.styled(" • ", text_style);
2744 for (s, st) in segs {
2745 ui.styled(s, st);
2746 }
2747 });
2748 }
2749 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
2750 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
2751 if parts.len() == 2 {
2752 let segs =
2753 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
2754 if segs.len() <= 1 {
2755 let mut line = String::with_capacity(4 + parts[0].len() + parts[1].len());
2756 line.push_str(" ");
2757 line.push_str(parts[0]);
2758 line.push_str(". ");
2759 line.push_str(parts[1]);
2760 self.styled(line, text_style);
2761 } else {
2762 self.line(|ui| {
2763 let mut prefix = String::with_capacity(4 + parts[0].len());
2764 prefix.push_str(" ");
2765 prefix.push_str(parts[0]);
2766 prefix.push_str(". ");
2767 ui.styled(prefix, text_style);
2768 for (s, st) in segs {
2769 ui.styled(s, st);
2770 }
2771 });
2772 }
2773 } else {
2774 self.text(trimmed);
2775 }
2776 } else if let Some(lang) = trimmed.strip_prefix("```") {
2777 in_code_block = true;
2778 code_block_lang = lang.trim().to_string();
2779 } else {
2780 self.render_md_inline(trimmed, text_style, bold_style, code_style);
2781 }
2782 }
2783
2784 if in_code_block && !code_block_lines.is_empty() {
2785 for cl in &code_block_lines {
2786 self.styled(cl, code_style);
2787 }
2788 }
2789
2790 if !table_lines.is_empty() {
2792 self.render_markdown_table(&table_lines, text_style, bold_style, border_style);
2793 }
2794
2795 self.commands.push(Command::EndContainer);
2796 self.last_text_idx = None;
2797 Response::none()
2798 }
2799
2800 fn render_markdown_table(
2802 &mut self,
2803 lines: &[String],
2804 text_style: Style,
2805 bold_style: Style,
2806 border_style: Style,
2807 ) {
2808 if lines.is_empty() {
2809 return;
2810 }
2811
2812 let is_separator = |line: &str| -> bool {
2814 let inner = line.trim_matches('|').trim();
2815 !inner.is_empty()
2816 && inner
2817 .chars()
2818 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
2819 };
2820
2821 let parse_row = |line: &str| -> Vec<String> {
2822 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
2823 trimmed.split('|').map(|c| c.trim().to_string()).collect()
2824 };
2825
2826 let mut header: Option<Vec<String>> = None;
2827 let mut data_rows: Vec<Vec<String>> = Vec::new();
2828 let mut found_separator = false;
2829
2830 for (i, line) in lines.iter().enumerate() {
2831 if is_separator(line) {
2832 found_separator = true;
2833 continue;
2834 }
2835 if i == 0 && !found_separator {
2836 header = Some(parse_row(line));
2837 } else {
2838 data_rows.push(parse_row(line));
2839 }
2840 }
2841
2842 if !found_separator && header.is_none() && !data_rows.is_empty() {
2844 header = Some(data_rows.remove(0));
2845 }
2846
2847 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
2849 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
2850 if col_count == 0 {
2851 return;
2852 }
2853 let mut col_widths = vec![0usize; col_count];
2854 let stripped_rows: Vec<Vec<String>> = all_rows
2856 .iter()
2857 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
2858 .collect();
2859 for row in &stripped_rows {
2860 for (i, cell) in row.iter().enumerate() {
2861 if i < col_count {
2862 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
2863 }
2864 }
2865 }
2866
2867 let mut top = String::from("┌");
2869 for (i, &w) in col_widths.iter().enumerate() {
2870 for _ in 0..w + 2 {
2871 top.push('─');
2872 }
2873 top.push(if i < col_count - 1 { '┬' } else { '┐' });
2874 }
2875 self.styled(&top, border_style);
2876
2877 if let Some(ref hdr) = header {
2879 let mut s = String::from("│");
2880 for (i, w) in col_widths.iter().enumerate() {
2881 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
2882 let display = Self::md_strip(raw);
2883 let cell_w = UnicodeWidthStr::width(display.as_str());
2884 s.push(' ');
2885 s.push_str(&display);
2886 for _ in cell_w..*w {
2887 s.push(' ');
2888 }
2889 s.push_str(" │");
2890 }
2891 self.styled(&s, bold_style);
2892
2893 let mut sep = String::from("├");
2895 for (i, &w) in col_widths.iter().enumerate() {
2896 for _ in 0..w + 2 {
2897 sep.push('─');
2898 }
2899 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
2900 }
2901 self.styled(&sep, border_style);
2902 }
2903
2904 for row in &data_rows {
2906 let mut s = String::from("│");
2907 for (i, w) in col_widths.iter().enumerate() {
2908 let raw = row.get(i).map(String::as_str).unwrap_or("");
2909 let display = Self::md_strip(raw);
2910 let cell_w = UnicodeWidthStr::width(display.as_str());
2911 s.push(' ');
2912 s.push_str(&display);
2913 for _ in cell_w..*w {
2914 s.push(' ');
2915 }
2916 s.push_str(" │");
2917 }
2918 self.styled(&s, text_style);
2919 }
2920
2921 let mut bot = String::from("└");
2923 for (i, &w) in col_widths.iter().enumerate() {
2924 for _ in 0..w + 2 {
2925 bot.push('─');
2926 }
2927 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
2928 }
2929 self.styled(&bot, border_style);
2930 }
2931
2932 pub(crate) fn parse_inline_segments(
2933 text: &str,
2934 base: Style,
2935 bold: Style,
2936 code: Style,
2937 ) -> Vec<(String, Style)> {
2938 let mut segments: Vec<(String, Style)> = Vec::new();
2939 let mut current = String::new();
2940 let chars: Vec<char> = text.chars().collect();
2941 let mut i = 0;
2942 while i < chars.len() {
2943 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
2944 let rest: String = chars[i + 2..].iter().collect();
2945 if let Some(end) = rest.find("**") {
2946 if !current.is_empty() {
2947 segments.push((std::mem::take(&mut current), base));
2948 }
2949 let inner: String = rest[..end].to_string();
2950 let char_count = inner.chars().count();
2951 segments.push((inner, bold));
2952 i += 2 + char_count + 2;
2953 continue;
2954 }
2955 }
2956 if chars[i] == '*'
2957 && (i + 1 >= chars.len() || chars[i + 1] != '*')
2958 && (i == 0 || chars[i - 1] != '*')
2959 {
2960 let rest: String = chars[i + 1..].iter().collect();
2961 if let Some(end) = rest.find('*') {
2962 if !current.is_empty() {
2963 segments.push((std::mem::take(&mut current), base));
2964 }
2965 let inner: String = rest[..end].to_string();
2966 let char_count = inner.chars().count();
2967 segments.push((inner, base.italic()));
2968 i += 1 + char_count + 1;
2969 continue;
2970 }
2971 }
2972 if chars[i] == '`' {
2973 let rest: String = chars[i + 1..].iter().collect();
2974 if let Some(end) = rest.find('`') {
2975 if !current.is_empty() {
2976 segments.push((std::mem::take(&mut current), base));
2977 }
2978 let inner: String = rest[..end].to_string();
2979 let char_count = inner.chars().count();
2980 segments.push((inner, code));
2981 i += 1 + char_count + 1;
2982 continue;
2983 }
2984 }
2985 current.push(chars[i]);
2986 i += 1;
2987 }
2988 if !current.is_empty() {
2989 segments.push((current, base));
2990 }
2991 segments
2992 }
2993
2994 fn render_md_inline(
2999 &mut self,
3000 text: &str,
3001 text_style: Style,
3002 bold_style: Style,
3003 code_style: Style,
3004 ) {
3005 let items = Self::split_md_links(text);
3006
3007 if items.len() == 1 {
3009 if let MdInline::Text(ref t) = items[0] {
3010 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
3011 if segs.len() <= 1 {
3012 self.text_wrap(text)
3013 .fg(text_style.fg.unwrap_or(Color::Reset));
3014 } else {
3015 self.line_wrap(|ui| {
3016 for (s, st) in segs {
3017 ui.styled(s, st);
3018 }
3019 });
3020 }
3021 return;
3022 }
3023 }
3024
3025 let has_link = items
3028 .iter()
3029 .any(|i| matches!(i, MdInline::Link { .. } | MdInline::Image { .. }));
3030 let render_items = |ui: &mut Context| {
3031 for item in &items {
3032 match item {
3033 MdInline::Text(t) => {
3034 let segs =
3035 Self::parse_inline_segments(t, text_style, bold_style, code_style);
3036 for (s, st) in segs {
3037 ui.styled(s, st);
3038 }
3039 }
3040 MdInline::Link { text, url } => {
3041 ui.link(text.clone(), url.clone());
3042 }
3043 MdInline::Image { alt, url } => {
3044 let label = format!("[Image: {alt}]");
3045 ui.styled(label, code_style);
3046 if !url.is_empty() {
3047 ui.styled(format!(" ({url})"), text_style.dim());
3048 }
3049 }
3050 }
3051 }
3052 };
3053 if has_link {
3054 self.line(render_items);
3055 } else {
3056 self.line_wrap(render_items);
3057 }
3058 }
3059
3060 fn split_md_links(text: &str) -> Vec<MdInline> {
3062 let chars: Vec<char> = text.chars().collect();
3063 let mut items: Vec<MdInline> = Vec::new();
3064 let mut current = String::new();
3065 let mut i = 0;
3066
3067 while i < chars.len() {
3068 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
3070 if let Some((alt, url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
3071 if !current.is_empty() {
3072 items.push(MdInline::Text(std::mem::take(&mut current)));
3073 }
3074 items.push(MdInline::Image { alt, url });
3075 i += 1 + consumed;
3076 continue;
3077 }
3078 }
3079 if chars[i] == '[' {
3081 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
3082 if !current.is_empty() {
3083 items.push(MdInline::Text(std::mem::take(&mut current)));
3084 }
3085 items.push(MdInline::Link {
3086 text: link_text,
3087 url,
3088 });
3089 i += consumed;
3090 continue;
3091 }
3092 }
3093 current.push(chars[i]);
3094 i += 1;
3095 }
3096 if !current.is_empty() {
3097 items.push(MdInline::Text(current));
3098 }
3099 if items.is_empty() {
3100 items.push(MdInline::Text(String::new()));
3101 }
3102 items
3103 }
3104
3105 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
3108 if start >= chars.len() || chars[start] != '[' {
3109 return None;
3110 }
3111 let mut depth = 0i32;
3113 let mut bracket_end = None;
3114 for (j, &ch) in chars.iter().enumerate().skip(start) {
3115 if ch == '[' {
3116 depth += 1;
3117 } else if ch == ']' {
3118 depth -= 1;
3119 if depth == 0 {
3120 bracket_end = Some(j);
3121 break;
3122 }
3123 }
3124 }
3125 let bracket_end = bracket_end?;
3126 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
3128 return None;
3129 }
3130 let paren_start = bracket_end + 2;
3132 let mut paren_end = None;
3133 let mut paren_depth = 1i32;
3134 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
3135 if ch == '(' {
3136 paren_depth += 1;
3137 } else if ch == ')' {
3138 paren_depth -= 1;
3139 if paren_depth == 0 {
3140 paren_end = Some(j);
3141 break;
3142 }
3143 }
3144 }
3145 let paren_end = paren_end?;
3146 let text: String = chars[start + 1..bracket_end].iter().collect();
3147 let url: String = chars[paren_start..paren_end].iter().collect();
3148 let consumed = paren_end - start + 1;
3149 Some((text, url, consumed))
3150 }
3151
3152 fn md_strip(text: &str) -> String {
3157 let mut result = String::with_capacity(text.len());
3158 let chars: Vec<char> = text.chars().collect();
3159 let mut i = 0;
3160 while i < chars.len() {
3161 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
3163 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
3164 result.push_str(&alt);
3165 i += 1 + consumed;
3166 continue;
3167 }
3168 }
3169 if chars[i] == '[' {
3171 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
3172 result.push_str(&link_text);
3173 i += consumed;
3174 continue;
3175 }
3176 }
3177 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
3179 let rest: String = chars[i + 2..].iter().collect();
3180 if let Some(end) = rest.find("**") {
3181 let inner = &rest[..end];
3182 result.push_str(inner);
3183 i += 2 + inner.chars().count() + 2;
3184 continue;
3185 }
3186 }
3187 if chars[i] == '*'
3189 && (i + 1 >= chars.len() || chars[i + 1] != '*')
3190 && (i == 0 || chars[i - 1] != '*')
3191 {
3192 let rest: String = chars[i + 1..].iter().collect();
3193 if let Some(end) = rest.find('*') {
3194 let inner = &rest[..end];
3195 result.push_str(inner);
3196 i += 1 + inner.chars().count() + 1;
3197 continue;
3198 }
3199 }
3200 if chars[i] == '`' {
3202 let rest: String = chars[i + 1..].iter().collect();
3203 if let Some(end) = rest.find('`') {
3204 let inner = &rest[..end];
3205 result.push_str(inner);
3206 i += 1 + inner.chars().count() + 1;
3207 continue;
3208 }
3209 }
3210 result.push(chars[i]);
3211 i += 1;
3212 }
3213 result
3214 }
3215
3216 pub fn key_seq(&self, seq: &str) -> bool {
3223 if seq.is_empty() {
3224 return false;
3225 }
3226 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3227 return false;
3228 }
3229 let target: Vec<char> = seq.chars().collect();
3230 let mut matched = 0;
3231 for (i, event) in self.events.iter().enumerate() {
3232 if self.consumed[i] {
3233 continue;
3234 }
3235 if let Event::Key(key) = event {
3236 if key.kind != KeyEventKind::Press {
3237 continue;
3238 }
3239 if let KeyCode::Char(c) = key.code {
3240 if c == target[matched] {
3241 matched += 1;
3242 if matched == target.len() {
3243 return true;
3244 }
3245 } else {
3246 matched = 0;
3247 if c == target[0] {
3248 matched = 1;
3249 }
3250 }
3251 }
3252 }
3253 }
3254 false
3255 }
3256
3257 pub fn separator(&mut self) -> &mut Self {
3262 self.commands.push(Command::Text {
3263 content: "─".repeat(200),
3264 style: Style::new().fg(self.theme.border).dim(),
3265 grow: 0,
3266 align: Align::Start,
3267 wrap: false,
3268 truncate: false,
3269 margin: Margin::default(),
3270 constraints: Constraints::default(),
3271 });
3272 self.last_text_idx = Some(self.commands.len() - 1);
3273 self
3274 }
3275
3276 pub fn separator_colored(&mut self, color: Color) -> &mut Self {
3278 self.commands.push(Command::Text {
3279 content: "─".repeat(200),
3280 style: Style::new().fg(color),
3281 grow: 0,
3282 align: Align::Start,
3283 wrap: false,
3284 truncate: false,
3285 margin: Margin::default(),
3286 constraints: Constraints::default(),
3287 });
3288 self.last_text_idx = Some(self.commands.len() - 1);
3289 self
3290 }
3291
3292 pub fn help(&mut self, bindings: &[(&str, &str)]) -> Response {
3298 if bindings.is_empty() {
3299 return Response::none();
3300 }
3301
3302 self.interaction_count += 1;
3303 self.commands.push(Command::BeginContainer {
3304 direction: Direction::Row,
3305 gap: 2,
3306 align: Align::Start,
3307 align_self: None,
3308 justify: Justify::Start,
3309 border: None,
3310 border_sides: BorderSides::all(),
3311 border_style: Style::new().fg(self.theme.border),
3312 bg_color: None,
3313 padding: Padding::default(),
3314 margin: Margin::default(),
3315 constraints: Constraints::default(),
3316 title: None,
3317 grow: 0,
3318 group_name: None,
3319 });
3320 for (idx, (key, action)) in bindings.iter().enumerate() {
3321 if idx > 0 {
3322 self.styled("·", Style::new().fg(self.theme.text_dim));
3323 }
3324 self.styled(*key, Style::new().bold().fg(self.theme.primary));
3325 self.styled(*action, Style::new().fg(self.theme.text_dim));
3326 }
3327 self.commands.push(Command::EndContainer);
3328 self.last_text_idx = None;
3329
3330 Response::none()
3331 }
3332
3333 pub fn help_colored(
3335 &mut self,
3336 bindings: &[(&str, &str)],
3337 key_color: Color,
3338 text_color: Color,
3339 ) -> Response {
3340 if bindings.is_empty() {
3341 return Response::none();
3342 }
3343
3344 self.interaction_count += 1;
3345 self.commands.push(Command::BeginContainer {
3346 direction: Direction::Row,
3347 gap: 2,
3348 align: Align::Start,
3349 align_self: None,
3350 justify: Justify::Start,
3351 border: None,
3352 border_sides: BorderSides::all(),
3353 border_style: Style::new().fg(self.theme.border),
3354 bg_color: None,
3355 padding: Padding::default(),
3356 margin: Margin::default(),
3357 constraints: Constraints::default(),
3358 title: None,
3359 grow: 0,
3360 group_name: None,
3361 });
3362 for (idx, (key, action)) in bindings.iter().enumerate() {
3363 if idx > 0 {
3364 self.styled("·", Style::new().fg(text_color));
3365 }
3366 self.styled(*key, Style::new().bold().fg(key_color));
3367 self.styled(*action, Style::new().fg(text_color));
3368 }
3369 self.commands.push(Command::EndContainer);
3370 self.last_text_idx = None;
3371
3372 Response::none()
3373 }
3374
3375 pub fn key(&self, c: char) -> bool {
3381 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3382 return false;
3383 }
3384 self.events.iter().enumerate().any(|(i, e)| {
3385 !self.consumed[i]
3386 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3387 })
3388 }
3389
3390 pub fn key_code(&self, code: KeyCode) -> bool {
3397 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3398 return false;
3399 }
3400 self.events.iter().enumerate().any(|(i, e)| {
3401 !self.consumed[i]
3402 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
3403 })
3404 }
3405
3406 pub fn raw_key_code(&self, code: KeyCode) -> bool {
3414 self.events.iter().enumerate().any(|(i, e)| {
3415 !self.consumed[i]
3416 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
3417 })
3418 }
3419
3420 pub fn key_release(&self, c: char) -> bool {
3424 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3425 return false;
3426 }
3427 self.events.iter().enumerate().any(|(i, e)| {
3428 !self.consumed[i]
3429 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
3430 })
3431 }
3432
3433 pub fn key_code_release(&self, code: KeyCode) -> bool {
3437 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3438 return false;
3439 }
3440 self.events.iter().enumerate().any(|(i, e)| {
3441 !self.consumed[i]
3442 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
3443 })
3444 }
3445
3446 pub fn consume_key(&mut self, c: char) -> bool {
3456 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3457 return false;
3458 }
3459 for (i, event) in self.events.iter().enumerate() {
3460 if self.consumed[i] {
3461 continue;
3462 }
3463 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3464 {
3465 self.consumed[i] = true;
3466 return true;
3467 }
3468 }
3469 false
3470 }
3471
3472 pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
3482 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3483 return false;
3484 }
3485 for (i, event) in self.events.iter().enumerate() {
3486 if self.consumed[i] {
3487 continue;
3488 }
3489 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code) {
3490 self.consumed[i] = true;
3491 return true;
3492 }
3493 }
3494 false
3495 }
3496
3497 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3501 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3502 return false;
3503 }
3504 self.events.iter().enumerate().any(|(i, e)| {
3505 !self.consumed[i]
3506 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3507 })
3508 }
3509
3510 pub fn raw_key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3512 self.events.iter().enumerate().any(|(i, e)| {
3513 !self.consumed[i]
3514 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3515 })
3516 }
3517
3518 pub fn mouse_down(&self) -> Option<(u32, u32)> {
3522 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3523 return None;
3524 }
3525 self.events.iter().enumerate().find_map(|(i, event)| {
3526 if self.consumed[i] {
3527 return None;
3528 }
3529 if let Event::Mouse(mouse) = event {
3530 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3531 return Some((mouse.x, mouse.y));
3532 }
3533 }
3534 None
3535 })
3536 }
3537
3538 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3543 self.mouse_pos
3544 }
3545
3546 pub fn paste(&self) -> Option<&str> {
3548 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3549 return None;
3550 }
3551 self.events.iter().enumerate().find_map(|(i, event)| {
3552 if self.consumed[i] {
3553 return None;
3554 }
3555 if let Event::Paste(ref text) = event {
3556 return Some(text.as_str());
3557 }
3558 None
3559 })
3560 }
3561
3562 pub fn scroll_up(&self) -> bool {
3564 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3565 return false;
3566 }
3567 self.events.iter().enumerate().any(|(i, event)| {
3568 !self.consumed[i]
3569 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3570 })
3571 }
3572
3573 pub fn scroll_down(&self) -> bool {
3575 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3576 return false;
3577 }
3578 self.events.iter().enumerate().any(|(i, event)| {
3579 !self.consumed[i]
3580 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3581 })
3582 }
3583
3584 pub fn scroll_left(&self) -> bool {
3586 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3587 return false;
3588 }
3589 self.events.iter().enumerate().any(|(i, event)| {
3590 !self.consumed[i]
3591 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollLeft))
3592 })
3593 }
3594
3595 pub fn scroll_right(&self) -> bool {
3597 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3598 return false;
3599 }
3600 self.events.iter().enumerate().any(|(i, event)| {
3601 !self.consumed[i]
3602 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollRight))
3603 })
3604 }
3605
3606 pub fn quit(&mut self) {
3608 self.should_quit = true;
3609 }
3610
3611 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
3619 self.clipboard_text = Some(text.into());
3620 }
3621
3622 pub fn theme(&self) -> &Theme {
3624 &self.theme
3625 }
3626
3627 pub fn set_theme(&mut self, theme: Theme) {
3631 self.theme = theme;
3632 }
3633
3634 pub fn is_dark_mode(&self) -> bool {
3636 self.dark_mode
3637 }
3638
3639 pub fn set_dark_mode(&mut self, dark: bool) {
3641 self.dark_mode = dark;
3642 }
3643
3644 pub fn width(&self) -> u32 {
3648 self.area_width
3649 }
3650
3651 pub fn breakpoint(&self) -> Breakpoint {
3675 let w = self.area_width;
3676 if w < 40 {
3677 Breakpoint::Xs
3678 } else if w < 80 {
3679 Breakpoint::Sm
3680 } else if w < 120 {
3681 Breakpoint::Md
3682 } else if w < 160 {
3683 Breakpoint::Lg
3684 } else {
3685 Breakpoint::Xl
3686 }
3687 }
3688
3689 pub fn height(&self) -> u32 {
3691 self.area_height
3692 }
3693
3694 pub fn tick(&self) -> u64 {
3699 self.tick
3700 }
3701
3702 pub fn debug_enabled(&self) -> bool {
3706 self.debug
3707 }
3708}
3709
3710fn calendar_month_name(month: u32) -> &'static str {
3711 match month {
3712 1 => "Jan",
3713 2 => "Feb",
3714 3 => "Mar",
3715 4 => "Apr",
3716 5 => "May",
3717 6 => "Jun",
3718 7 => "Jul",
3719 8 => "Aug",
3720 9 => "Sep",
3721 10 => "Oct",
3722 11 => "Nov",
3723 12 => "Dec",
3724 _ => "???",
3725 }
3726}
3727
3728struct DirectoryRenderRow {
3729 depth: usize,
3730 label: String,
3731 is_leaf: bool,
3732 expanded: bool,
3733 is_last: bool,
3734 branch_mask: Vec<bool>,
3735}
3736
3737fn flatten_directory_rows(
3738 nodes: &[TreeNode],
3739 branch_mask: Vec<bool>,
3740 out: &mut Vec<DirectoryRenderRow>,
3741) {
3742 for (idx, node) in nodes.iter().enumerate() {
3743 let is_last = idx + 1 == nodes.len();
3744 out.push(DirectoryRenderRow {
3745 depth: branch_mask.len(),
3746 label: node.label.clone(),
3747 is_leaf: node.children.is_empty(),
3748 expanded: node.expanded,
3749 is_last,
3750 branch_mask: branch_mask.clone(),
3751 });
3752
3753 if node.expanded && !node.children.is_empty() {
3754 let mut next_mask = branch_mask.clone();
3755 next_mask.push(!is_last);
3756 flatten_directory_rows(&node.children, next_mask, out);
3757 }
3758 }
3759}
3760
3761fn calendar_move_cursor_by_days(state: &mut CalendarState, delta: i32) {
3762 let mut remaining = delta;
3763 while remaining != 0 {
3764 let days = CalendarState::days_in_month(state.year, state.month);
3765 if remaining > 0 {
3766 let forward = days.saturating_sub(state.cursor_day) as i32;
3767 if remaining <= forward {
3768 state.cursor_day += remaining as u32;
3769 return;
3770 }
3771
3772 remaining -= forward + 1;
3773 if state.month == 12 {
3774 state.month = 1;
3775 state.year += 1;
3776 } else {
3777 state.month += 1;
3778 }
3779 state.cursor_day = 1;
3780 } else {
3781 let backward = state.cursor_day.saturating_sub(1) as i32;
3782 if -remaining <= backward {
3783 state.cursor_day -= (-remaining) as u32;
3784 return;
3785 }
3786
3787 remaining += backward + 1;
3788 if state.month == 1 {
3789 state.month = 12;
3790 state.year -= 1;
3791 } else {
3792 state.month -= 1;
3793 }
3794 state.cursor_day = CalendarState::days_in_month(state.year, state.month);
3795 }
3796 }
3797}