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 match mouse.kind {
2022 MouseKind::ScrollUp => {
2023 state.scroll_offset = state.scroll_offset.saturating_sub(1);
2024 self.consumed[i] = true;
2025 }
2026 MouseKind::ScrollDown => {
2027 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
2028 self.consumed[i] = true;
2029 }
2030 _ => {}
2031 }
2032 }
2033 }
2034 }
2035
2036 state.scroll_offset = state.scroll_offset.min(max_offset);
2037 let start = state
2038 .scroll_offset
2039 .min(state.entries.len().saturating_sub(visible_rows));
2040 let end = (start + visible_rows).min(state.entries.len());
2041
2042 self.commands.push(Command::BeginContainer {
2043 direction: Direction::Column,
2044 gap: 0,
2045 align: Align::Start,
2046 align_self: None,
2047 justify: Justify::Start,
2048 border: Some(Border::Single),
2049 border_sides: BorderSides::all(),
2050 border_style: Style::new().fg(self.theme.border),
2051 bg_color: None,
2052 padding: Padding::default(),
2053 margin: Margin::default(),
2054 constraints: Constraints::default(),
2055 title: None,
2056 grow: 0,
2057 group_name: None,
2058 });
2059
2060 for entry in state
2061 .entries
2062 .iter()
2063 .skip(start)
2064 .take(end.saturating_sub(start))
2065 {
2066 self.commands.push(Command::RichText {
2067 segments: entry.segments.clone(),
2068 wrap: false,
2069 align: Align::Start,
2070 margin: Margin::default(),
2071 constraints: Constraints::default(),
2072 });
2073 }
2074
2075 if show_indicator {
2076 let end_pos = end.min(state.entries.len());
2077 let line = format!(
2078 "{}-{} / {}",
2079 start.saturating_add(1),
2080 end_pos,
2081 state.entries.len()
2082 );
2083 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
2084 }
2085
2086 self.commands.push(Command::EndContainer);
2087 self.last_text_idx = None;
2088 response.changed = state.scroll_offset != old_offset;
2089 response
2090 }
2091
2092 pub fn tree(&mut self, state: &mut TreeState) -> Response {
2094 let entries = state.flatten();
2095 if entries.is_empty() {
2096 return Response::none();
2097 }
2098 state.selected = state.selected.min(entries.len().saturating_sub(1));
2099 let old_selected = state.selected;
2100 let focused = self.register_focusable();
2101 let interaction_id = self.next_interaction_id();
2102 let mut response = self.response_for(interaction_id);
2103 response.focused = focused;
2104 let mut changed = false;
2105
2106 if focused {
2107 let mut consumed_indices = Vec::new();
2108 for (i, event) in self.events.iter().enumerate() {
2109 if self.consumed[i] {
2110 continue;
2111 }
2112 if let Event::Key(key) = event {
2113 if key.kind != KeyEventKind::Press {
2114 continue;
2115 }
2116 match key.code {
2117 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2118 let max_index = state.flatten().len().saturating_sub(1);
2119 let _ = handle_vertical_nav(
2120 &mut state.selected,
2121 max_index,
2122 key.code.clone(),
2123 );
2124 changed = changed || state.selected != old_selected;
2125 consumed_indices.push(i);
2126 }
2127 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
2128 state.toggle_at(state.selected);
2129 changed = true;
2130 consumed_indices.push(i);
2131 }
2132 KeyCode::Left => {
2133 let entry = &entries[state.selected.min(entries.len() - 1)];
2134 if entry.expanded {
2135 state.toggle_at(state.selected);
2136 changed = true;
2137 }
2138 consumed_indices.push(i);
2139 }
2140 _ => {}
2141 }
2142 }
2143 }
2144 for idx in consumed_indices {
2145 self.consumed[idx] = true;
2146 }
2147 }
2148
2149 self.commands.push(Command::BeginContainer {
2150 direction: Direction::Column,
2151 gap: 0,
2152 align: Align::Start,
2153 align_self: None,
2154 justify: Justify::Start,
2155 border: None,
2156 border_sides: BorderSides::all(),
2157 border_style: Style::new().fg(self.theme.border),
2158 bg_color: None,
2159 padding: Padding::default(),
2160 margin: Margin::default(),
2161 constraints: Constraints::default(),
2162 title: None,
2163 grow: 0,
2164 group_name: None,
2165 });
2166
2167 let entries = state.flatten();
2168 for (idx, entry) in entries.iter().enumerate() {
2169 let indent = " ".repeat(entry.depth);
2170 let icon = if entry.is_leaf {
2171 " "
2172 } else if entry.expanded {
2173 "▾ "
2174 } else {
2175 "▸ "
2176 };
2177 let is_selected = idx == state.selected;
2178 let style = if is_selected && focused {
2179 Style::new().bold().fg(self.theme.primary)
2180 } else if is_selected {
2181 Style::new().fg(self.theme.primary)
2182 } else {
2183 Style::new().fg(self.theme.text)
2184 };
2185 let cursor = if is_selected && focused { "▸" } else { " " };
2186 let mut row =
2187 String::with_capacity(cursor.len() + indent.len() + icon.len() + entry.label.len());
2188 row.push_str(cursor);
2189 row.push_str(&indent);
2190 row.push_str(icon);
2191 row.push_str(&entry.label);
2192 self.styled(row, style);
2193 }
2194
2195 self.commands.push(Command::EndContainer);
2196 self.last_text_idx = None;
2197 response.changed = changed || state.selected != old_selected;
2198 response
2199 }
2200
2201 pub fn directory_tree(&mut self, state: &mut DirectoryTreeState) -> Response {
2203 let entries = state.tree.flatten();
2204 if entries.is_empty() {
2205 return Response::none();
2206 }
2207 state.tree.selected = state.tree.selected.min(entries.len().saturating_sub(1));
2208 let old_selected = state.tree.selected;
2209 let focused = self.register_focusable();
2210 let interaction_id = self.next_interaction_id();
2211 let mut response = self.response_for(interaction_id);
2212 response.focused = focused;
2213 let mut changed = false;
2214
2215 if focused {
2216 let mut consumed_indices = Vec::new();
2217 for (i, event) in self.events.iter().enumerate() {
2218 if self.consumed[i] {
2219 continue;
2220 }
2221 if let Event::Key(key) = event {
2222 if key.kind != KeyEventKind::Press {
2223 continue;
2224 }
2225 match key.code {
2226 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2227 let max_index = state.tree.flatten().len().saturating_sub(1);
2228 let _ = handle_vertical_nav(
2229 &mut state.tree.selected,
2230 max_index,
2231 key.code.clone(),
2232 );
2233 changed = changed || state.tree.selected != old_selected;
2234 consumed_indices.push(i);
2235 }
2236 KeyCode::Right => {
2237 let current_entries = state.tree.flatten();
2238 let entry = ¤t_entries
2239 [state.tree.selected.min(current_entries.len() - 1)];
2240 if !entry.is_leaf && !entry.expanded {
2241 state.tree.toggle_at(state.tree.selected);
2242 changed = true;
2243 }
2244 consumed_indices.push(i);
2245 }
2246 KeyCode::Enter | KeyCode::Char(' ') => {
2247 state.tree.toggle_at(state.tree.selected);
2248 changed = true;
2249 consumed_indices.push(i);
2250 }
2251 KeyCode::Left => {
2252 let current_entries = state.tree.flatten();
2253 let entry = ¤t_entries
2254 [state.tree.selected.min(current_entries.len() - 1)];
2255 if entry.expanded {
2256 state.tree.toggle_at(state.tree.selected);
2257 changed = true;
2258 }
2259 consumed_indices.push(i);
2260 }
2261 _ => {}
2262 }
2263 }
2264 }
2265 for idx in consumed_indices {
2266 self.consumed[idx] = true;
2267 }
2268 }
2269
2270 self.commands.push(Command::BeginContainer {
2271 direction: Direction::Column,
2272 gap: 0,
2273 align: Align::Start,
2274 align_self: None,
2275 justify: Justify::Start,
2276 border: None,
2277 border_sides: BorderSides::all(),
2278 border_style: Style::new().fg(self.theme.border),
2279 bg_color: None,
2280 padding: Padding::default(),
2281 margin: Margin::default(),
2282 constraints: Constraints::default(),
2283 title: None,
2284 grow: 0,
2285 group_name: None,
2286 });
2287
2288 let mut rows = Vec::new();
2289 flatten_directory_rows(&state.tree.nodes, Vec::new(), &mut rows);
2290 for (idx, row_entry) in rows.iter().enumerate() {
2291 let mut row = String::new();
2292 let cursor = if idx == state.tree.selected && focused {
2293 "▸"
2294 } else {
2295 " "
2296 };
2297 row.push_str(cursor);
2298 row.push(' ');
2299
2300 if row_entry.depth > 0 {
2301 for has_more in &row_entry.branch_mask {
2302 if *has_more {
2303 row.push_str("│ ");
2304 } else {
2305 row.push_str(" ");
2306 }
2307 }
2308 if row_entry.is_last {
2309 row.push_str("└── ");
2310 } else {
2311 row.push_str("├── ");
2312 }
2313 }
2314
2315 let icon = if row_entry.is_leaf {
2316 " "
2317 } else if row_entry.expanded {
2318 "▾ "
2319 } else {
2320 "▸ "
2321 };
2322 if state.show_icons {
2323 row.push_str(icon);
2324 }
2325 row.push_str(&row_entry.label);
2326
2327 let style = if idx == state.tree.selected && focused {
2328 Style::new().bold().fg(self.theme.primary)
2329 } else if idx == state.tree.selected {
2330 Style::new().fg(self.theme.primary)
2331 } else {
2332 Style::new().fg(self.theme.text)
2333 };
2334 self.styled(row, style);
2335 }
2336
2337 self.commands.push(Command::EndContainer);
2338 self.last_text_idx = None;
2339 response.changed = changed || state.tree.selected != old_selected;
2340 response
2341 }
2342
2343 pub fn virtual_list(
2350 &mut self,
2351 state: &mut ListState,
2352 visible_height: usize,
2353 f: impl Fn(&mut Context, usize),
2354 ) -> Response {
2355 if state.items.is_empty() {
2356 return Response::none();
2357 }
2358 state.selected = state.selected.min(state.items.len().saturating_sub(1));
2359 let interaction_id = self.next_interaction_id();
2360 let focused = self.register_focusable();
2361 let old_selected = state.selected;
2362
2363 if focused {
2364 let mut consumed_indices = Vec::new();
2365 for (i, event) in self.events.iter().enumerate() {
2366 if self.consumed[i] {
2367 continue;
2368 }
2369 if let Event::Key(key) = event {
2370 if key.kind != KeyEventKind::Press {
2371 continue;
2372 }
2373 match key.code {
2374 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2375 let _ = handle_vertical_nav(
2376 &mut state.selected,
2377 state.items.len().saturating_sub(1),
2378 key.code.clone(),
2379 );
2380 consumed_indices.push(i);
2381 }
2382 KeyCode::PageUp => {
2383 state.selected = state.selected.saturating_sub(visible_height);
2384 consumed_indices.push(i);
2385 }
2386 KeyCode::PageDown => {
2387 state.selected = (state.selected + visible_height)
2388 .min(state.items.len().saturating_sub(1));
2389 consumed_indices.push(i);
2390 }
2391 KeyCode::Home => {
2392 state.selected = 0;
2393 consumed_indices.push(i);
2394 }
2395 KeyCode::End => {
2396 state.selected = state.items.len().saturating_sub(1);
2397 consumed_indices.push(i);
2398 }
2399 _ => {}
2400 }
2401 }
2402 }
2403 for idx in consumed_indices {
2404 self.consumed[idx] = true;
2405 }
2406 }
2407
2408 let start = if state.selected >= visible_height {
2409 state.selected - visible_height + 1
2410 } else {
2411 0
2412 };
2413 let end = (start + visible_height).min(state.items.len());
2414
2415 self.commands.push(Command::BeginContainer {
2416 direction: Direction::Column,
2417 gap: 0,
2418 align: Align::Start,
2419 align_self: None,
2420 justify: Justify::Start,
2421 border: None,
2422 border_sides: BorderSides::all(),
2423 border_style: Style::new().fg(self.theme.border),
2424 bg_color: None,
2425 padding: Padding::default(),
2426 margin: Margin::default(),
2427 constraints: Constraints::default(),
2428 title: None,
2429 grow: 0,
2430 group_name: None,
2431 });
2432
2433 if start > 0 {
2434 let hidden = start.to_string();
2435 let mut line = String::with_capacity(hidden.len() + 10);
2436 line.push_str(" ↑ ");
2437 line.push_str(&hidden);
2438 line.push_str(" more");
2439 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2440 }
2441
2442 for idx in start..end {
2443 f(self, idx);
2444 }
2445
2446 let remaining = state.items.len().saturating_sub(end);
2447 if remaining > 0 {
2448 let hidden = remaining.to_string();
2449 let mut line = String::with_capacity(hidden.len() + 10);
2450 line.push_str(" ↓ ");
2451 line.push_str(&hidden);
2452 line.push_str(" more");
2453 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2454 }
2455
2456 self.commands.push(Command::EndContainer);
2457 self.last_text_idx = None;
2458 let mut response = self.response_for(interaction_id);
2459 response.focused = focused;
2460 response.changed = state.selected != old_selected;
2461 response
2462 }
2463
2464 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
2468 if !state.open {
2469 return Response::none();
2470 }
2471
2472 state.last_selected = None;
2473 let interaction_id = self.next_interaction_id();
2474
2475 let filtered = state.filtered_indices();
2476 let sel = state.selected().min(filtered.len().saturating_sub(1));
2477 state.set_selected(sel);
2478
2479 let mut consumed_indices = Vec::new();
2480
2481 for (i, event) in self.events.iter().enumerate() {
2482 if self.consumed[i] {
2483 continue;
2484 }
2485 if let Event::Key(key) = event {
2486 if key.kind != KeyEventKind::Press {
2487 continue;
2488 }
2489 match key.code {
2490 KeyCode::Esc => {
2491 state.open = false;
2492 consumed_indices.push(i);
2493 }
2494 KeyCode::Up => {
2495 let s = state.selected();
2496 state.set_selected(s.saturating_sub(1));
2497 consumed_indices.push(i);
2498 }
2499 KeyCode::Down => {
2500 let s = state.selected();
2501 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
2502 consumed_indices.push(i);
2503 }
2504 KeyCode::Enter => {
2505 if let Some(&cmd_idx) = filtered.get(state.selected()) {
2506 state.last_selected = Some(cmd_idx);
2507 state.open = false;
2508 }
2509 consumed_indices.push(i);
2510 }
2511 KeyCode::Backspace => {
2512 if state.cursor > 0 {
2513 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
2514 let end_idx = byte_index_for_char(&state.input, state.cursor);
2515 state.input.replace_range(byte_idx..end_idx, "");
2516 state.cursor -= 1;
2517 state.set_selected(0);
2518 }
2519 consumed_indices.push(i);
2520 }
2521 KeyCode::Char(ch) => {
2522 let byte_idx = byte_index_for_char(&state.input, state.cursor);
2523 state.input.insert(byte_idx, ch);
2524 state.cursor += 1;
2525 state.set_selected(0);
2526 consumed_indices.push(i);
2527 }
2528 _ => {}
2529 }
2530 }
2531 }
2532 for idx in consumed_indices {
2533 self.consumed[idx] = true;
2534 }
2535
2536 let filtered = state.filtered_indices();
2537
2538 let _ = self.modal(|ui| {
2539 let primary = ui.theme.primary;
2540 let _ = ui
2541 .container()
2542 .border(Border::Rounded)
2543 .border_style(Style::new().fg(primary))
2544 .pad(1)
2545 .max_w(60)
2546 .col(|ui| {
2547 let border_color = ui.theme.primary;
2548 let _ = ui
2549 .bordered(Border::Rounded)
2550 .border_style(Style::new().fg(border_color))
2551 .px(1)
2552 .col(|ui| {
2553 let display = if state.input.is_empty() {
2554 "Type to search...".to_string()
2555 } else {
2556 state.input.clone()
2557 };
2558 let style = if state.input.is_empty() {
2559 Style::new().dim().fg(ui.theme.text_dim)
2560 } else {
2561 Style::new().fg(ui.theme.text)
2562 };
2563 ui.styled(display, style);
2564 });
2565
2566 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
2567 let cmd = &state.commands[cmd_idx];
2568 let is_selected = list_idx == state.selected();
2569 let style = if is_selected {
2570 Style::new().bold().fg(ui.theme.primary)
2571 } else {
2572 Style::new().fg(ui.theme.text)
2573 };
2574 let prefix = if is_selected { "▸ " } else { " " };
2575 let shortcut_text = cmd
2576 .shortcut
2577 .as_deref()
2578 .map(|s| {
2579 let mut text = String::with_capacity(s.len() + 4);
2580 text.push_str(" (");
2581 text.push_str(s);
2582 text.push(')');
2583 text
2584 })
2585 .unwrap_or_default();
2586 let mut line = String::with_capacity(
2587 prefix.len() + cmd.label.len() + shortcut_text.len(),
2588 );
2589 line.push_str(prefix);
2590 line.push_str(&cmd.label);
2591 line.push_str(&shortcut_text);
2592 ui.styled(line, style);
2593 if is_selected && !cmd.description.is_empty() {
2594 let mut desc = String::with_capacity(4 + cmd.description.len());
2595 desc.push_str(" ");
2596 desc.push_str(&cmd.description);
2597 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
2598 }
2599 }
2600
2601 if filtered.is_empty() {
2602 ui.styled(
2603 " No matching commands",
2604 Style::new().dim().fg(ui.theme.text_dim),
2605 );
2606 }
2607 });
2608 });
2609
2610 let mut response = self.response_for(interaction_id);
2611 response.changed = state.last_selected.is_some();
2612 response
2613 }
2614
2615 pub fn markdown(&mut self, text: &str) -> Response {
2622 self.commands.push(Command::BeginContainer {
2623 direction: Direction::Column,
2624 gap: 0,
2625 align: Align::Start,
2626 align_self: None,
2627 justify: Justify::Start,
2628 border: None,
2629 border_sides: BorderSides::all(),
2630 border_style: Style::new().fg(self.theme.border),
2631 bg_color: None,
2632 padding: Padding::default(),
2633 margin: Margin::default(),
2634 constraints: Constraints::default(),
2635 title: None,
2636 grow: 0,
2637 group_name: None,
2638 });
2639 self.interaction_count += 1;
2640
2641 let text_style = Style::new().fg(self.theme.text);
2642 let bold_style = Style::new().fg(self.theme.text).bold();
2643 let code_style = Style::new().fg(self.theme.accent);
2644 let border_style = Style::new().fg(self.theme.border).dim();
2645
2646 let mut in_code_block = false;
2647 let mut code_block_lang = String::new();
2648 let mut code_block_lines: Vec<String> = Vec::new();
2649
2650 for line in text.lines() {
2651 let trimmed = line.trim();
2652
2653 if in_code_block {
2654 if trimmed.starts_with("```") {
2655 in_code_block = false;
2656 let code_content = code_block_lines.join("\n");
2657 let theme = self.theme;
2658 let highlighted: Option<Vec<Vec<(String, Style)>>> =
2659 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
2660 let _ = self.container().bg(theme.surface).p(1).col(|ui| {
2661 if let Some(ref hl_lines) = highlighted {
2662 for segs in hl_lines {
2663 if segs.is_empty() {
2664 ui.text(" ");
2665 } else {
2666 ui.line(|ui| {
2667 for (t, s) in segs {
2668 ui.styled(t, *s);
2669 }
2670 });
2671 }
2672 }
2673 } else {
2674 for cl in &code_block_lines {
2675 ui.styled(cl, code_style);
2676 }
2677 }
2678 });
2679 code_block_lang.clear();
2680 code_block_lines.clear();
2681 } else {
2682 code_block_lines.push(line.to_string());
2683 }
2684 continue;
2685 }
2686
2687 if trimmed.is_empty() {
2688 self.text(" ");
2689 continue;
2690 }
2691 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
2692 self.styled("─".repeat(40), border_style);
2693 continue;
2694 }
2695 if let Some(heading) = trimmed.strip_prefix("### ") {
2696 self.styled(heading, Style::new().bold().fg(self.theme.accent));
2697 } else if let Some(heading) = trimmed.strip_prefix("## ") {
2698 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
2699 } else if let Some(heading) = trimmed.strip_prefix("# ") {
2700 self.styled(heading, Style::new().bold().fg(self.theme.primary));
2701 } else if let Some(item) = trimmed
2702 .strip_prefix("- ")
2703 .or_else(|| trimmed.strip_prefix("* "))
2704 {
2705 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
2706 if segs.len() <= 1 {
2707 let mut line = String::with_capacity(4 + item.len());
2708 line.push_str(" • ");
2709 line.push_str(item);
2710 self.styled(line, text_style);
2711 } else {
2712 self.line(|ui| {
2713 ui.styled(" • ", text_style);
2714 for (s, st) in segs {
2715 ui.styled(s, st);
2716 }
2717 });
2718 }
2719 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
2720 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
2721 if parts.len() == 2 {
2722 let segs =
2723 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
2724 if segs.len() <= 1 {
2725 let mut line = String::with_capacity(4 + parts[0].len() + parts[1].len());
2726 line.push_str(" ");
2727 line.push_str(parts[0]);
2728 line.push_str(". ");
2729 line.push_str(parts[1]);
2730 self.styled(line, text_style);
2731 } else {
2732 self.line(|ui| {
2733 let mut prefix = String::with_capacity(4 + parts[0].len());
2734 prefix.push_str(" ");
2735 prefix.push_str(parts[0]);
2736 prefix.push_str(". ");
2737 ui.styled(prefix, text_style);
2738 for (s, st) in segs {
2739 ui.styled(s, st);
2740 }
2741 });
2742 }
2743 } else {
2744 self.text(trimmed);
2745 }
2746 } else if let Some(lang) = trimmed.strip_prefix("```") {
2747 in_code_block = true;
2748 code_block_lang = lang.trim().to_string();
2749 } else {
2750 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
2751 if segs.len() <= 1 {
2752 self.styled(trimmed, text_style);
2753 } else {
2754 self.line(|ui| {
2755 for (s, st) in segs {
2756 ui.styled(s, st);
2757 }
2758 });
2759 }
2760 }
2761 }
2762
2763 if in_code_block && !code_block_lines.is_empty() {
2764 for cl in &code_block_lines {
2765 self.styled(cl, code_style);
2766 }
2767 }
2768
2769 self.commands.push(Command::EndContainer);
2770 self.last_text_idx = None;
2771 Response::none()
2772 }
2773
2774 pub(crate) fn parse_inline_segments(
2775 text: &str,
2776 base: Style,
2777 bold: Style,
2778 code: Style,
2779 ) -> Vec<(String, Style)> {
2780 let mut segments: Vec<(String, Style)> = Vec::new();
2781 let mut current = String::new();
2782 let chars: Vec<char> = text.chars().collect();
2783 let mut i = 0;
2784 while i < chars.len() {
2785 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
2786 let rest: String = chars[i + 2..].iter().collect();
2787 if let Some(end) = rest.find("**") {
2788 if !current.is_empty() {
2789 segments.push((std::mem::take(&mut current), base));
2790 }
2791 let inner: String = rest[..end].to_string();
2792 let char_count = inner.chars().count();
2793 segments.push((inner, bold));
2794 i += 2 + char_count + 2;
2795 continue;
2796 }
2797 }
2798 if chars[i] == '*'
2799 && (i + 1 >= chars.len() || chars[i + 1] != '*')
2800 && (i == 0 || chars[i - 1] != '*')
2801 {
2802 let rest: String = chars[i + 1..].iter().collect();
2803 if let Some(end) = rest.find('*') {
2804 if !current.is_empty() {
2805 segments.push((std::mem::take(&mut current), base));
2806 }
2807 let inner: String = rest[..end].to_string();
2808 let char_count = inner.chars().count();
2809 segments.push((inner, base.italic()));
2810 i += 1 + char_count + 1;
2811 continue;
2812 }
2813 }
2814 if chars[i] == '`' {
2815 let rest: String = chars[i + 1..].iter().collect();
2816 if let Some(end) = rest.find('`') {
2817 if !current.is_empty() {
2818 segments.push((std::mem::take(&mut current), base));
2819 }
2820 let inner: String = rest[..end].to_string();
2821 let char_count = inner.chars().count();
2822 segments.push((inner, code));
2823 i += 1 + char_count + 1;
2824 continue;
2825 }
2826 }
2827 current.push(chars[i]);
2828 i += 1;
2829 }
2830 if !current.is_empty() {
2831 segments.push((current, base));
2832 }
2833 segments
2834 }
2835
2836 pub fn key_seq(&self, seq: &str) -> bool {
2843 if seq.is_empty() {
2844 return false;
2845 }
2846 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2847 return false;
2848 }
2849 let target: Vec<char> = seq.chars().collect();
2850 let mut matched = 0;
2851 for (i, event) in self.events.iter().enumerate() {
2852 if self.consumed[i] {
2853 continue;
2854 }
2855 if let Event::Key(key) = event {
2856 if key.kind != KeyEventKind::Press {
2857 continue;
2858 }
2859 if let KeyCode::Char(c) = key.code {
2860 if c == target[matched] {
2861 matched += 1;
2862 if matched == target.len() {
2863 return true;
2864 }
2865 } else {
2866 matched = 0;
2867 if c == target[0] {
2868 matched = 1;
2869 }
2870 }
2871 }
2872 }
2873 }
2874 false
2875 }
2876
2877 pub fn separator(&mut self) -> &mut Self {
2882 self.commands.push(Command::Text {
2883 content: "─".repeat(200),
2884 style: Style::new().fg(self.theme.border).dim(),
2885 grow: 0,
2886 align: Align::Start,
2887 wrap: false,
2888 truncate: false,
2889 margin: Margin::default(),
2890 constraints: Constraints::default(),
2891 });
2892 self.last_text_idx = Some(self.commands.len() - 1);
2893 self
2894 }
2895
2896 pub fn separator_colored(&mut self, color: Color) -> &mut Self {
2898 self.commands.push(Command::Text {
2899 content: "─".repeat(200),
2900 style: Style::new().fg(color),
2901 grow: 0,
2902 align: Align::Start,
2903 wrap: false,
2904 truncate: false,
2905 margin: Margin::default(),
2906 constraints: Constraints::default(),
2907 });
2908 self.last_text_idx = Some(self.commands.len() - 1);
2909 self
2910 }
2911
2912 pub fn help(&mut self, bindings: &[(&str, &str)]) -> Response {
2918 if bindings.is_empty() {
2919 return Response::none();
2920 }
2921
2922 self.interaction_count += 1;
2923 self.commands.push(Command::BeginContainer {
2924 direction: Direction::Row,
2925 gap: 2,
2926 align: Align::Start,
2927 align_self: None,
2928 justify: Justify::Start,
2929 border: None,
2930 border_sides: BorderSides::all(),
2931 border_style: Style::new().fg(self.theme.border),
2932 bg_color: None,
2933 padding: Padding::default(),
2934 margin: Margin::default(),
2935 constraints: Constraints::default(),
2936 title: None,
2937 grow: 0,
2938 group_name: None,
2939 });
2940 for (idx, (key, action)) in bindings.iter().enumerate() {
2941 if idx > 0 {
2942 self.styled("·", Style::new().fg(self.theme.text_dim));
2943 }
2944 self.styled(*key, Style::new().bold().fg(self.theme.primary));
2945 self.styled(*action, Style::new().fg(self.theme.text_dim));
2946 }
2947 self.commands.push(Command::EndContainer);
2948 self.last_text_idx = None;
2949
2950 Response::none()
2951 }
2952
2953 pub fn help_colored(
2955 &mut self,
2956 bindings: &[(&str, &str)],
2957 key_color: Color,
2958 text_color: Color,
2959 ) -> Response {
2960 if bindings.is_empty() {
2961 return Response::none();
2962 }
2963
2964 self.interaction_count += 1;
2965 self.commands.push(Command::BeginContainer {
2966 direction: Direction::Row,
2967 gap: 2,
2968 align: Align::Start,
2969 align_self: None,
2970 justify: Justify::Start,
2971 border: None,
2972 border_sides: BorderSides::all(),
2973 border_style: Style::new().fg(self.theme.border),
2974 bg_color: None,
2975 padding: Padding::default(),
2976 margin: Margin::default(),
2977 constraints: Constraints::default(),
2978 title: None,
2979 grow: 0,
2980 group_name: None,
2981 });
2982 for (idx, (key, action)) in bindings.iter().enumerate() {
2983 if idx > 0 {
2984 self.styled("·", Style::new().fg(text_color));
2985 }
2986 self.styled(*key, Style::new().bold().fg(key_color));
2987 self.styled(*action, Style::new().fg(text_color));
2988 }
2989 self.commands.push(Command::EndContainer);
2990 self.last_text_idx = None;
2991
2992 Response::none()
2993 }
2994
2995 pub fn key(&self, c: char) -> bool {
3001 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3002 return false;
3003 }
3004 self.events.iter().enumerate().any(|(i, e)| {
3005 !self.consumed[i]
3006 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3007 })
3008 }
3009
3010 pub fn key_code(&self, code: KeyCode) -> bool {
3014 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3015 return false;
3016 }
3017 self.events.iter().enumerate().any(|(i, e)| {
3018 !self.consumed[i]
3019 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
3020 })
3021 }
3022
3023 pub fn key_release(&self, c: char) -> bool {
3027 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3028 return false;
3029 }
3030 self.events.iter().enumerate().any(|(i, e)| {
3031 !self.consumed[i]
3032 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
3033 })
3034 }
3035
3036 pub fn key_code_release(&self, code: KeyCode) -> bool {
3040 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3041 return false;
3042 }
3043 self.events.iter().enumerate().any(|(i, e)| {
3044 !self.consumed[i]
3045 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
3046 })
3047 }
3048
3049 pub fn consume_key(&mut self, c: char) -> bool {
3059 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3060 return false;
3061 }
3062 for (i, event) in self.events.iter().enumerate() {
3063 if self.consumed[i] {
3064 continue;
3065 }
3066 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3067 {
3068 self.consumed[i] = true;
3069 return true;
3070 }
3071 }
3072 false
3073 }
3074
3075 pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
3085 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3086 return false;
3087 }
3088 for (i, event) in self.events.iter().enumerate() {
3089 if self.consumed[i] {
3090 continue;
3091 }
3092 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code) {
3093 self.consumed[i] = true;
3094 return true;
3095 }
3096 }
3097 false
3098 }
3099
3100 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3104 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3105 return false;
3106 }
3107 self.events.iter().enumerate().any(|(i, e)| {
3108 !self.consumed[i]
3109 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3110 })
3111 }
3112
3113 pub fn mouse_down(&self) -> Option<(u32, u32)> {
3117 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3118 return None;
3119 }
3120 self.events.iter().enumerate().find_map(|(i, event)| {
3121 if self.consumed[i] {
3122 return None;
3123 }
3124 if let Event::Mouse(mouse) = event {
3125 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3126 return Some((mouse.x, mouse.y));
3127 }
3128 }
3129 None
3130 })
3131 }
3132
3133 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3138 self.mouse_pos
3139 }
3140
3141 pub fn paste(&self) -> Option<&str> {
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::Paste(ref text) = event {
3151 return Some(text.as_str());
3152 }
3153 None
3154 })
3155 }
3156
3157 pub fn scroll_up(&self) -> bool {
3159 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3160 return false;
3161 }
3162 self.events.iter().enumerate().any(|(i, event)| {
3163 !self.consumed[i]
3164 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3165 })
3166 }
3167
3168 pub fn scroll_down(&self) -> bool {
3170 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3171 return false;
3172 }
3173 self.events.iter().enumerate().any(|(i, event)| {
3174 !self.consumed[i]
3175 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3176 })
3177 }
3178
3179 pub fn quit(&mut self) {
3181 self.should_quit = true;
3182 }
3183
3184 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
3192 self.clipboard_text = Some(text.into());
3193 }
3194
3195 pub fn theme(&self) -> &Theme {
3197 &self.theme
3198 }
3199
3200 pub fn set_theme(&mut self, theme: Theme) {
3204 self.theme = theme;
3205 }
3206
3207 pub fn is_dark_mode(&self) -> bool {
3209 self.dark_mode
3210 }
3211
3212 pub fn set_dark_mode(&mut self, dark: bool) {
3214 self.dark_mode = dark;
3215 }
3216
3217 pub fn width(&self) -> u32 {
3221 self.area_width
3222 }
3223
3224 pub fn breakpoint(&self) -> Breakpoint {
3248 let w = self.area_width;
3249 if w < 40 {
3250 Breakpoint::Xs
3251 } else if w < 80 {
3252 Breakpoint::Sm
3253 } else if w < 120 {
3254 Breakpoint::Md
3255 } else if w < 160 {
3256 Breakpoint::Lg
3257 } else {
3258 Breakpoint::Xl
3259 }
3260 }
3261
3262 pub fn height(&self) -> u32 {
3264 self.area_height
3265 }
3266
3267 pub fn tick(&self) -> u64 {
3272 self.tick
3273 }
3274
3275 pub fn debug_enabled(&self) -> bool {
3279 self.debug
3280 }
3281}
3282
3283fn calendar_month_name(month: u32) -> &'static str {
3284 match month {
3285 1 => "Jan",
3286 2 => "Feb",
3287 3 => "Mar",
3288 4 => "Apr",
3289 5 => "May",
3290 6 => "Jun",
3291 7 => "Jul",
3292 8 => "Aug",
3293 9 => "Sep",
3294 10 => "Oct",
3295 11 => "Nov",
3296 12 => "Dec",
3297 _ => "???",
3298 }
3299}
3300
3301struct DirectoryRenderRow {
3302 depth: usize,
3303 label: String,
3304 is_leaf: bool,
3305 expanded: bool,
3306 is_last: bool,
3307 branch_mask: Vec<bool>,
3308}
3309
3310fn flatten_directory_rows(
3311 nodes: &[TreeNode],
3312 branch_mask: Vec<bool>,
3313 out: &mut Vec<DirectoryRenderRow>,
3314) {
3315 for (idx, node) in nodes.iter().enumerate() {
3316 let is_last = idx + 1 == nodes.len();
3317 out.push(DirectoryRenderRow {
3318 depth: branch_mask.len(),
3319 label: node.label.clone(),
3320 is_leaf: node.children.is_empty(),
3321 expanded: node.expanded,
3322 is_last,
3323 branch_mask: branch_mask.clone(),
3324 });
3325
3326 if node.expanded && !node.children.is_empty() {
3327 let mut next_mask = branch_mask.clone();
3328 next_mask.push(!is_last);
3329 flatten_directory_rows(&node.children, next_mask, out);
3330 }
3331 }
3332}
3333
3334fn calendar_move_cursor_by_days(state: &mut CalendarState, delta: i32) {
3335 let mut remaining = delta;
3336 while remaining != 0 {
3337 let days = CalendarState::days_in_month(state.year, state.month);
3338 if remaining > 0 {
3339 let forward = days.saturating_sub(state.cursor_day) as i32;
3340 if remaining <= forward {
3341 state.cursor_day += remaining as u32;
3342 return;
3343 }
3344
3345 remaining -= forward + 1;
3346 if state.month == 12 {
3347 state.month = 1;
3348 state.year += 1;
3349 } else {
3350 state.month += 1;
3351 }
3352 state.cursor_day = 1;
3353 } else {
3354 let backward = state.cursor_day.saturating_sub(1) as i32;
3355 if -remaining <= backward {
3356 state.cursor_day -= (-remaining) as u32;
3357 return;
3358 }
3359
3360 remaining += backward + 1;
3361 if state.month == 1 {
3362 state.month = 12;
3363 state.year -= 1;
3364 } else {
3365 state.month -= 1;
3366 }
3367 state.cursor_day = CalendarState::days_in_month(state.year, state.month);
3368 }
3369 }
3370}