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