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