1use super::*;
2
3impl Context {
4 pub fn table(&mut self, state: &mut TableState) -> Response {
10 self.table_colored(state, &WidgetColors::new())
11 }
12
13 pub fn table_colored(&mut self, state: &mut TableState, colors: &WidgetColors) -> Response {
15 if state.is_dirty() {
16 state.recompute_widths();
17 }
18
19 let old_selected = state.selected;
20 let old_sort_column = state.sort_column;
21 let old_sort_ascending = state.sort_ascending;
22 let old_page = state.page;
23 let old_filter = state.filter.clone();
24
25 let focused = self.register_focusable();
26 let interaction_id = self.next_interaction_id();
27 let mut response = self.response_for(interaction_id);
28 response.focused = focused;
29
30 self.table_handle_events(state, focused, interaction_id);
31
32 if state.is_dirty() {
33 state.recompute_widths();
34 }
35
36 self.table_render(state, focused, colors);
37
38 response.changed = state.selected != old_selected
39 || state.sort_column != old_sort_column
40 || state.sort_ascending != old_sort_ascending
41 || state.page != old_page
42 || state.filter != old_filter;
43 response
44 }
45
46 fn table_handle_events(
47 &mut self,
48 state: &mut TableState,
49 focused: bool,
50 interaction_id: usize,
51 ) {
52 self.handle_table_keys(state, focused);
53
54 if state.visible_indices().is_empty() && state.headers.is_empty() {
55 return;
56 }
57
58 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
59 for (i, event) in self.events.iter().enumerate() {
60 if self.consumed[i] {
61 continue;
62 }
63 if let Event::Mouse(mouse) = event {
64 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
65 continue;
66 }
67 let in_bounds = mouse.x >= rect.x
68 && mouse.x < rect.right()
69 && mouse.y >= rect.y
70 && mouse.y < rect.bottom();
71 if !in_bounds {
72 continue;
73 }
74
75 if mouse.y == rect.y {
76 let rel_x = mouse.x.saturating_sub(rect.x);
77 let mut x_offset = 0u32;
78 for (col_idx, width) in state.column_widths().iter().enumerate() {
79 if rel_x >= x_offset && rel_x < x_offset + *width {
80 state.toggle_sort(col_idx);
81 state.selected = 0;
82 self.consumed[i] = true;
83 break;
84 }
85 x_offset += *width;
86 if col_idx + 1 < state.column_widths().len() {
87 x_offset += 3;
88 }
89 }
90 continue;
91 }
92
93 if mouse.y < rect.y + 2 {
94 continue;
95 }
96
97 let visible_len = if state.page_size > 0 {
98 let start = state
99 .page
100 .saturating_mul(state.page_size)
101 .min(state.visible_indices().len());
102 let end = (start + state.page_size).min(state.visible_indices().len());
103 end.saturating_sub(start)
104 } else {
105 state.visible_indices().len()
106 };
107 let clicked_idx = (mouse.y - rect.y - 2) as usize;
108 if clicked_idx < visible_len {
109 state.selected = clicked_idx;
110 self.consumed[i] = true;
111 }
112 }
113 }
114 }
115 }
116
117 fn table_render(&mut self, state: &mut TableState, focused: bool, colors: &WidgetColors) {
118 let total_visible = state.visible_indices().len();
119 let page_start = if state.page_size > 0 {
120 state
121 .page
122 .saturating_mul(state.page_size)
123 .min(total_visible)
124 } else {
125 0
126 };
127 let page_end = if state.page_size > 0 {
128 (page_start + state.page_size).min(total_visible)
129 } else {
130 total_visible
131 };
132 let visible_len = page_end.saturating_sub(page_start);
133 state.selected = state.selected.min(visible_len.saturating_sub(1));
134
135 self.commands.push(Command::BeginContainer {
136 direction: Direction::Column,
137 gap: 0,
138 align: Align::Start,
139 align_self: None,
140 justify: Justify::Start,
141 border: None,
142 border_sides: BorderSides::all(),
143 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
144 bg_color: None,
145 padding: Padding::default(),
146 margin: Margin::default(),
147 constraints: Constraints::default(),
148 title: None,
149 grow: 0,
150 group_name: None,
151 });
152
153 self.render_table_header(state, colors);
154 self.render_table_rows(state, focused, page_start, visible_len, colors);
155
156 if state.page_size > 0 && state.total_pages() > 1 {
157 let current_page = (state.page + 1).to_string();
158 let total_pages = state.total_pages().to_string();
159 let mut page_text = String::with_capacity(current_page.len() + total_pages.len() + 6);
160 page_text.push_str("Page ");
161 page_text.push_str(¤t_page);
162 page_text.push('/');
163 page_text.push_str(&total_pages);
164 self.styled(
165 page_text,
166 Style::new()
167 .dim()
168 .fg(colors.fg.unwrap_or(self.theme.text_dim)),
169 );
170 }
171
172 self.commands.push(Command::EndContainer);
173 self.last_text_idx = None;
174 }
175
176 fn handle_table_keys(&mut self, state: &mut TableState, focused: bool) {
177 if !focused || state.visible_indices().is_empty() {
178 return;
179 }
180
181 let mut consumed_indices = Vec::new();
182 for (i, event) in self.events.iter().enumerate() {
183 if let Event::Key(key) = event {
184 if key.kind != KeyEventKind::Press {
185 continue;
186 }
187 match key.code {
188 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
189 let visible_len = table_visible_len(state);
190 state.selected = state.selected.min(visible_len.saturating_sub(1));
191 let _ = handle_vertical_nav(
192 &mut state.selected,
193 visible_len.saturating_sub(1),
194 key.code.clone(),
195 );
196 consumed_indices.push(i);
197 }
198 KeyCode::PageUp => {
199 let old_page = state.page;
200 state.prev_page();
201 if state.page != old_page {
202 state.selected = 0;
203 }
204 consumed_indices.push(i);
205 }
206 KeyCode::PageDown => {
207 let old_page = state.page;
208 state.next_page();
209 if state.page != old_page {
210 state.selected = 0;
211 }
212 consumed_indices.push(i);
213 }
214 _ => {}
215 }
216 }
217 }
218 for index in consumed_indices {
219 self.consumed[index] = true;
220 }
221 }
222
223 fn render_table_header(&mut self, state: &TableState, colors: &WidgetColors) {
224 let header_cells = state
225 .headers
226 .iter()
227 .enumerate()
228 .map(|(i, header)| {
229 if state.sort_column == Some(i) {
230 if state.sort_ascending {
231 let mut sorted_header = String::with_capacity(header.len() + 2);
232 sorted_header.push_str(header);
233 sorted_header.push_str(" ▲");
234 sorted_header
235 } else {
236 let mut sorted_header = String::with_capacity(header.len() + 2);
237 sorted_header.push_str(header);
238 sorted_header.push_str(" ▼");
239 sorted_header
240 }
241 } else {
242 header.clone()
243 }
244 })
245 .collect::<Vec<_>>();
246 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
247 self.styled(
248 header_line,
249 Style::new().bold().fg(colors.fg.unwrap_or(self.theme.text)),
250 );
251
252 let separator = state
253 .column_widths()
254 .iter()
255 .map(|w| "─".repeat(*w as usize))
256 .collect::<Vec<_>>()
257 .join("─┼─");
258 self.text(separator);
259 }
260
261 fn render_table_rows(
262 &mut self,
263 state: &TableState,
264 focused: bool,
265 page_start: usize,
266 visible_len: usize,
267 colors: &WidgetColors,
268 ) {
269 for idx in 0..visible_len {
270 let data_idx = state.visible_indices()[page_start + idx];
271 let Some(row) = state.rows.get(data_idx) else {
272 continue;
273 };
274 let line = format_table_row(row, state.column_widths(), " │ ");
275 if idx == state.selected {
276 let mut style = Style::new()
277 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
278 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
279 if focused {
280 style = style.bold();
281 }
282 self.styled(line, style);
283 } else {
284 let mut style = Style::new().fg(colors.fg.unwrap_or(self.theme.text));
285 if state.zebra {
286 let zebra_bg = colors.bg.unwrap_or({
287 if idx % 2 == 0 {
288 self.theme.surface
289 } else {
290 self.theme.surface_hover
291 }
292 });
293 style = style.bg(zebra_bg);
294 }
295 self.styled(line, style);
296 }
297 }
298 }
299
300 pub fn tabs(&mut self, state: &mut TabsState) -> Response {
306 self.tabs_colored(state, &WidgetColors::new())
307 }
308
309 pub fn tabs_colored(&mut self, state: &mut TabsState, colors: &WidgetColors) -> Response {
311 if state.labels.is_empty() {
312 state.selected = 0;
313 return Response::none();
314 }
315
316 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
317 let old_selected = state.selected;
318 let focused = self.register_focusable();
319 let interaction_id = self.next_interaction_id();
320 let mut response = self.response_for(interaction_id);
321 response.focused = focused;
322
323 if focused {
324 let mut consumed_indices = Vec::new();
325 for (i, event) in self.events.iter().enumerate() {
326 if let Event::Key(key) = event {
327 if key.kind != KeyEventKind::Press {
328 continue;
329 }
330 match key.code {
331 KeyCode::Left => {
332 state.selected = if state.selected == 0 {
333 state.labels.len().saturating_sub(1)
334 } else {
335 state.selected - 1
336 };
337 consumed_indices.push(i);
338 }
339 KeyCode::Right => {
340 if !state.labels.is_empty() {
341 state.selected = (state.selected + 1) % state.labels.len();
342 }
343 consumed_indices.push(i);
344 }
345 _ => {}
346 }
347 }
348 }
349
350 for index in consumed_indices {
351 self.consumed[index] = true;
352 }
353 }
354
355 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
356 for (i, event) in self.events.iter().enumerate() {
357 if self.consumed[i] {
358 continue;
359 }
360 if let Event::Mouse(mouse) = event {
361 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
362 continue;
363 }
364 let in_bounds = mouse.x >= rect.x
365 && mouse.x < rect.right()
366 && mouse.y >= rect.y
367 && mouse.y < rect.bottom();
368 if !in_bounds {
369 continue;
370 }
371
372 let mut x_offset = 0u32;
373 let rel_x = mouse.x - rect.x;
374 for (idx, label) in state.labels.iter().enumerate() {
375 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
376 if rel_x >= x_offset && rel_x < x_offset + tab_width {
377 state.selected = idx;
378 self.consumed[i] = true;
379 break;
380 }
381 x_offset += tab_width + 1;
382 }
383 }
384 }
385 }
386
387 self.commands.push(Command::BeginContainer {
388 direction: Direction::Row,
389 gap: 1,
390 align: Align::Start,
391 align_self: None,
392 justify: Justify::Start,
393 border: None,
394 border_sides: BorderSides::all(),
395 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
396 bg_color: None,
397 padding: Padding::default(),
398 margin: Margin::default(),
399 constraints: Constraints::default(),
400 title: None,
401 grow: 0,
402 group_name: None,
403 });
404 for (idx, label) in state.labels.iter().enumerate() {
405 let style = if idx == state.selected {
406 let s = Style::new()
407 .fg(colors.accent.unwrap_or(self.theme.primary))
408 .bold();
409 if focused {
410 s.underline()
411 } else {
412 s
413 }
414 } else {
415 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
416 };
417 let mut tab = String::with_capacity(label.len() + 4);
418 tab.push_str("[ ");
419 tab.push_str(label);
420 tab.push_str(" ]");
421 self.styled(tab, style);
422 }
423 self.commands.push(Command::EndContainer);
424 self.last_text_idx = None;
425
426 response.changed = state.selected != old_selected;
427 response
428 }
429
430 pub fn button(&mut self, label: impl Into<String>) -> Response {
436 self.button_colored(label, &WidgetColors::new())
437 }
438
439 pub fn button_colored(&mut self, label: impl Into<String>, colors: &WidgetColors) -> Response {
441 let focused = self.register_focusable();
442 let interaction_id = self.next_interaction_id();
443 let mut response = self.response_for(interaction_id);
444 response.focused = focused;
445
446 let mut activated = response.clicked;
447 if focused {
448 let mut consumed_indices = Vec::new();
449 for (i, event) in self.events.iter().enumerate() {
450 if let Event::Key(key) = event {
451 if key.kind != KeyEventKind::Press {
452 continue;
453 }
454 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
455 activated = true;
456 consumed_indices.push(i);
457 }
458 }
459 }
460
461 for index in consumed_indices {
462 self.consumed[index] = true;
463 }
464 }
465
466 let hovered = response.hovered;
467 let base_fg = colors.fg.unwrap_or(self.theme.text);
468 let accent = colors.accent.unwrap_or(self.theme.accent);
469 let base_bg = colors.bg.unwrap_or(self.theme.surface_hover);
470 let style = if focused {
471 Style::new().fg(accent).bold()
472 } else if hovered {
473 Style::new().fg(accent)
474 } else {
475 Style::new().fg(base_fg)
476 };
477 let has_custom_bg = colors.bg.is_some();
478 let bg_color = if has_custom_bg || hovered || focused {
479 Some(base_bg)
480 } else {
481 None
482 };
483
484 self.commands.push(Command::BeginContainer {
485 direction: Direction::Row,
486 gap: 0,
487 align: Align::Start,
488 align_self: None,
489 justify: Justify::Start,
490 border: None,
491 border_sides: BorderSides::all(),
492 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
493 bg_color,
494 padding: Padding::default(),
495 margin: Margin::default(),
496 constraints: Constraints::default(),
497 title: None,
498 grow: 0,
499 group_name: None,
500 });
501 let raw_label = label.into();
502 let mut label_text = String::with_capacity(raw_label.len() + 4);
503 label_text.push_str("[ ");
504 label_text.push_str(&raw_label);
505 label_text.push_str(" ]");
506 self.styled(label_text, style);
507 self.commands.push(Command::EndContainer);
508 self.last_text_idx = None;
509
510 response.clicked = activated;
511 response
512 }
513
514 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> Response {
519 let focused = self.register_focusable();
520 let interaction_id = self.next_interaction_id();
521 let mut response = self.response_for(interaction_id);
522 response.focused = focused;
523
524 let mut activated = response.clicked;
525 if focused {
526 let mut consumed_indices = Vec::new();
527 for (i, event) in self.events.iter().enumerate() {
528 if let Event::Key(key) = event {
529 if key.kind != KeyEventKind::Press {
530 continue;
531 }
532 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
533 activated = true;
534 consumed_indices.push(i);
535 }
536 }
537 }
538 for index in consumed_indices {
539 self.consumed[index] = true;
540 }
541 }
542
543 let label = label.into();
544 let hover_bg = if response.hovered || focused {
545 Some(self.theme.surface_hover)
546 } else {
547 None
548 };
549 let (text, style, bg_color, border) = match variant {
550 ButtonVariant::Default => {
551 let style = if focused {
552 Style::new().fg(self.theme.primary).bold()
553 } else if response.hovered {
554 Style::new().fg(self.theme.accent)
555 } else {
556 Style::new().fg(self.theme.text)
557 };
558 let mut text = String::with_capacity(label.len() + 4);
559 text.push_str("[ ");
560 text.push_str(&label);
561 text.push_str(" ]");
562 (text, style, hover_bg, None)
563 }
564 ButtonVariant::Primary => {
565 let style = if focused {
566 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
567 } else if response.hovered {
568 Style::new().fg(self.theme.bg).bg(self.theme.accent)
569 } else {
570 Style::new().fg(self.theme.bg).bg(self.theme.primary)
571 };
572 let mut text = String::with_capacity(label.len() + 2);
573 text.push(' ');
574 text.push_str(&label);
575 text.push(' ');
576 (text, style, hover_bg, None)
577 }
578 ButtonVariant::Danger => {
579 let style = if focused {
580 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
581 } else if response.hovered {
582 Style::new().fg(self.theme.bg).bg(self.theme.warning)
583 } else {
584 Style::new().fg(self.theme.bg).bg(self.theme.error)
585 };
586 let mut text = String::with_capacity(label.len() + 2);
587 text.push(' ');
588 text.push_str(&label);
589 text.push(' ');
590 (text, style, hover_bg, None)
591 }
592 ButtonVariant::Outline => {
593 let border_color = if focused {
594 self.theme.primary
595 } else if response.hovered {
596 self.theme.accent
597 } else {
598 self.theme.border
599 };
600 let style = if focused {
601 Style::new().fg(self.theme.primary).bold()
602 } else if response.hovered {
603 Style::new().fg(self.theme.accent)
604 } else {
605 Style::new().fg(self.theme.text)
606 };
607 (
608 {
609 let mut text = String::with_capacity(label.len() + 2);
610 text.push(' ');
611 text.push_str(&label);
612 text.push(' ');
613 text
614 },
615 style,
616 hover_bg,
617 Some((Border::Rounded, Style::new().fg(border_color))),
618 )
619 }
620 };
621
622 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
623 self.commands.push(Command::BeginContainer {
624 direction: Direction::Row,
625 gap: 0,
626 align: Align::Center,
627 align_self: None,
628 justify: Justify::Center,
629 border: if border.is_some() {
630 Some(btn_border)
631 } else {
632 None
633 },
634 border_sides: BorderSides::all(),
635 border_style: btn_border_style,
636 bg_color,
637 padding: Padding::default(),
638 margin: Margin::default(),
639 constraints: Constraints::default(),
640 title: None,
641 grow: 0,
642 group_name: None,
643 });
644 self.styled(text, style);
645 self.commands.push(Command::EndContainer);
646 self.last_text_idx = None;
647
648 response.clicked = activated;
649 response
650 }
651
652 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> Response {
658 self.checkbox_colored(label, checked, &WidgetColors::new())
659 }
660
661 pub fn checkbox_colored(
663 &mut self,
664 label: impl Into<String>,
665 checked: &mut bool,
666 colors: &WidgetColors,
667 ) -> Response {
668 let focused = self.register_focusable();
669 let interaction_id = self.next_interaction_id();
670 let mut response = self.response_for(interaction_id);
671 response.focused = focused;
672 let mut should_toggle = response.clicked;
673 let old_checked = *checked;
674
675 if focused {
676 let mut consumed_indices = Vec::new();
677 for (i, event) in self.events.iter().enumerate() {
678 if let Event::Key(key) = event {
679 if key.kind != KeyEventKind::Press {
680 continue;
681 }
682 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
683 should_toggle = true;
684 consumed_indices.push(i);
685 }
686 }
687 }
688
689 for index in consumed_indices {
690 self.consumed[index] = true;
691 }
692 }
693
694 if should_toggle {
695 *checked = !*checked;
696 }
697
698 let hover_bg = if response.hovered || focused {
699 Some(self.theme.surface_hover)
700 } else {
701 None
702 };
703 self.commands.push(Command::BeginContainer {
704 direction: Direction::Row,
705 gap: 1,
706 align: Align::Start,
707 align_self: None,
708 justify: Justify::Start,
709 border: None,
710 border_sides: BorderSides::all(),
711 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
712 bg_color: hover_bg,
713 padding: Padding::default(),
714 margin: Margin::default(),
715 constraints: Constraints::default(),
716 title: None,
717 grow: 0,
718 group_name: None,
719 });
720 let marker_style = if *checked {
721 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
722 } else {
723 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
724 };
725 let marker = if *checked { "[x]" } else { "[ ]" };
726 let label_text = label.into();
727 if focused {
728 let mut marker_text = String::with_capacity(2 + marker.len());
729 marker_text.push_str("▸ ");
730 marker_text.push_str(marker);
731 self.styled(marker_text, marker_style.bold());
732 self.styled(
733 label_text,
734 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
735 );
736 } else {
737 self.styled(marker, marker_style);
738 self.styled(
739 label_text,
740 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
741 );
742 }
743 self.commands.push(Command::EndContainer);
744 self.last_text_idx = None;
745
746 response.changed = *checked != old_checked;
747 response
748 }
749
750 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> Response {
757 self.toggle_colored(label, on, &WidgetColors::new())
758 }
759
760 pub fn toggle_colored(
762 &mut self,
763 label: impl Into<String>,
764 on: &mut bool,
765 colors: &WidgetColors,
766 ) -> Response {
767 let focused = self.register_focusable();
768 let interaction_id = self.next_interaction_id();
769 let mut response = self.response_for(interaction_id);
770 response.focused = focused;
771 let mut should_toggle = response.clicked;
772 let old_on = *on;
773
774 if focused {
775 let mut consumed_indices = Vec::new();
776 for (i, event) in self.events.iter().enumerate() {
777 if let Event::Key(key) = event {
778 if key.kind != KeyEventKind::Press {
779 continue;
780 }
781 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
782 should_toggle = true;
783 consumed_indices.push(i);
784 }
785 }
786 }
787
788 for index in consumed_indices {
789 self.consumed[index] = true;
790 }
791 }
792
793 if should_toggle {
794 *on = !*on;
795 }
796
797 let hover_bg = if response.hovered || focused {
798 Some(self.theme.surface_hover)
799 } else {
800 None
801 };
802 self.commands.push(Command::BeginContainer {
803 direction: Direction::Row,
804 gap: 2,
805 align: Align::Start,
806 align_self: None,
807 justify: Justify::Start,
808 border: None,
809 border_sides: BorderSides::all(),
810 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
811 bg_color: hover_bg,
812 padding: Padding::default(),
813 margin: Margin::default(),
814 constraints: Constraints::default(),
815 title: None,
816 grow: 0,
817 group_name: None,
818 });
819 let label_text = label.into();
820 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
821 let switch_style = if *on {
822 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
823 } else {
824 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
825 };
826 if focused {
827 let mut focused_label = String::with_capacity(2 + label_text.len());
828 focused_label.push_str("▸ ");
829 focused_label.push_str(&label_text);
830 self.styled(
831 focused_label,
832 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
833 );
834 self.styled(switch, switch_style.bold());
835 } else {
836 self.styled(
837 label_text,
838 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
839 );
840 self.styled(switch, switch_style);
841 }
842 self.commands.push(Command::EndContainer);
843 self.last_text_idx = None;
844
845 response.changed = *on != old_on;
846 response
847 }
848
849 pub fn select(&mut self, state: &mut SelectState) -> Response {
856 self.select_colored(state, &WidgetColors::new())
857 }
858
859 pub fn select_colored(&mut self, state: &mut SelectState, colors: &WidgetColors) -> Response {
861 if state.items.is_empty() {
862 return Response::none();
863 }
864 state.selected = state.selected.min(state.items.len().saturating_sub(1));
865
866 let focused = self.register_focusable();
867 let interaction_id = self.next_interaction_id();
868 let mut response = self.response_for(interaction_id);
869 response.focused = focused;
870 let old_selected = state.selected;
871
872 self.select_handle_events(state, focused, response.clicked);
873 self.select_render(state, focused, colors);
874 response.changed = state.selected != old_selected;
875 response
876 }
877
878 fn select_handle_events(&mut self, state: &mut SelectState, focused: bool, clicked: bool) {
879 if clicked {
880 state.open = !state.open;
881 if state.open {
882 state.set_cursor(state.selected);
883 }
884 }
885
886 if !focused {
887 return;
888 }
889
890 let mut consumed_indices = Vec::new();
891 for (i, event) in self.events.iter().enumerate() {
892 if self.consumed[i] {
893 continue;
894 }
895 if let Event::Key(key) = event {
896 if key.kind != KeyEventKind::Press {
897 continue;
898 }
899 if state.open {
900 match key.code {
901 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
902 let mut cursor = state.cursor();
903 let _ = handle_vertical_nav(
904 &mut cursor,
905 state.items.len().saturating_sub(1),
906 key.code.clone(),
907 );
908 state.set_cursor(cursor);
909 consumed_indices.push(i);
910 }
911 KeyCode::Enter | KeyCode::Char(' ') => {
912 state.selected = state.cursor();
913 state.open = false;
914 consumed_indices.push(i);
915 }
916 KeyCode::Esc => {
917 state.open = false;
918 consumed_indices.push(i);
919 }
920 _ => {}
921 }
922 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
923 state.open = true;
924 state.set_cursor(state.selected);
925 consumed_indices.push(i);
926 }
927 }
928 }
929 for idx in consumed_indices {
930 self.consumed[idx] = true;
931 }
932 }
933
934 fn select_render(&mut self, state: &SelectState, focused: bool, colors: &WidgetColors) {
935 let border_color = if focused {
936 colors.accent.unwrap_or(self.theme.primary)
937 } else {
938 colors.border.unwrap_or(self.theme.border)
939 };
940 let display_text = state
941 .items
942 .get(state.selected)
943 .cloned()
944 .unwrap_or_else(|| state.placeholder.clone());
945 let arrow = if state.open { "▲" } else { "▼" };
946
947 self.commands.push(Command::BeginContainer {
948 direction: Direction::Column,
949 gap: 0,
950 align: Align::Start,
951 align_self: None,
952 justify: Justify::Start,
953 border: None,
954 border_sides: BorderSides::all(),
955 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
956 bg_color: None,
957 padding: Padding::default(),
958 margin: Margin::default(),
959 constraints: Constraints::default(),
960 title: None,
961 grow: 0,
962 group_name: None,
963 });
964
965 self.render_select_trigger(&display_text, arrow, border_color, colors);
966
967 if state.open {
968 self.render_select_dropdown(state, colors);
969 }
970
971 self.commands.push(Command::EndContainer);
972 self.last_text_idx = None;
973 }
974
975 fn render_select_trigger(
976 &mut self,
977 display_text: &str,
978 arrow: &str,
979 border_color: Color,
980 colors: &WidgetColors,
981 ) {
982 self.commands.push(Command::BeginContainer {
983 direction: Direction::Row,
984 gap: 1,
985 align: Align::Start,
986 align_self: None,
987 justify: Justify::Start,
988 border: Some(Border::Rounded),
989 border_sides: BorderSides::all(),
990 border_style: Style::new().fg(border_color),
991 bg_color: None,
992 padding: Padding {
993 left: 1,
994 right: 1,
995 top: 0,
996 bottom: 0,
997 },
998 margin: Margin::default(),
999 constraints: Constraints::default(),
1000 title: None,
1001 grow: 0,
1002 group_name: None,
1003 });
1004 self.interaction_count += 1;
1005 self.styled(
1006 display_text,
1007 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1008 );
1009 self.styled(
1010 arrow,
1011 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim)),
1012 );
1013 self.commands.push(Command::EndContainer);
1014 self.last_text_idx = None;
1015 }
1016
1017 fn render_select_dropdown(&mut self, state: &SelectState, colors: &WidgetColors) {
1018 for (idx, item) in state.items.iter().enumerate() {
1019 let is_cursor = idx == state.cursor();
1020 let style = if is_cursor {
1021 Style::new()
1022 .bold()
1023 .fg(colors.accent.unwrap_or(self.theme.primary))
1024 } else {
1025 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1026 };
1027 let prefix = if is_cursor { "▸ " } else { " " };
1028 let mut row = String::with_capacity(prefix.len() + item.len());
1029 row.push_str(prefix);
1030 row.push_str(item);
1031 self.styled(row, style);
1032 }
1033 }
1034
1035 pub fn radio(&mut self, state: &mut RadioState) -> Response {
1040 self.radio_colored(state, &WidgetColors::new())
1041 }
1042
1043 pub fn radio_colored(&mut self, state: &mut RadioState, colors: &WidgetColors) -> Response {
1045 if state.items.is_empty() {
1046 return Response::none();
1047 }
1048 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1049 let focused = self.register_focusable();
1050 let old_selected = state.selected;
1051
1052 if focused {
1053 let mut consumed_indices = Vec::new();
1054 for (i, event) in self.events.iter().enumerate() {
1055 if self.consumed[i] {
1056 continue;
1057 }
1058 if let Event::Key(key) = event {
1059 if key.kind != KeyEventKind::Press {
1060 continue;
1061 }
1062 match key.code {
1063 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1064 let _ = handle_vertical_nav(
1065 &mut state.selected,
1066 state.items.len().saturating_sub(1),
1067 key.code.clone(),
1068 );
1069 consumed_indices.push(i);
1070 }
1071 KeyCode::Enter | KeyCode::Char(' ') => {
1072 consumed_indices.push(i);
1073 }
1074 _ => {}
1075 }
1076 }
1077 }
1078 for idx in consumed_indices {
1079 self.consumed[idx] = true;
1080 }
1081 }
1082
1083 let interaction_id = self.next_interaction_id();
1084 let mut response = self.response_for(interaction_id);
1085 response.focused = focused;
1086
1087 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1088 for (i, event) in self.events.iter().enumerate() {
1089 if self.consumed[i] {
1090 continue;
1091 }
1092 if let Event::Mouse(mouse) = event {
1093 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1094 continue;
1095 }
1096 let in_bounds = mouse.x >= rect.x
1097 && mouse.x < rect.right()
1098 && mouse.y >= rect.y
1099 && mouse.y < rect.bottom();
1100 if !in_bounds {
1101 continue;
1102 }
1103 let clicked_idx = (mouse.y - rect.y) as usize;
1104 if clicked_idx < state.items.len() {
1105 state.selected = clicked_idx;
1106 self.consumed[i] = true;
1107 }
1108 }
1109 }
1110 }
1111
1112 self.commands.push(Command::BeginContainer {
1113 direction: Direction::Column,
1114 gap: 0,
1115 align: Align::Start,
1116 align_self: None,
1117 justify: Justify::Start,
1118 border: None,
1119 border_sides: BorderSides::all(),
1120 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1121 bg_color: None,
1122 padding: Padding::default(),
1123 margin: Margin::default(),
1124 constraints: Constraints::default(),
1125 title: None,
1126 grow: 0,
1127 group_name: None,
1128 });
1129
1130 for (idx, item) in state.items.iter().enumerate() {
1131 let is_selected = idx == state.selected;
1132 let marker = if is_selected { "●" } else { "○" };
1133 let style = if is_selected {
1134 if focused {
1135 Style::new()
1136 .bold()
1137 .fg(colors.accent.unwrap_or(self.theme.primary))
1138 } else {
1139 Style::new().fg(colors.accent.unwrap_or(self.theme.primary))
1140 }
1141 } else {
1142 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1143 };
1144 let prefix = if focused && idx == state.selected {
1145 "▸ "
1146 } else {
1147 " "
1148 };
1149 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1150 row.push_str(prefix);
1151 row.push_str(marker);
1152 row.push(' ');
1153 row.push_str(item);
1154 self.styled(row, style);
1155 }
1156
1157 self.commands.push(Command::EndContainer);
1158 self.last_text_idx = None;
1159 response.changed = state.selected != old_selected;
1160 response
1161 }
1162
1163 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> Response {
1167 if state.items.is_empty() {
1168 return Response::none();
1169 }
1170 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
1171 let focused = self.register_focusable();
1172 let old_selected = state.selected.clone();
1173
1174 if focused {
1175 let mut consumed_indices = Vec::new();
1176 for (i, event) in self.events.iter().enumerate() {
1177 if self.consumed[i] {
1178 continue;
1179 }
1180 if let Event::Key(key) = event {
1181 if key.kind != KeyEventKind::Press {
1182 continue;
1183 }
1184 match key.code {
1185 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1186 let _ = handle_vertical_nav(
1187 &mut state.cursor,
1188 state.items.len().saturating_sub(1),
1189 key.code.clone(),
1190 );
1191 consumed_indices.push(i);
1192 }
1193 KeyCode::Char(' ') | KeyCode::Enter => {
1194 state.toggle(state.cursor);
1195 consumed_indices.push(i);
1196 }
1197 _ => {}
1198 }
1199 }
1200 }
1201 for idx in consumed_indices {
1202 self.consumed[idx] = true;
1203 }
1204 }
1205
1206 let interaction_id = self.next_interaction_id();
1207 let mut response = self.response_for(interaction_id);
1208 response.focused = focused;
1209
1210 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1211 for (i, event) in self.events.iter().enumerate() {
1212 if self.consumed[i] {
1213 continue;
1214 }
1215 if let Event::Mouse(mouse) = event {
1216 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1217 continue;
1218 }
1219 let in_bounds = mouse.x >= rect.x
1220 && mouse.x < rect.right()
1221 && mouse.y >= rect.y
1222 && mouse.y < rect.bottom();
1223 if !in_bounds {
1224 continue;
1225 }
1226 let clicked_idx = (mouse.y - rect.y) as usize;
1227 if clicked_idx < state.items.len() {
1228 state.toggle(clicked_idx);
1229 state.cursor = clicked_idx;
1230 self.consumed[i] = true;
1231 }
1232 }
1233 }
1234 }
1235
1236 self.commands.push(Command::BeginContainer {
1237 direction: Direction::Column,
1238 gap: 0,
1239 align: Align::Start,
1240 align_self: None,
1241 justify: Justify::Start,
1242 border: None,
1243 border_sides: BorderSides::all(),
1244 border_style: Style::new().fg(self.theme.border),
1245 bg_color: None,
1246 padding: Padding::default(),
1247 margin: Margin::default(),
1248 constraints: Constraints::default(),
1249 title: None,
1250 grow: 0,
1251 group_name: None,
1252 });
1253
1254 for (idx, item) in state.items.iter().enumerate() {
1255 let checked = state.selected.contains(&idx);
1256 let marker = if checked { "[x]" } else { "[ ]" };
1257 let is_cursor = idx == state.cursor;
1258 let style = if is_cursor && focused {
1259 Style::new().bold().fg(self.theme.primary)
1260 } else if checked {
1261 Style::new().fg(self.theme.success)
1262 } else {
1263 Style::new().fg(self.theme.text)
1264 };
1265 let prefix = if is_cursor && focused { "▸ " } else { " " };
1266 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1267 row.push_str(prefix);
1268 row.push_str(marker);
1269 row.push(' ');
1270 row.push_str(item);
1271 self.styled(row, style);
1272 }
1273
1274 self.commands.push(Command::EndContainer);
1275 self.last_text_idx = None;
1276 response.changed = state.selected != old_selected;
1277 response
1278 }
1279
1280 }