1use std::sync::Arc;
4
5use crate::callback::{Callback, KeyHandler};
6use crate::core::element::{Element, ElementKind};
7use crate::core::event::{KeyEvent, MouseEvent};
8use crate::style::{BorderStyle, Length, Padding, ScrollbarConfig, Span, Style, StyleSlot};
9use unicode_width::UnicodeWidthStr;
10
11use super::{Spinner, SpinnerSpeed, SpinnerStyle};
12use crate::widgets::scroll::{ScrollAction, ScrollKeymap, scroll_action_from_key};
13
14#[derive(Clone, Debug, PartialEq)]
25pub struct ListConfig {
26 pub border: bool,
28 pub border_style: BorderStyle,
31 pub padding: Padding,
34 pub style: Style,
36 pub selection_style: StyleSlot,
38 pub unfocused_selection_style: StyleSlot,
42 pub item_hover_style: Option<StyleSlot>,
45 pub selection_full_width: bool,
47 pub selection_symbol: Option<Arc<str>>,
49 pub selection_symbol_right: Option<Arc<str>>,
52 pub selection_symbol_style: Option<Style>,
54 pub unfocused_selection_symbol_style: Option<Style>,
58 pub symbol_column: bool,
60 pub gutter_gap: u16,
62 pub gutter_for_non_selectable: bool,
64 pub item_horizontal_padding: Padding,
66 pub header_horizontal_padding: Padding,
68 pub empty_text_style: Style,
70 pub scrollbar: bool,
72 pub scrollbar_config: ScrollbarConfig,
74}
75
76impl Default for ListConfig {
77 fn default() -> Self {
78 Self {
79 border: true,
80 border_style: BorderStyle::Plain,
81 padding: Padding::default(),
82 style: Style::default(),
83 selection_style: StyleSlot::Inherit,
84 unfocused_selection_style: StyleSlot::Inherit,
85 item_hover_style: None,
86 selection_full_width: false,
87 selection_symbol: None,
88 selection_symbol_right: None,
89 selection_symbol_style: None,
90 unfocused_selection_symbol_style: None,
91 symbol_column: true,
92 gutter_gap: 0,
93 gutter_for_non_selectable: false,
94 item_horizontal_padding: Padding::default(),
95 header_horizontal_padding: Padding::default(),
96 empty_text_style: Style::default(),
97 scrollbar: false,
98 scrollbar_config: ScrollbarConfig::default(),
99 }
100 }
101}
102
103impl ListConfig {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn border(mut self, border: bool) -> Self {
111 self.border = border;
112 self
113 }
114
115 pub fn border_style(mut self, style: BorderStyle) -> Self {
117 self.border_style = style;
118 self
119 }
120
121 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
123 self.padding = padding.into();
124 self
125 }
126
127 pub fn style(mut self, style: Style) -> Self {
129 self.style = style;
130 self
131 }
132
133 pub fn selection_style(mut self, style: Style) -> Self {
135 self.selection_style = StyleSlot::Replace(style);
136 self
137 }
138
139 pub fn extend_selection_style(mut self, style: Style) -> Self {
141 self.selection_style = StyleSlot::Extend(style);
142 self
143 }
144
145 pub fn inherit_selection_style(mut self) -> Self {
147 self.selection_style = StyleSlot::Inherit;
148 self
149 }
150
151 pub fn unfocused_selection_style(mut self, style: Style) -> Self {
153 self.unfocused_selection_style = StyleSlot::Replace(style);
154 self
155 }
156
157 pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
159 self.unfocused_selection_style = StyleSlot::Extend(style);
160 self
161 }
162
163 pub fn inherit_unfocused_selection_style(mut self) -> Self {
165 self.unfocused_selection_style = StyleSlot::Inherit;
166 self
167 }
168
169 pub fn selection_full_width(mut self, full_width: bool) -> Self {
171 self.selection_full_width = full_width;
172 self
173 }
174
175 pub fn selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
177 self.selection_symbol = symbol.map(Into::into);
178 self
179 }
180
181 pub fn selection_symbol_right(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
184 self.selection_symbol_right = symbol.map(Into::into);
185 self
186 }
187
188 pub fn selection_symbol_style(mut self, style: Style) -> Self {
190 self.selection_symbol_style = Some(style);
191 self
192 }
193
194 pub fn unfocused_selection_symbol_style(mut self, style: Style) -> Self {
196 self.unfocused_selection_symbol_style = Some(style);
197 self
198 }
199
200 pub fn symbol_column(mut self, enabled: bool) -> Self {
202 self.symbol_column = enabled;
203 self
204 }
205
206 pub fn gutter_gap(mut self, gap: u16) -> Self {
208 self.gutter_gap = gap;
209 self
210 }
211
212 pub fn gutter_for_non_selectable(mut self, enabled: bool) -> Self {
214 self.gutter_for_non_selectable = enabled;
215 self
216 }
217
218 pub fn item_hover_style(mut self, style: Style) -> Self {
220 self.item_hover_style = Some(StyleSlot::Replace(style));
221 self
222 }
223
224 pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
226 self.item_hover_style = Some(slot);
227 self
228 }
229
230 pub fn item_horizontal_padding(mut self, padding: impl Into<Padding>) -> Self {
232 self.item_horizontal_padding = padding.into();
233 self
234 }
235
236 pub fn header_horizontal_padding(mut self, padding: impl Into<Padding>) -> Self {
238 self.header_horizontal_padding = padding.into();
239 self
240 }
241
242 pub fn empty_text_style(mut self, style: Style) -> Self {
244 self.empty_text_style = style;
245 self
246 }
247
248 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
250 self.scrollbar = scrollbar;
251 self
252 }
253
254 pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
256 self.scrollbar_config = config;
257 self
258 }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
263pub struct ListEvent {
264 pub index: usize,
266}
267
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
270pub enum ListItemRole {
271 #[default]
273 Normal,
274 Header,
276 Spacer,
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
281pub(crate) enum ListItemPrefixKind {
282 Plain,
283 Numbered(usize),
284}
285
286#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
288pub enum ListSymbolPosition {
289 #[default]
291 Left,
292 Right,
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
298pub struct ListItemGutter {
299 pub(crate) kind: ListItemGutterKind,
300}
301
302#[derive(Clone, Debug, PartialEq, Eq)]
303pub(crate) enum ListItemGutterKind {
304 Text(Vec<Span>),
305 Spinner(ListItemSpinnerGutter),
306}
307
308#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct ListItemStatus {
311 pub(crate) kind: ListItemStatusKind,
312}
313
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub(crate) enum ListItemStatusKind {
316 Text(Vec<Span>),
317 Spinner(ListItemSpinnerGutter),
318}
319
320#[derive(Clone, Debug, PartialEq, Eq)]
321pub(crate) struct ListItemSpinnerGutter {
322 pub spinner_style: SpinnerStyle,
323 pub speed: SpinnerSpeed,
324 pub frame: usize,
325 pub auto_frame: bool,
326 pub label: Option<Arc<str>>,
327 pub gap: u16,
328 pub leading: u16,
330 pub style: Style,
331 pub label_style: Style,
332}
333
334impl ListItemGutter {
335 pub fn text(text: impl Into<Arc<str>>) -> Self {
337 Self::from_spans([Span::new(text)])
338 }
339
340 pub fn from_spans(spans: impl IntoIterator<Item = Span>) -> Self {
342 Self {
343 kind: ListItemGutterKind::Text(spans.into_iter().collect()),
344 }
345 }
346
347 pub fn spinner(spinner: Spinner) -> Self {
349 spinner.into()
350 }
351
352 pub fn leading(mut self, leading: u16) -> Self {
356 if let ListItemGutterKind::Spinner(spinner) = &mut self.kind {
357 spinner.leading = leading;
358 }
359 self
360 }
361
362 pub(crate) fn width(&self) -> u16 {
363 match &self.kind {
364 ListItemGutterKind::Text(spans) => spans
365 .iter()
366 .map(|span| UnicodeWidthStr::width(span.content.as_ref()))
367 .sum::<usize>()
368 .min(u16::MAX as usize) as u16,
369 ListItemGutterKind::Spinner(spinner) => {
370 let label_width = spinner
371 .label
372 .as_ref()
373 .map(|label| UnicodeWidthStr::width(label.as_ref()) as u16)
374 .unwrap_or(0);
375 let gap = if label_width > 0 { spinner.gap } else { 0 };
376 spinner
377 .leading
378 .saturating_add(spinner.spinner_style.width())
379 .saturating_add(gap)
380 .saturating_add(label_width)
381 }
382 }
383 }
384
385 pub(crate) fn has_spinner(&self) -> bool {
386 matches!(self.kind, ListItemGutterKind::Spinner(_))
387 }
388
389 pub(crate) fn spinner_mut(&mut self) -> Option<&mut ListItemSpinnerGutter> {
390 match &mut self.kind {
391 ListItemGutterKind::Spinner(spinner) => Some(spinner),
392 ListItemGutterKind::Text(_) => None,
393 }
394 }
395}
396
397impl From<Spinner> for ListItemGutter {
398 fn from(spinner: Spinner) -> Self {
399 Self {
400 kind: ListItemGutterKind::Spinner(ListItemSpinnerGutter {
401 spinner_style: spinner.spinner_style,
402 speed: spinner.speed,
403 frame: spinner.frame.unwrap_or(0),
404 auto_frame: spinner.frame.is_none(),
405 label: spinner.label,
406 gap: spinner.gap,
407 leading: 0,
408 style: spinner.style,
409 label_style: spinner.label_style,
410 }),
411 }
412 }
413}
414
415impl ListItemStatus {
416 pub fn text(text: impl Into<Arc<str>>) -> Self {
418 Self::from_spans([Span::new(text)])
419 }
420
421 pub fn from_spans(spans: impl IntoIterator<Item = Span>) -> Self {
423 Self {
424 kind: ListItemStatusKind::Text(spans.into_iter().collect()),
425 }
426 }
427
428 pub fn spinner(spinner: Spinner) -> Self {
430 spinner.into()
431 }
432
433 pub(crate) fn width(&self) -> u16 {
434 match &self.kind {
435 ListItemStatusKind::Text(spans) => spans
436 .iter()
437 .map(|span| UnicodeWidthStr::width(span.content.as_ref()))
438 .sum::<usize>()
439 .min(u16::MAX as usize) as u16,
440 ListItemStatusKind::Spinner(spinner) => spinner.spinner_style.width(),
441 }
442 }
443
444 pub(crate) fn has_spinner(&self) -> bool {
445 matches!(self.kind, ListItemStatusKind::Spinner(_))
446 }
447
448 pub(crate) fn spinner_mut(&mut self) -> Option<&mut ListItemSpinnerGutter> {
449 match &mut self.kind {
450 ListItemStatusKind::Spinner(spinner) => Some(spinner),
451 ListItemStatusKind::Text(_) => None,
452 }
453 }
454}
455
456impl From<Spinner> for ListItemStatus {
457 fn from(spinner: Spinner) -> Self {
458 Self {
459 kind: ListItemStatusKind::Spinner(ListItemSpinnerGutter {
460 spinner_style: spinner.spinner_style,
461 speed: spinner.speed,
462 frame: spinner.frame.unwrap_or(0),
463 auto_frame: spinner.frame.is_none(),
464 label: None,
465 gap: 0,
466 leading: 0,
467 style: spinner.style,
468 label_style: spinner.label_style,
469 }),
470 }
471 }
472}
473
474#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct ListItem {
477 pub(crate) spans: Vec<Span>,
478 pub(crate) description_spans: Vec<Span>,
479 pub(crate) extra_lines: Vec<ListItemLine>,
480 pub(crate) status: Option<ListItemStatus>,
481 pub(crate) gutter: Option<ListItemGutter>,
482 pub(crate) gutter_line: usize,
483 pub(crate) prefix: Option<Arc<str>>,
484 pub(crate) prefix_kind: ListItemPrefixKind,
485 pub(crate) prefix_style: Option<Style>,
486 pub(crate) extra_line_indent: u16,
487 pub(crate) style: Style,
488 pub(crate) role: ListItemRole,
489 pub(crate) active: bool,
490 pub(crate) primary_selection_label: bool,
491 pub(crate) primary_selection_description: bool,
492 pub(crate) primary_hover_label: bool,
493 pub(crate) primary_hover_description: bool,
494 pub(crate) primary_truncate_description_first: bool,
495 pub(crate) primary_wrap_label: bool,
496 pub(crate) primary_wrap_description: bool,
497 pub(crate) primary_max_label_width: Option<u16>,
498 pub(crate) primary_max_description_width: Option<u16>,
499 pub(crate) symbol_line: usize,
500}
501
502#[derive(Clone, Debug, PartialEq, Eq)]
504pub struct ListItemLine {
505 pub(crate) spans: Vec<Span>,
506 pub(crate) description_spans: Vec<Span>,
507 pub(crate) style: Style,
508 pub(crate) selection_label: bool,
509 pub(crate) selection_description: bool,
510 pub(crate) hover_label: bool,
511 pub(crate) hover_description: bool,
512 pub(crate) truncate_description_first: bool,
513 pub(crate) wrap_label: bool,
514 pub(crate) wrap_description: bool,
515 pub(crate) max_label_width: Option<u16>,
516 pub(crate) max_description_width: Option<u16>,
517}
518
519impl ListItemLine {
520 pub fn new(content: impl Into<Arc<str>>) -> Self {
522 Self {
523 spans: vec![Span::new(content)],
524 description_spans: Vec::new(),
525 style: Style::default(),
526 selection_label: true,
527 selection_description: true,
528 hover_label: true,
529 hover_description: true,
530 truncate_description_first: false,
531 wrap_label: false,
532 wrap_description: false,
533 max_label_width: None,
534 max_description_width: None,
535 }
536 }
537
538 pub fn from_spans(spans: impl IntoIterator<Item = Span>) -> Self {
540 Self {
541 spans: spans.into_iter().collect(),
542 description_spans: Vec::new(),
543 style: Style::default(),
544 selection_label: true,
545 selection_description: true,
546 hover_label: true,
547 hover_description: true,
548 truncate_description_first: false,
549 wrap_label: false,
550 wrap_description: false,
551 max_label_width: None,
552 max_description_width: None,
553 }
554 }
555
556 pub fn description_spans(mut self, spans: impl IntoIterator<Item = Span>) -> Self {
558 self.description_spans = spans.into_iter().collect();
559 self
560 }
561
562 pub fn description(mut self, text: impl Into<Arc<str>>) -> Self {
564 self.description_spans = vec![Span::new(text)];
565 self
566 }
567
568 pub fn description_style(mut self, style: Style) -> Self {
570 for span in &mut self.description_spans {
571 span.style = style;
572 }
573 self
574 }
575
576 pub fn style(mut self, style: Style) -> Self {
578 self.style = style;
579 self
580 }
581
582 pub fn selection_label(mut self, highlight: bool) -> Self {
584 self.selection_label = highlight;
585 self
586 }
587
588 pub fn selection_description(mut self, highlight: bool) -> Self {
590 self.selection_description = highlight;
591 self
592 }
593
594 pub fn hover_label(mut self, hover: bool) -> Self {
596 self.hover_label = hover;
597 self
598 }
599
600 pub fn hover_description(mut self, hover: bool) -> Self {
602 self.hover_description = hover;
603 self
604 }
605
606 pub fn truncate_description_first(mut self, truncate: bool) -> Self {
608 self.truncate_description_first = truncate;
609 self
610 }
611
612 pub fn wrap_label(mut self, wrap: bool) -> Self {
614 self.wrap_label = wrap;
615 self
616 }
617
618 pub fn wrap_description(mut self, wrap: bool) -> Self {
620 self.wrap_description = wrap;
621 self
622 }
623
624 pub fn max_label_width(mut self, width: u16) -> Self {
626 self.max_label_width = Some(width);
627 self
628 }
629
630 pub fn max_description_width(mut self, width: u16) -> Self {
632 self.max_description_width = Some(width);
633 self
634 }
635}
636
637impl ListItem {
638 pub fn new(content: impl Into<Arc<str>>) -> Self {
640 Self {
641 spans: vec![Span::new(content)],
642 description_spans: Vec::new(),
643 extra_lines: Vec::new(),
644 status: None,
645 gutter: None,
646 gutter_line: 0,
647 prefix: None,
648 prefix_kind: ListItemPrefixKind::Plain,
649 prefix_style: None,
650 extra_line_indent: 0,
651 style: Style::default(),
652 role: ListItemRole::Normal,
653 active: false,
654 primary_selection_label: true,
655 primary_selection_description: true,
656 primary_hover_label: true,
657 primary_hover_description: true,
658 primary_truncate_description_first: false,
659 primary_wrap_label: false,
660 primary_wrap_description: false,
661 primary_max_label_width: None,
662 primary_max_description_width: None,
663 symbol_line: 0,
664 }
665 }
666
667 pub fn header(content: impl Into<Arc<str>>) -> Self {
669 Self::new(content)
670 .role(ListItemRole::Header)
671 .style(Style::default())
672 }
673
674 pub fn spacer() -> Self {
676 Self {
677 spans: vec![Span::new("")],
678 description_spans: Vec::new(),
679 extra_lines: Vec::new(),
680 status: None,
681 gutter: None,
682 gutter_line: 0,
683 prefix: None,
684 prefix_kind: ListItemPrefixKind::Plain,
685 prefix_style: None,
686 extra_line_indent: 0,
687 style: Style::default(),
688 role: ListItemRole::Spacer,
689 active: false,
690 primary_selection_label: true,
691 primary_selection_description: true,
692 primary_hover_label: true,
693 primary_hover_description: true,
694 primary_truncate_description_first: false,
695 primary_wrap_label: false,
696 primary_wrap_description: false,
697 primary_max_label_width: None,
698 primary_max_description_width: None,
699 symbol_line: 0,
700 }
701 }
702
703 pub fn from_spans(spans: impl IntoIterator<Item = Span>) -> Self {
705 Self {
706 spans: spans.into_iter().collect(),
707 description_spans: Vec::new(),
708 extra_lines: Vec::new(),
709 status: None,
710 gutter: None,
711 gutter_line: 0,
712 prefix: None,
713 prefix_kind: ListItemPrefixKind::Plain,
714 prefix_style: None,
715 extra_line_indent: 0,
716 style: Style::default(),
717 role: ListItemRole::Normal,
718 active: false,
719 primary_selection_label: true,
720 primary_selection_description: true,
721 primary_hover_label: true,
722 primary_hover_description: true,
723 primary_truncate_description_first: false,
724 primary_wrap_label: false,
725 primary_wrap_description: false,
726 primary_max_label_width: None,
727 primary_max_description_width: None,
728 symbol_line: 0,
729 }
730 }
731
732 pub fn description_spans(mut self, spans: impl IntoIterator<Item = Span>) -> Self {
734 self.description_spans = spans.into_iter().collect();
735 self
736 }
737
738 pub fn description(mut self, text: impl Into<Arc<str>>) -> Self {
740 self.description_spans = vec![Span::new(text)];
741 self
742 }
743
744 pub fn description_style(mut self, style: Style) -> Self {
746 for span in &mut self.description_spans {
747 span.style = style;
748 }
749 self
750 }
751
752 pub fn line(mut self, line: impl Into<ListItemLine>) -> Self {
754 self.extra_lines.push(line.into());
755 self
756 }
757
758 pub fn lines(mut self, lines: impl IntoIterator<Item = ListItemLine>) -> Self {
760 self.extra_lines = lines.into_iter().collect();
761 self
762 }
763
764 pub fn gutter(mut self, gutter: impl Into<ListItemGutter>) -> Self {
766 self.gutter = Some(gutter.into());
767 self
768 }
769
770 pub fn status(mut self, status: impl Into<ListItemStatus>) -> Self {
776 self.status = Some(status.into());
777 self
778 }
779
780 pub fn status_symbol(self, symbol: impl Into<Arc<str>>) -> Self {
782 self.status(ListItemStatus::text(symbol))
783 }
784
785 pub fn status_spinner(self, spinner: Spinner) -> Self {
787 self.status(ListItemStatus::spinner(spinner))
788 }
789
790 pub fn gutter_line(mut self, line: usize) -> Self {
794 self.gutter_line = line;
795 self
796 }
797
798 pub fn prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
800 let prefix = prefix.into();
801 self.extra_line_indent =
802 UnicodeWidthStr::width(prefix.as_ref()).min(u16::MAX as usize) as u16;
803 self.prefix = Some(prefix);
804 self.prefix_kind = ListItemPrefixKind::Plain;
805 self
806 }
807
808 pub fn prefix_style(mut self, style: Style) -> Self {
810 self.prefix_style = Some(style);
811 self
812 }
813
814 pub fn numbered(mut self, n: usize) -> Self {
816 let prefix = format!("{n}. ");
817 self.extra_line_indent =
818 UnicodeWidthStr::width(prefix.as_str()).min(u16::MAX as usize) as u16;
819 self.prefix = Some(prefix.into());
820 self.prefix_kind = ListItemPrefixKind::Numbered(n);
821 self
822 }
823
824 pub fn bulleted(self, ch: char) -> Self {
826 self.prefix(format!("{ch} "))
827 }
828
829 pub fn extra_line_indent(mut self, indent: u16) -> Self {
831 self.extra_line_indent = indent;
832 self
833 }
834
835 pub fn primary_selection_label(mut self, highlight: bool) -> Self {
837 self.primary_selection_label = highlight;
838 self
839 }
840
841 pub fn primary_selection_description(mut self, highlight: bool) -> Self {
843 self.primary_selection_description = highlight;
844 self
845 }
846
847 pub fn primary_hover_label(mut self, hover: bool) -> Self {
849 self.primary_hover_label = hover;
850 self
851 }
852
853 pub fn primary_hover_description(mut self, hover: bool) -> Self {
855 self.primary_hover_description = hover;
856 self
857 }
858
859 pub fn primary_truncate_description_first(mut self, truncate: bool) -> Self {
861 self.primary_truncate_description_first = truncate;
862 self
863 }
864
865 pub fn primary_wrap_label(mut self, wrap: bool) -> Self {
867 self.primary_wrap_label = wrap;
868 self
869 }
870
871 pub fn primary_wrap_description(mut self, wrap: bool) -> Self {
873 self.primary_wrap_description = wrap;
874 self
875 }
876
877 pub fn primary_max_label_width(mut self, width: u16) -> Self {
879 self.primary_max_label_width = Some(width);
880 self
881 }
882
883 pub fn primary_max_description_width(mut self, width: u16) -> Self {
885 self.primary_max_description_width = Some(width);
886 self
887 }
888
889 pub fn symbol_line(mut self, line: usize) -> Self {
893 self.symbol_line = line;
894 self
895 }
896
897 pub fn style(mut self, style: Style) -> Self {
899 self.style = style;
900 self
901 }
902
903 pub fn role(mut self, role: ListItemRole) -> Self {
905 self.role = role;
906 self
907 }
908
909 pub fn active(mut self, active: bool) -> Self {
911 self.active = active;
912 self
913 }
914
915 pub fn is_selectable(&self) -> bool {
917 matches!(self.role, ListItemRole::Normal)
918 }
919
920 pub fn is_active(&self) -> bool {
922 self.active
923 }
924
925 pub fn plain_content(&self) -> String {
927 let mut s = String::new();
928 for span in &self.spans {
929 s.push_str(&span.content);
930 }
931 for line in &self.extra_lines {
932 s.push('\n');
933 for span in &line.spans {
934 s.push_str(&span.content);
935 }
936 }
937 s
938 }
939
940 pub(crate) fn line_count(&self) -> usize {
941 1 + self.extra_lines.len()
942 }
943}
944
945impl From<&'static str> for ListItemLine {
946 fn from(value: &'static str) -> Self {
947 Self::new(value)
948 }
949}
950
951impl From<String> for ListItemLine {
952 fn from(value: String) -> Self {
953 Self::new(value)
954 }
955}
956
957impl From<Arc<str>> for ListItemLine {
958 fn from(value: Arc<str>) -> Self {
959 Self::new(value)
960 }
961}
962
963impl From<&'static str> for ListItem {
964 fn from(value: &'static str) -> Self {
965 Self::new(value)
966 }
967}
968
969impl From<String> for ListItem {
970 fn from(value: String) -> Self {
971 Self::new(value)
972 }
973}
974
975impl From<Arc<str>> for ListItem {
976 fn from(value: Arc<str>) -> Self {
977 Self::new(value)
978 }
979}
980
981pub(crate) fn reserved_symbol_width_for_items(
982 items: &[ListItem],
983 symbol_column: bool,
984 active_symbol_position: ListSymbolPosition,
985 active_symbol: Option<&str>,
986 selection_symbol: Option<&str>,
987 unselected_symbol: Option<&str>,
988) -> u16 {
989 if !symbol_column {
990 return 0;
991 }
992
993 let status_width = items
994 .iter()
995 .filter(|item| item.is_selectable())
996 .filter_map(|item| item.status.as_ref())
997 .map(ListItemStatus::width)
998 .max()
999 .unwrap_or(0) as usize;
1000
1001 selection_symbol
1002 .map(UnicodeWidthStr::width)
1003 .unwrap_or(0)
1004 .max(
1005 if matches!(active_symbol_position, ListSymbolPosition::Left) {
1006 active_symbol.map(UnicodeWidthStr::width).unwrap_or(0)
1007 } else {
1008 0
1009 },
1010 )
1011 .max(unselected_symbol.map(UnicodeWidthStr::width).unwrap_or(0))
1012 .max(status_width)
1013 .min(u16::MAX as usize) as u16
1014}
1015
1016pub(crate) fn reserved_symbol_width(list: &List) -> u16 {
1017 reserved_symbol_width_for_items(
1018 &list.items,
1019 list.symbol_column,
1020 list.active_symbol_position,
1021 list.active_symbol.as_deref(),
1022 list.selection_symbol.as_deref(),
1023 list.unselected_symbol.as_deref(),
1024 )
1025}
1026
1027pub(crate) struct ListSymbolWidthCtx<'a> {
1028 pub active_symbol_position: ListSymbolPosition,
1029 pub active_symbol: Option<&'a str>,
1030 pub selection_symbol: Option<&'a str>,
1031 pub unselected_symbol: Option<&'a str>,
1032}
1033
1034pub(crate) fn item_symbol_width_for_reserved(
1035 reserved: u16,
1036 item: &ListItem,
1037 is_selected: bool,
1038 ctx: ListSymbolWidthCtx<'_>,
1039) -> u16 {
1040 let ListSymbolWidthCtx {
1041 active_symbol_position,
1042 active_symbol,
1043 selection_symbol,
1044 unselected_symbol,
1045 } = ctx;
1046 if reserved == 0 || !item.is_selectable() {
1047 return 0;
1048 }
1049
1050 if matches!(active_symbol_position, ListSymbolPosition::Left)
1051 && item.is_active()
1052 && let Some(symbol) = active_symbol
1053 {
1054 return UnicodeWidthStr::width(symbol).min(u16::MAX as usize) as u16;
1055 }
1056
1057 if item.status.is_some() {
1058 return reserved;
1059 }
1060
1061 if is_selected {
1062 return selection_symbol
1063 .map(|symbol| UnicodeWidthStr::width(symbol).min(u16::MAX as usize) as u16)
1064 .unwrap_or(reserved);
1065 }
1066
1067 unselected_symbol
1068 .map(|symbol| UnicodeWidthStr::width(symbol).min(u16::MAX as usize) as u16)
1069 .unwrap_or(reserved)
1070}
1071
1072pub(crate) fn item_symbol_width(list: &List, item: &ListItem, is_selected: bool) -> u16 {
1073 item_symbol_width_for_reserved(
1074 reserved_symbol_width(list),
1075 item,
1076 is_selected,
1077 ListSymbolWidthCtx {
1078 active_symbol_position: list.active_symbol_position,
1079 active_symbol: list.active_symbol.as_deref(),
1080 selection_symbol: list.selection_symbol.as_deref(),
1081 unselected_symbol: list.unselected_symbol.as_deref(),
1082 },
1083 )
1084}
1085
1086pub(crate) fn item_active_right_symbol_width(list: &List, item: &ListItem) -> u16 {
1087 if matches!(list.active_symbol_position, ListSymbolPosition::Right)
1088 && item.is_active()
1089 && let Some(symbol) = list.active_symbol.as_deref()
1090 {
1091 UnicodeWidthStr::width(symbol).min(u16::MAX as usize) as u16
1092 } else {
1093 0
1094 }
1095}
1096
1097pub(crate) fn item_selection_right_symbol_width(
1104 list: &List,
1105 item: &ListItem,
1106 is_selected: bool,
1107) -> u16 {
1108 if is_selected
1109 && item.is_selectable()
1110 && item_active_right_symbol_width(list, item) == 0
1111 && let Some(symbol) = list.selection_symbol_right.as_deref()
1112 {
1113 UnicodeWidthStr::width(symbol).min(u16::MAX as usize) as u16
1114 } else {
1115 0
1116 }
1117}
1118
1119pub(crate) fn item_uses_gutter(item: &ListItem, gutter_for_non_selectable: bool) -> bool {
1120 item.is_selectable() || gutter_for_non_selectable
1121}
1122
1123pub(crate) fn reserved_gutter_width_for_items(
1124 items: &[ListItem],
1125 gutter_gap: u16,
1126 gutter_for_non_selectable: bool,
1127) -> u16 {
1128 let width = items
1129 .iter()
1130 .filter(|item| item_uses_gutter(item, gutter_for_non_selectable))
1131 .filter_map(|item| item.gutter.as_ref())
1132 .map(ListItemGutter::width)
1133 .max()
1134 .unwrap_or(0);
1135 if width > 0 {
1136 width.saturating_add(gutter_gap)
1137 } else {
1138 0
1139 }
1140}
1141
1142pub(crate) fn reserved_gutter_width(list: &List) -> u16 {
1143 reserved_gutter_width_for_items(&list.items, list.gutter_gap, list.gutter_for_non_selectable)
1144}
1145
1146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1147pub(crate) struct ListLeadingMetrics {
1148 pub symbol_width: u16,
1149 pub gutter_width: u16,
1150 pub active_right_symbol_width: u16,
1151 pub selection_right_symbol_width: u16,
1152}
1153
1154pub(crate) fn leading_metrics(
1155 list: &List,
1156 item: &ListItem,
1157 is_selected: bool,
1158) -> ListLeadingMetrics {
1159 ListLeadingMetrics {
1160 symbol_width: item_symbol_width(list, item, is_selected),
1161 gutter_width: if item_uses_gutter(item, list.gutter_for_non_selectable) {
1162 reserved_gutter_width(list)
1163 } else {
1164 0
1165 },
1166 active_right_symbol_width: item_active_right_symbol_width(list, item),
1167 selection_right_symbol_width: item_selection_right_symbol_width(list, item, is_selected),
1168 }
1169}
1170
1171pub(crate) fn max_numbered_prefix_width(list: &List) -> u16 {
1172 list.items
1173 .iter()
1174 .filter_map(|item| match item.prefix_kind {
1175 ListItemPrefixKind::Numbered(n) => Some(n),
1176 ListItemPrefixKind::Plain => None,
1177 })
1178 .map(|n| UnicodeWidthStr::width(format!("{n}. ").as_str()))
1179 .max()
1180 .unwrap_or(0)
1181 .min(u16::MAX as usize) as u16
1182}
1183
1184pub(crate) fn max_numbered_prefix_width_for_items(items: &[ListItem]) -> u16 {
1185 items
1186 .iter()
1187 .filter_map(|item| match item.prefix_kind {
1188 ListItemPrefixKind::Numbered(n) => Some(n),
1189 ListItemPrefixKind::Plain => None,
1190 })
1191 .map(|n| UnicodeWidthStr::width(format!("{n}. ").as_str()))
1192 .max()
1193 .unwrap_or(0)
1194 .min(u16::MAX as usize) as u16
1195}
1196
1197pub(crate) fn effective_prefix_for_width<'a>(
1198 item: &'a ListItem,
1199 numbered_prefix_width: u16,
1200) -> Option<std::borrow::Cow<'a, str>> {
1201 match (&item.prefix, &item.prefix_kind) {
1202 (Some(prefix), ListItemPrefixKind::Numbered(n)) => {
1203 let target_width = numbered_prefix_width as usize;
1204 let text = format!("{n}. ");
1205 let width = UnicodeWidthStr::width(text.as_str());
1206 if target_width > width {
1207 Some(std::borrow::Cow::Owned(format!(
1208 "{}{}",
1209 " ".repeat(target_width - width),
1210 text
1211 )))
1212 } else {
1213 Some(std::borrow::Cow::Borrowed(prefix.as_ref()))
1214 }
1215 }
1216 (Some(prefix), ListItemPrefixKind::Plain) => {
1217 Some(std::borrow::Cow::Borrowed(prefix.as_ref()))
1218 }
1219 (None, _) => None,
1220 }
1221}
1222
1223pub(crate) fn effective_extra_line_indent_for_width(
1224 item: &ListItem,
1225 numbered_prefix_width: u16,
1226) -> u16 {
1227 match item.prefix_kind {
1228 ListItemPrefixKind::Numbered(_) => numbered_prefix_width,
1229 ListItemPrefixKind::Plain => item.extra_line_indent,
1230 }
1231}
1232
1233pub(crate) fn effective_prefix<'a>(
1234 list: &List,
1235 item: &'a ListItem,
1236) -> Option<std::borrow::Cow<'a, str>> {
1237 effective_prefix_for_width(item, max_numbered_prefix_width(list))
1238}
1239
1240pub(crate) fn effective_extra_line_indent(list: &List, item: &ListItem) -> u16 {
1241 effective_extra_line_indent_for_width(item, max_numbered_prefix_width(list))
1242}
1243
1244#[derive(Clone)]
1246pub struct List {
1247 pub(crate) items: Arc<[ListItem]>,
1248 pub(crate) selected: Option<usize>,
1249 pub(crate) scroll_keys: ScrollKeymap,
1250 pub(crate) scroll_wheel: bool,
1251 pub(crate) style: Style,
1252 pub(crate) hover_style: StyleSlot,
1253 pub(crate) item_hover_style: StyleSlot,
1254 pub(crate) active_style: StyleSlot,
1255 pub(crate) selection_style: StyleSlot,
1256 pub(crate) unfocused_selection_style: StyleSlot,
1257 pub(crate) active_symbol: Option<Arc<str>>,
1258 pub(crate) active_symbol_position: ListSymbolPosition,
1259 pub(crate) active_symbol_style: Option<Style>,
1260 pub(crate) selection_symbol: Option<Arc<str>>,
1261 pub(crate) selection_symbol_right: Option<Arc<str>>,
1262 pub(crate) selection_symbol_style: Option<Style>,
1263 pub(crate) unfocused_selection_symbol_style: Option<Style>,
1264 pub(crate) unselected_symbol: Option<Arc<str>>,
1265 pub(crate) symbol_column: bool,
1266 pub(crate) gutter_gap: u16,
1267 pub(crate) gutter_for_non_selectable: bool,
1268 pub(crate) selection_full_width: bool,
1269 pub(crate) item_horizontal_padding: Padding,
1270 pub(crate) header_horizontal_padding: Padding,
1271 pub(crate) border: bool,
1272 pub(crate) border_style: BorderStyle,
1273 pub(crate) title: Option<Arc<str>>,
1274 pub(crate) title_style: Style,
1275 pub(crate) padding: Padding,
1276 pub(crate) scrollbar: bool,
1277 pub(crate) scrollbar_config: ScrollbarConfig,
1278 pub(crate) width: Length,
1279 pub(crate) height: Length,
1280 pub(crate) on_select: Option<Callback<ListEvent>>,
1281 pub(crate) on_item_click: Option<Callback<ListEvent>>,
1282 pub(crate) on_activate: Option<Callback<ListEvent>>,
1283 pub(crate) on_click: Option<Callback<MouseEvent>>,
1284 pub(crate) activate_on_click: bool,
1285 pub(crate) on_scroll_to: Option<Callback<usize>>,
1286 pub(crate) on_key: Option<KeyHandler>,
1287 pub(crate) disabled: bool,
1288 pub(crate) disabled_style: Style,
1289 pub(crate) focusable: bool,
1290 pub(crate) tab_stop: bool,
1291 pub(crate) on_focus: Option<Callback<()>>,
1292 pub(crate) on_blur: Option<Callback<()>>,
1293 pub(crate) show_scroll_indicators: bool,
1294 pub(crate) scroll_indicator_style: Style,
1295 pub(crate) empty_text: Option<Arc<str>>,
1296 pub(crate) empty_text_style: Style,
1297 pub(crate) force_scroll_to_selected: bool,
1298}
1299
1300impl Default for List {
1301 fn default() -> Self {
1302 Self {
1303 items: Arc::new([]),
1304 selected: Some(0),
1305 scroll_keys: ScrollKeymap::default(),
1306 scroll_wheel: true,
1307 style: Style::default(),
1308 hover_style: StyleSlot::Inherit,
1309 item_hover_style: StyleSlot::Inherit,
1310 active_style: StyleSlot::Inherit,
1311 selection_style: StyleSlot::Inherit,
1312 unfocused_selection_style: StyleSlot::Inherit,
1313 active_symbol: None,
1314 active_symbol_position: ListSymbolPosition::Left,
1315 active_symbol_style: None,
1316 selection_symbol: None,
1317 selection_symbol_right: None,
1318 selection_symbol_style: None,
1319 unfocused_selection_symbol_style: None,
1320 unselected_symbol: None,
1321 symbol_column: true,
1322 gutter_gap: 0,
1323 gutter_for_non_selectable: false,
1324 selection_full_width: false,
1325 item_horizontal_padding: Padding::default(),
1326 header_horizontal_padding: Padding::default(),
1327 border: false,
1328 border_style: BorderStyle::Plain,
1329 title: None,
1330 title_style: Style::default(),
1331 padding: Padding::default(),
1332 scrollbar: false,
1333 scrollbar_config: ScrollbarConfig::default(),
1334 width: Length::Flex(1),
1335 height: Length::Flex(1),
1336 on_select: None,
1337 on_item_click: None,
1338 on_activate: None,
1339 on_click: None,
1340 activate_on_click: true,
1341 on_scroll_to: None,
1342 on_key: None,
1343 disabled: false,
1344 disabled_style: Style::default(),
1345 focusable: true,
1346 tab_stop: true,
1347 on_focus: None,
1348 on_blur: None,
1349 show_scroll_indicators: false,
1350 scroll_indicator_style: Style::default(),
1351 empty_text: None,
1352 empty_text_style: Style::default(),
1353 force_scroll_to_selected: false,
1354 }
1355 }
1356}
1357
1358impl List {
1359 pub fn new() -> Self {
1361 Self::default()
1362 }
1363
1364 pub fn items<I>(mut self, items: I) -> Self
1366 where
1367 I: IntoIterator<Item = ListItem>,
1368 {
1369 self.items = items.into_iter().collect();
1370 self
1371 }
1372
1373 pub fn item(mut self, item: impl Into<ListItem>) -> Self {
1378 let mut items = self.items.to_vec();
1379 items.push(item.into());
1380 self.items = items.into();
1381 self
1382 }
1383
1384 pub fn items_arc(mut self, items: Arc<[ListItem]>) -> Self {
1388 self.items = items;
1389 self
1390 }
1391
1392 pub fn selected(mut self, selected: impl Into<Option<usize>>) -> Self {
1397 self.selected = selected.into();
1398 self
1399 }
1400
1401 pub fn force_scroll_to_selected(mut self, force: bool) -> Self {
1408 self.force_scroll_to_selected = force;
1409 self
1410 }
1411
1412 pub fn scroll_keys(mut self, keys: ScrollKeymap) -> Self {
1414 self.scroll_keys = keys;
1415 self
1416 }
1417
1418 pub fn scroll_wheel(mut self, enabled: bool) -> Self {
1420 self.scroll_wheel = enabled;
1421 self
1422 }
1423
1424 pub fn style(mut self, style: Style) -> Self {
1426 self.style = style;
1427 self
1428 }
1429
1430 pub fn hover_style(mut self, style: Style) -> Self {
1432 self.hover_style = StyleSlot::Replace(style);
1433 self
1434 }
1435
1436 pub fn extend_hover_style(mut self, style: Style) -> Self {
1438 self.hover_style = StyleSlot::Extend(style);
1439 self
1440 }
1441
1442 pub fn inherit_hover_style(mut self) -> Self {
1444 self.hover_style = StyleSlot::Inherit;
1445 self
1446 }
1447
1448 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
1450 self.hover_style = slot;
1451 self
1452 }
1453
1454 pub fn item_hover_style(mut self, style: Style) -> Self {
1456 self.item_hover_style = StyleSlot::Replace(style);
1457 self
1458 }
1459
1460 pub fn extend_item_hover_style(mut self, style: Style) -> Self {
1462 self.item_hover_style = StyleSlot::Extend(style);
1463 self
1464 }
1465
1466 pub fn inherit_item_hover_style(mut self) -> Self {
1468 self.item_hover_style = StyleSlot::Inherit;
1469 self
1470 }
1471
1472 pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
1474 self.item_hover_style = slot;
1475 self
1476 }
1477
1478 pub fn active_style(mut self, style: Style) -> Self {
1480 self.active_style = StyleSlot::Replace(style);
1481 self
1482 }
1483
1484 pub fn extend_active_style(mut self, style: Style) -> Self {
1486 self.active_style = StyleSlot::Extend(style);
1487 self
1488 }
1489
1490 pub fn inherit_active_style(mut self) -> Self {
1492 self.active_style = StyleSlot::Inherit;
1493 self
1494 }
1495
1496 pub fn active_style_slot(mut self, slot: StyleSlot) -> Self {
1498 self.active_style = slot;
1499 self
1500 }
1501
1502 pub fn selection_style(mut self, style: Style) -> Self {
1504 self.selection_style = StyleSlot::Replace(style);
1505 self
1506 }
1507
1508 pub fn extend_selection_style(mut self, style: Style) -> Self {
1510 self.selection_style = StyleSlot::Extend(style);
1511 self
1512 }
1513
1514 pub fn inherit_selection_style(mut self) -> Self {
1516 self.selection_style = StyleSlot::Inherit;
1517 self
1518 }
1519
1520 pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
1522 self.selection_style = slot;
1523 self
1524 }
1525
1526 pub fn unfocused_selection_style(mut self, style: Style) -> Self {
1530 self.unfocused_selection_style = StyleSlot::Replace(style);
1531 self
1532 }
1533
1534 pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
1536 self.unfocused_selection_style = StyleSlot::Extend(style);
1537 self
1538 }
1539
1540 pub fn inherit_unfocused_selection_style(mut self) -> Self {
1542 self.unfocused_selection_style = StyleSlot::Inherit;
1543 self
1544 }
1545
1546 pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
1548 self.unfocused_selection_style = slot;
1549 self
1550 }
1551
1552 pub fn selection_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
1564 self.selection_symbol = symbol.map(Into::into);
1565 self
1566 }
1567
1568 pub fn selection_symbol_right(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
1586 self.selection_symbol_right = symbol.map(Into::into);
1587 self
1588 }
1589
1590 pub fn active_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
1597 self.active_symbol = symbol.map(Into::into);
1598 self
1599 }
1600
1601 pub fn active_symbol_position(mut self, position: ListSymbolPosition) -> Self {
1603 self.active_symbol_position = position;
1604 self
1605 }
1606
1607 pub fn active_symbol_style(mut self, style: Style) -> Self {
1611 self.active_symbol_style = Some(style);
1612 self
1613 }
1614
1615 pub fn selection_symbol_style(mut self, style: Style) -> Self {
1619 self.selection_symbol_style = Some(style);
1620 self
1621 }
1622
1623 pub fn unfocused_selection_symbol_style(mut self, style: Style) -> Self {
1627 self.unfocused_selection_symbol_style = Some(style);
1628 self
1629 }
1630
1631 pub fn unselected_symbol(mut self, symbol: Option<impl Into<Arc<str>>>) -> Self {
1637 self.unselected_symbol = symbol.map(Into::into);
1638 self
1639 }
1640
1641 pub fn symbol_column(mut self, enabled: bool) -> Self {
1645 self.symbol_column = enabled;
1646 self
1647 }
1648
1649 pub fn gutter_gap(mut self, gap: u16) -> Self {
1651 self.gutter_gap = gap;
1652 self
1653 }
1654
1655 pub fn gutter_for_non_selectable(mut self, enabled: bool) -> Self {
1657 self.gutter_for_non_selectable = enabled;
1658 self
1659 }
1660
1661 pub fn selection_full_width(mut self, full_width: bool) -> Self {
1663 self.selection_full_width = full_width;
1664 self
1665 }
1666
1667 pub fn item_horizontal_padding(mut self, padding: impl Into<Padding>) -> Self {
1671 self.item_horizontal_padding = padding.into();
1672 self
1673 }
1674
1675 pub fn header_horizontal_padding(mut self, padding: impl Into<Padding>) -> Self {
1679 self.header_horizontal_padding = padding.into();
1680 self
1681 }
1682
1683 pub fn border(mut self, border: bool) -> Self {
1685 self.border = border;
1686 self
1687 }
1688
1689 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
1691 self.border_style = border_style;
1692 self
1693 }
1694
1695 pub fn title(mut self, title: impl Into<Arc<str>>) -> Self {
1697 self.title = Some(title.into());
1698 self
1699 }
1700
1701 pub fn title_style(mut self, style: Style) -> Self {
1703 self.title_style = style;
1704 self
1705 }
1706
1707 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
1709 self.padding = padding.into();
1710 self
1711 }
1712
1713 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
1715 self.scrollbar = scrollbar;
1716 self
1717 }
1718
1719 pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
1721 self.scrollbar_config = config;
1722 self
1723 }
1724
1725 pub fn width(mut self, width: Length) -> Self {
1727 self.width = width;
1728 self
1729 }
1730
1731 pub fn height(mut self, height: Length) -> Self {
1733 self.height = height;
1734 self
1735 }
1736
1737 pub fn on_select(mut self, cb: Callback<ListEvent>) -> Self {
1739 self.on_select = Some(cb);
1740 self
1741 }
1742
1743 pub fn on_item_click(mut self, cb: Callback<ListEvent>) -> Self {
1745 self.on_item_click = Some(cb);
1746 self
1747 }
1748
1749 pub fn on_activate(mut self, cb: Callback<ListEvent>) -> Self {
1751 self.on_activate = Some(cb);
1752 self
1753 }
1754
1755 pub fn activate_on_click(mut self, activate: bool) -> Self {
1757 self.activate_on_click = activate;
1758 self
1759 }
1760
1761 pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
1763 self.on_click = Some(cb);
1764 self
1765 }
1766
1767 pub fn on_scroll_to(mut self, cb: Callback<usize>) -> Self {
1769 self.on_scroll_to = Some(cb);
1770 self
1771 }
1772
1773 pub fn on_key(mut self, handler: KeyHandler) -> Self {
1775 self.on_key = Some(handler);
1776 self
1777 }
1778
1779 pub fn disabled(mut self, disabled: bool) -> Self {
1781 self.disabled = disabled;
1782 self
1783 }
1784
1785 pub fn disabled_style(mut self, style: Style) -> Self {
1787 self.disabled_style = style;
1788 self
1789 }
1790
1791 pub fn focusable(mut self, focusable: bool) -> Self {
1793 self.focusable = focusable;
1794 self
1795 }
1796
1797 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
1799 self.tab_stop = tab_stop;
1800 self
1801 }
1802
1803 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
1805 self.on_focus = Some(cb);
1806 self
1807 }
1808
1809 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
1811 self.on_blur = Some(cb);
1812 self
1813 }
1814
1815 pub fn show_scroll_indicators(mut self, show: bool) -> Self {
1817 self.show_scroll_indicators = show;
1818 self
1819 }
1820
1821 pub fn scroll_indicator_style(mut self, style: Style) -> Self {
1823 self.scroll_indicator_style = style;
1824 self
1825 }
1826
1827 pub fn empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
1829 self.empty_text = Some(text.into());
1830 self
1831 }
1832
1833 pub fn empty_text_style(mut self, style: Style) -> Self {
1835 self.empty_text_style = style;
1836 self
1837 }
1838
1839 pub(crate) fn first_selectable_index(items: &[ListItem]) -> Option<usize> {
1840 items.iter().position(ListItem::is_selectable)
1841 }
1842
1843 pub(crate) fn last_selectable_index(items: &[ListItem]) -> Option<usize> {
1844 items.iter().rposition(ListItem::is_selectable)
1845 }
1846
1847 pub(crate) fn is_selectable_index(items: &[ListItem], index: usize) -> bool {
1848 items.get(index).is_some_and(ListItem::is_selectable)
1849 }
1850
1851 pub(crate) fn selectable_at_or_after(items: &[ListItem], start: usize) -> Option<usize> {
1852 if items.is_empty() {
1853 return None;
1854 }
1855
1856 let from = start.min(items.len().saturating_sub(1));
1857 items
1858 .iter()
1859 .enumerate()
1860 .skip(from)
1861 .find_map(|(idx, item)| item.is_selectable().then_some(idx))
1862 }
1863
1864 pub(crate) fn selectable_at_or_before(items: &[ListItem], start: usize) -> Option<usize> {
1865 if items.is_empty() {
1866 return None;
1867 }
1868
1869 let to = start.min(items.len().saturating_sub(1));
1870 items
1871 .iter()
1872 .enumerate()
1873 .take(to.saturating_add(1))
1874 .rfind(|(_, item)| item.is_selectable())
1875 .map(|(idx, _)| idx)
1876 }
1877
1878 pub(crate) fn nearest_selectable_index(items: &[ListItem], preferred: usize) -> Option<usize> {
1879 if items.is_empty() {
1880 return None;
1881 }
1882
1883 let preferred = preferred.min(items.len().saturating_sub(1));
1884 if Self::is_selectable_index(items, preferred) {
1885 return Some(preferred);
1886 }
1887
1888 let before = Self::selectable_at_or_before(items, preferred);
1889 let after = Self::selectable_at_or_after(items, preferred);
1890
1891 match (before, after) {
1892 (Some(left), Some(right)) => {
1893 if preferred.abs_diff(right) <= preferred.abs_diff(left) {
1894 Some(right)
1895 } else {
1896 Some(left)
1897 }
1898 }
1899 (Some(left), None) => Some(left),
1900 (None, Some(right)) => Some(right),
1901 (None, None) => None,
1902 }
1903 }
1904
1905 pub(crate) fn selection_for_action(
1906 selected: usize,
1907 items: &[ListItem],
1908 action: ScrollAction,
1909 ) -> Option<usize> {
1910 let len = items.len();
1911 if len == 0 {
1912 return None;
1913 }
1914
1915 let mut selected = Self::nearest_selectable_index(items, selected)?;
1916
1917 let next = match action {
1918 ScrollAction::LineUp(lines) => {
1919 for _ in 0..lines {
1920 if selected == 0 {
1921 break;
1922 }
1923
1924 let Some(prev) = Self::selectable_at_or_before(items, selected - 1) else {
1925 break;
1926 };
1927 selected = prev;
1928 }
1929 selected
1930 }
1931 ScrollAction::LineDown(lines) => {
1932 for _ in 0..lines {
1933 if selected.saturating_add(1) >= len {
1934 break;
1935 }
1936
1937 let Some(next) = Self::selectable_at_or_after(items, selected + 1) else {
1938 break;
1939 };
1940 selected = next;
1941 }
1942 selected
1943 }
1944 ScrollAction::LineLeft(_) | ScrollAction::LineRight(_) => return None,
1945 ScrollAction::Home => Self::first_selectable_index(items)?,
1946 ScrollAction::End => Self::last_selectable_index(items)?,
1947 };
1948
1949 Some(next)
1950 }
1951
1952 pub(crate) fn selection_for_action_in_len(
1953 selected: usize,
1954 len: usize,
1955 action: ScrollAction,
1956 ) -> Option<usize> {
1957 if len == 0 {
1958 return None;
1959 }
1960
1961 let selected = selected.min(len.saturating_sub(1));
1962 let next = match action {
1963 ScrollAction::LineUp(lines) => selected.saturating_sub(lines),
1964 ScrollAction::LineDown(lines) => (selected + lines).min(len.saturating_sub(1)),
1965 ScrollAction::LineLeft(_) | ScrollAction::LineRight(_) => return None,
1966 ScrollAction::Home => 0,
1967 ScrollAction::End => len.saturating_sub(1),
1968 };
1969
1970 Some(next)
1971 }
1972
1973 pub(crate) fn next_selection(
1974 selected: usize,
1975 items: &[ListItem],
1976 key: &KeyEvent,
1977 scroll_keys: ScrollKeymap,
1978 ) -> Option<usize> {
1979 let action = scroll_action_from_key(key, scroll_keys)?;
1980 Self::selection_for_action(selected, items, action)
1981 }
1982}
1983
1984impl From<List> for Element {
1985 fn from(value: List) -> Self {
1986 Element::new(ElementKind::List(Box::new(value)))
1987 }
1988}
1989
1990fn hash_list_item_layout(item: &ListItem, hasher: &mut impl std::hash::Hasher) {
1991 use std::hash::Hash;
1992
1993 item.role.hash(hasher);
1994 item.active.hash(hasher);
1995 item.status.as_ref().map(ListItemStatus::width).hash(hasher);
1996 item.gutter.as_ref().map(ListItemGutter::width).hash(hasher);
1997 item.gutter_line.hash(hasher);
1998 item.prefix.hash(hasher);
1999 item.prefix_kind.hash(hasher);
2000 item.extra_line_indent.hash(hasher);
2001 item.primary_truncate_description_first.hash(hasher);
2002 item.primary_wrap_label.hash(hasher);
2003 item.primary_wrap_description.hash(hasher);
2004 item.primary_max_label_width.hash(hasher);
2005 item.primary_max_description_width.hash(hasher);
2006 item.symbol_line.hash(hasher);
2007}
2008
2009fn hash_list_item_line_layout(line: &ListItemLine, hasher: &mut impl std::hash::Hasher) {
2010 use std::hash::Hash;
2011
2012 line.truncate_description_first.hash(hasher);
2013 line.wrap_label.hash(hasher);
2014 line.wrap_description.hash(hasher);
2015 line.max_label_width.hash(hasher);
2016 line.max_description_width.hash(hasher);
2017}
2018
2019impl crate::layout::hash::LayoutHash for List {
2020 fn layout_hash(
2021 &self,
2022 hasher: &mut impl std::hash::Hasher,
2023 _recurse: &dyn Fn(&Element) -> Option<u64>,
2024 ) -> Option<()> {
2025 use crate::layout::hash::hash_spans_content;
2026 use std::hash::Hash;
2027
2028 self.width.hash(hasher);
2029 self.height.hash(hasher);
2030 self.border.hash(hasher);
2031 self.scrollbar.hash(hasher);
2032 self.scrollbar_config.variant.hash(hasher);
2033 self.scrollbar_config.gap.hash(hasher);
2034 self.padding.hash(hasher);
2035 self.item_horizontal_padding.hash(hasher);
2036 self.header_horizontal_padding.hash(hasher);
2037 self.symbol_column.hash(hasher);
2038 self.gutter_gap.hash(hasher);
2039 self.gutter_for_non_selectable.hash(hasher);
2040
2041 let needs_content = matches!(self.width, Length::Auto);
2042 let needs_len = matches!(self.height, Length::Auto) || self.scrollbar;
2043
2044 if needs_len {
2045 self.items.len().hash(hasher);
2046 }
2047
2048 if needs_content || needs_len {
2049 self.selected.hash(hasher);
2050 for item in self.items.iter() {
2051 hash_list_item_layout(item, hasher);
2052 hash_spans_content(&item.spans, hasher);
2053 hash_spans_content(&item.description_spans, hasher);
2054 for line in &item.extra_lines {
2055 hash_list_item_line_layout(line, hasher);
2056 hash_spans_content(&line.spans, hasher);
2057 hash_spans_content(&line.description_spans, hasher);
2058 }
2059 }
2060 }
2061
2062 self.title.hash(hasher);
2063 self.empty_text.hash(hasher);
2064 self.selection_symbol.hash(hasher);
2065 self.selection_symbol_right.hash(hasher);
2066 self.active_symbol.hash(hasher);
2067 self.active_symbol_position.hash(hasher);
2068 self.unselected_symbol.hash(hasher);
2069 Some(())
2070 }
2071}
2072
2073#[cfg(test)]
2074mod tests {
2075 use super::*;
2076 use crate::core::element::Element;
2077 use crate::core::event::{KeyCode, KeyEvent, KeyMods};
2078 use crate::layout::hash::element_layout_hash;
2079 fn key(code: KeyCode) -> KeyEvent {
2080 KeyEvent {
2081 code,
2082 mods: KeyMods::default(),
2083 }
2084 }
2085
2086 fn fixture_items() -> Vec<ListItem> {
2087 vec![
2088 ListItem::header("Group A"),
2089 ListItem::new("Alpha"),
2090 ListItem::new("Beta"),
2091 ListItem::spacer(),
2092 ListItem::header("Group B"),
2093 ListItem::new("Gamma"),
2094 ]
2095 }
2096
2097 #[test]
2098 fn keyboard_navigation_skips_headers_and_spacers() {
2099 let items = fixture_items();
2100 let next = List::next_selection(1, &items, &key(KeyCode::Down), ScrollKeymap::default());
2101 assert_eq!(next, Some(2));
2102
2103 let next = List::next_selection(2, &items, &key(KeyCode::Down), ScrollKeymap::default());
2104 assert_eq!(next, Some(5));
2105 }
2106
2107 #[test]
2108 fn home_end_resolve_to_selectable_rows() {
2109 let items = fixture_items();
2110
2111 let home = List::next_selection(5, &items, &key(KeyCode::Home), ScrollKeymap::default());
2112 assert_eq!(home, Some(1));
2113
2114 let end = List::next_selection(1, &items, &key(KeyCode::End), ScrollKeymap::default());
2115 assert_eq!(end, Some(5));
2116 }
2117
2118 #[test]
2119 fn all_non_selectable_rows_have_no_selection_target() {
2120 let items = vec![ListItem::header("Section"), ListItem::spacer()];
2121
2122 assert_eq!(List::first_selectable_index(&items), None);
2123 assert_eq!(
2124 List::next_selection(0, &items, &key(KeyCode::Down), ScrollKeymap::default()),
2125 None
2126 );
2127 }
2128
2129 #[test]
2130 fn auto_height_layout_hash_tracks_extra_line_content() {
2131 let base: Element = List::new()
2132 .items(vec![ListItem::new("Type your own answer")])
2133 .height(Length::Auto)
2134 .into();
2135 let with_answer: Element = List::new()
2136 .items(vec![
2137 ListItem::new("Type your own answer").line("saved answer"),
2138 ])
2139 .height(Length::Auto)
2140 .into();
2141
2142 assert_ne!(
2143 element_layout_hash(&base),
2144 element_layout_hash(&with_answer)
2145 );
2146 }
2147
2148 #[test]
2149 fn layout_hash_tracks_leading_column_config() {
2150 let base: Element = List::new()
2151 .items(vec![ListItem::new("Alpha").gutter(Spinner::new())])
2152 .width(Length::Auto)
2153 .into();
2154 let no_symbol: Element = List::new()
2155 .items(vec![ListItem::new("Alpha").gutter(Spinner::new())])
2156 .width(Length::Auto)
2157 .symbol_column(false)
2158 .into();
2159 let gap: Element = List::new()
2160 .items(vec![ListItem::new("Alpha").gutter(Spinner::new())])
2161 .width(Length::Auto)
2162 .gutter_gap(1)
2163 .into();
2164
2165 assert_ne!(element_layout_hash(&base), element_layout_hash(&no_symbol));
2166 assert_ne!(element_layout_hash(&base), element_layout_hash(&gap));
2167 }
2168
2169 #[test]
2170 fn layout_hash_tracks_row_leading_width_fields() {
2171 let base: Element = List::new()
2172 .items(vec![ListItem::new("A")])
2173 .width(Length::Auto)
2174 .into();
2175 let with_status: Element = List::new()
2176 .items(vec![ListItem::new("A").status_symbol("!!")])
2177 .width(Length::Auto)
2178 .into();
2179 let with_gutter: Element = List::new()
2180 .items(vec![ListItem::new("A").gutter(ListItemGutter::text(">>"))])
2181 .width(Length::Auto)
2182 .into();
2183 let header_with_gutter: Element = List::new()
2184 .items(vec![
2185 ListItem::header("A").gutter(ListItemGutter::text(">>")),
2186 ])
2187 .width(Length::Auto)
2188 .into();
2189
2190 assert_ne!(
2191 element_layout_hash(&base),
2192 element_layout_hash(&with_status)
2193 );
2194 assert_ne!(
2195 element_layout_hash(&base),
2196 element_layout_hash(&with_gutter)
2197 );
2198 assert_ne!(
2199 element_layout_hash(&with_gutter),
2200 element_layout_hash(&header_with_gutter)
2201 );
2202 }
2203
2204 #[test]
2205 fn layout_hash_tracks_wrap_flags_for_auto_height() {
2206 let base: Element = List::new()
2207 .items(vec![ListItem::new("alpha beta gamma")])
2208 .height(Length::Auto)
2209 .into();
2210 let wrapped: Element = List::new()
2211 .items(vec![
2212 ListItem::new("alpha beta gamma").primary_wrap_label(true),
2213 ])
2214 .height(Length::Auto)
2215 .into();
2216
2217 assert_ne!(element_layout_hash(&base), element_layout_hash(&wrapped));
2218 }
2219}
2220
2221pub(crate) mod layout;
2222pub(crate) mod node;
2223pub(crate) mod reconcile;
2224pub(crate) mod utils;
2225
2226pub use node::ListNode;
2227pub(crate) use reconcile::reconcile_list;