1mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_draggable_tab_bar;
8pub use node::DraggableTabBarNode;
9pub use reconcile::reconcile_draggable_tab_bar;
10
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::Arc;
14
15use crate::callback::{Callback, KeyHandler};
16use crate::core::element::{Element, ElementKind};
17use crate::core::event::MouseEvent;
18use crate::style::{BorderStyle, Color, FileIconPalette, Length, Padding, Span, Style, StyleSlot};
19use crate::utils::file_icons::FileIconOverride;
20use crate::utils::file_icons::file_icon;
21use crate::widgets::file_tree::FileIconStyle;
22use crate::widgets::{Spinner, TabsEvent, Text};
23use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
27pub enum DraggableTabBarVariant {
28 #[default]
30 Bordered,
31 FrameLine,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
37pub enum DragReorderMode {
38 #[default]
40 Live,
41 OnDrop,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
47pub enum DraggableTabBarOverflow {
48 #[default]
50 Scroll,
51 ShrinkThenScroll {
53 min_tab_width: u16,
55 },
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
60pub enum DraggableTabKind {
61 #[default]
63 Tab,
64 Action,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70pub struct DraggableTabActionEvent {
71 pub index: usize,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub struct DraggableTabCloseEvent {
78 pub index: usize,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub struct DraggableTabReorderEvent {
85 pub from: usize,
87 pub to: usize,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Hash)]
93pub struct DraggableTabTransferEvent {
94 pub from_bar: Arc<str>,
96 pub to_bar: Arc<str>,
98 pub from: usize,
100 pub to: usize,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum DraggableTabHitPart {
107 Body,
109 Close,
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub struct DraggableTabHit {
116 pub index: usize,
118 pub part: DraggableTabHitPart,
120}
121
122#[derive(Clone, Debug)]
124pub(crate) enum TabLeadingContent {
125 Spinner(TabLeadingSpinner),
126 Text(Text),
127}
128
129#[derive(Clone, Debug)]
130pub(crate) struct TabLeadingSpinner {
131 pub spinner: Spinner,
132 pub auto_frame: bool,
133}
134
135impl TabLeadingContent {
136 pub(crate) fn from_element(element: Element) -> Self {
137 match element.kind {
138 ElementKind::Spinner(spinner) => Self::Spinner(TabLeadingSpinner {
139 auto_frame: spinner.frame.is_none(),
140 spinner,
141 }),
142 ElementKind::Text(text) => Self::Text(text),
143 _ => panic!("DraggableTab::leading only supports Spinner or Text elements"),
144 }
145 }
146
147 pub(crate) fn spinner_mut(&mut self) -> Option<&mut TabLeadingSpinner> {
148 match self {
149 Self::Spinner(spinner) => Some(spinner),
150 Self::Text(_) => None,
151 }
152 }
153
154 pub(crate) fn spinner_frame(&self) -> Option<usize> {
155 match self {
156 Self::Spinner(spinner) => spinner.spinner.frame,
157 Self::Text(_) => None,
158 }
159 }
160
161 pub(crate) fn has_spinner(&self) -> bool {
162 matches!(self, Self::Spinner(_))
163 }
164
165 pub(crate) fn to_span(&self) -> Span {
166 match self {
167 Self::Spinner(spinner) => {
168 let frames = spinner.spinner.spinner_style.frames();
169 let frame_str = frames[spinner.spinner.frame.unwrap_or(0) % frames.len()];
170 let mut span = Span::new(frame_str);
171 span.style = spinner.spinner.style;
172 span
173 }
174 Self::Text(text) => {
175 let mut span = Span::new(text.plain_content());
176 let span_style = text
177 .spans
178 .first()
179 .map(|span| span.style)
180 .unwrap_or_default();
181 span.style = text.style.patch(span_style);
182 span
183 }
184 }
185 }
186}
187
188#[derive(Clone, Debug)]
190pub struct DraggableTab {
191 pub(crate) label: Arc<str>,
192 pub(crate) kind: DraggableTabKind,
193 pub(crate) style: Style,
194 pub(crate) hover_style: Style,
195 pub(crate) active_style: Style,
196 pub(crate) accent_style: Style,
197 pub(crate) active_accent_style: Style,
198 pub(crate) closeable: bool,
199 pub(crate) icon: Option<Span>,
200 pub(crate) leading: Option<TabLeadingContent>,
201 pub(crate) path: Option<Arc<str>>,
202 pub(crate) right_badge: Option<Span>,
203}
204
205impl DraggableTab {
206 pub fn new(label: impl Into<Arc<str>>) -> Self {
208 Self {
209 label: label.into(),
210 kind: DraggableTabKind::Tab,
211 style: Style::default(),
212 hover_style: Style::default(),
213 active_style: Style::default(),
214 accent_style: Style::default(),
215 active_accent_style: Style::default(),
216 closeable: false,
217 icon: None,
218 leading: None,
219 path: None,
220 right_badge: None,
221 }
222 }
223
224 pub fn action(label: impl Into<Arc<str>>) -> Self {
229 Self::new(label).kind(DraggableTabKind::Action)
230 }
231
232 pub fn kind(mut self, kind: DraggableTabKind) -> Self {
234 self.kind = kind;
235 self
236 }
237
238 pub fn style(mut self, style: Style) -> Self {
240 self.style = style;
241 self
242 }
243
244 pub fn hover_style(mut self, style: Style) -> Self {
249 self.hover_style = style;
250 self
251 }
252
253 pub fn active_style(mut self, style: Style) -> Self {
257 self.active_style = style;
258 self
259 }
260
261 pub fn accent_style(mut self, style: Style) -> Self {
265 self.accent_style = style;
266 self
267 }
268
269 pub fn active_accent_style(mut self, style: Style) -> Self {
273 self.active_accent_style = style;
274 self
275 }
276
277 pub fn closeable(mut self, closeable: bool) -> Self {
279 self.closeable = closeable;
280 self
281 }
282
283 pub fn icon(mut self, icon: impl Into<Span>) -> Self {
285 self.icon = Some(icon.into());
286 self
287 }
288
289 pub fn leading(mut self, leading: Element) -> Self {
295 self.leading = Some(TabLeadingContent::from_element(leading));
296 self
297 }
298
299 pub fn path(mut self, path: impl Into<Arc<str>>) -> Self {
301 self.path = Some(path.into());
302 self
303 }
304
305 pub fn right_badge(mut self, badge: impl Into<Span>) -> Self {
307 self.right_badge = Some(badge.into());
308 self
309 }
310}
311
312impl From<&'static str> for DraggableTab {
313 fn from(value: &'static str) -> Self {
314 Self::new(value)
315 }
316}
317
318impl From<String> for DraggableTab {
319 fn from(value: String) -> Self {
320 Self::new(value)
321 }
322}
323
324impl From<Arc<str>> for DraggableTab {
325 fn from(value: Arc<str>) -> Self {
326 Self::new(value)
327 }
328}
329
330#[derive(Clone, Copy, Debug)]
331pub(crate) struct TabMetrics {
332 pub width: usize,
333 pub close_start: Option<usize>,
334 pub close_end: Option<usize>,
335 pub label_width: usize,
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
339pub(crate) enum OverflowControlSide {
340 Left,
341 Right,
342}
343
344#[derive(Clone, Debug)]
345pub(crate) struct OverflowControl {
346 pub start: usize,
347 pub end: usize,
348 pub label: Arc<str>,
349}
350
351pub(crate) type OverflowLabelFormatter = Arc<dyn Fn(usize) -> Arc<str>>;
353
354#[derive(Clone, Default)]
358pub(crate) struct OverflowLabels {
359 pub left: Option<OverflowLabelFormatter>,
360 pub right: Option<OverflowLabelFormatter>,
361}
362
363impl OverflowLabels {
364 pub(crate) fn label(&self, side: OverflowControlSide, hidden_count: usize) -> Arc<str> {
365 let custom = match side {
366 OverflowControlSide::Left => self.left.as_ref(),
367 OverflowControlSide::Right => self.right.as_ref(),
368 };
369 match custom {
370 Some(format) => format(hidden_count),
371 None => default_overflow_control_label(side, hidden_count),
372 }
373 }
374
375 pub(crate) fn width(&self, side: OverflowControlSide, hidden_count: usize) -> usize {
376 UnicodeWidthStr::width(self.label(side, hidden_count).as_ref())
377 }
378}
379
380#[derive(Clone, Debug)]
381pub(crate) struct VisibleTab {
382 pub index: usize,
383 pub start: usize,
384 pub end: usize,
385 pub metrics: TabMetrics,
386 pub clip_left: usize,
387}
388
389#[derive(Clone, Debug)]
390pub(crate) struct TabViewportLayout {
391 pub offset: usize,
392 pub visible_tabs: Vec<VisibleTab>,
393 pub hidden_left: usize,
394 pub hidden_right: usize,
395 pub content_start: usize,
396 pub content_width: usize,
397 pub left_control: Option<OverflowControl>,
398 pub right_control: Option<OverflowControl>,
399}
400
401pub(crate) const TAB_SCROLL_STEP_CHARS: usize = 12;
402pub(crate) const TAB_SCROLL_BUTTON_STEP_CHARS: usize = TAB_SCROLL_STEP_CHARS * 2;
403
404pub(crate) struct TabDisplayOptions<'a> {
405 pub variant: DraggableTabBarVariant,
406 pub divider: char,
407 pub accent_symbol: char,
408 pub close_symbol: &'a str,
409 pub show_close_buttons: bool,
410 pub tab_max_width: Option<u16>,
411 pub overflow: DraggableTabBarOverflow,
412 pub show_file_icons: bool,
413 pub file_icon_style: FileIconStyle,
414 pub file_icon_palette: &'a FileIconPalette,
415 pub file_icon_overrides: &'a HashMap<Arc<str>, FileIconOverride>,
416 pub width_lock: Option<TabWidthLock>,
417}
418
419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420pub(crate) struct TabWidthLock {
421 pub index: usize,
422 pub width: usize,
423}
424
425#[derive(Clone, Default)]
426pub(crate) struct TabViewportOptions {
427 pub scroll_offset: usize,
428 pub viewport_width: usize,
429 pub show_overflow_controls: bool,
430 pub overflow_labels: OverflowLabels,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
434pub(crate) enum DraggableTabHitTarget {
435 Tab(DraggableTabHit),
436 Overflow(OverflowControlSide),
437}
438
439#[derive(Clone)]
441pub struct DraggableTabBar {
442 pub(crate) tabs: Arc<[DraggableTab]>,
443 pub(crate) active: usize,
444 pub(crate) style: Style,
445 pub(crate) focus_style: StyleSlot,
446 pub(crate) hover_style: StyleSlot,
447 pub(crate) tab_hover_style: StyleSlot,
448 pub(crate) active_style: StyleSlot,
449 pub(crate) close_style: Style,
450 pub(crate) close_hover_style: Style,
451 pub(crate) divider: char,
452 pub(crate) border: bool,
453 pub(crate) border_style: BorderStyle,
454 pub(crate) padding: Padding,
455 pub(crate) width: Length,
456 pub(crate) height: Length,
457 pub(crate) variant: DraggableTabBarVariant,
458 pub(crate) accent_symbol: char,
459 pub(crate) active_accent_symbol: char,
460 pub(crate) accent_style: Style,
461 pub(crate) active_accent_style: Style,
462 pub(crate) close_symbol: Arc<str>,
463 pub(crate) show_close_buttons: bool,
464 pub(crate) close_on_hover_only: bool,
465 pub(crate) tab_max_width: Option<u16>,
466 pub(crate) overflow: DraggableTabBarOverflow,
467 pub(crate) scroll_wheel: bool,
468 pub(crate) show_overflow_controls: bool,
469 pub(crate) overflow_style: Style,
470 pub(crate) overflow_hover_style: Style,
471 pub(crate) overflow_labels: OverflowLabels,
472 pub(crate) empty_text: Option<Arc<str>>,
473 pub(crate) empty_text_style: Style,
474 pub(crate) scroll_offset: usize,
475 pub(crate) show_file_icons: bool,
476 pub(crate) file_icon_style: FileIconStyle,
477 pub(crate) file_icon_palette: FileIconPalette,
478 pub(crate) file_icon_overrides: HashMap<Arc<str>, FileIconOverride>,
479 pub(crate) bar_id: Option<Arc<str>>,
480 pub(crate) drag_group: Option<Arc<str>>,
481 pub(crate) draggable: bool,
482 pub(crate) drag_preview: bool,
483 pub(crate) reorder_mode: DragReorderMode,
484 pub(crate) drag_threshold: u16,
485 pub(crate) on_change: Option<Callback<TabsEvent>>,
486 pub(crate) on_action: Option<Callback<DraggableTabActionEvent>>,
487 pub(crate) on_close: Option<Callback<DraggableTabCloseEvent>>,
488 pub(crate) on_reorder: Option<Callback<DraggableTabReorderEvent>>,
489 pub(crate) on_transfer: Option<Callback<DraggableTabTransferEvent>>,
490 pub(crate) on_click: Option<Callback<MouseEvent>>,
491 pub(crate) on_key: Option<KeyHandler>,
492 pub(crate) disabled: bool,
493 pub(crate) disabled_style: Style,
494 pub(crate) focusable: bool,
495 pub(crate) tab_stop: bool,
496 pub(crate) on_focus: Option<Callback<()>>,
497 pub(crate) on_blur: Option<Callback<()>>,
498}
499
500impl Default for DraggableTabBar {
501 fn default() -> Self {
502 Self {
503 tabs: Arc::new([]),
504 active: 0,
505 style: Style::default(),
506 focus_style: StyleSlot::Inherit,
507 hover_style: StyleSlot::Inherit,
508 tab_hover_style: StyleSlot::Inherit,
509 active_style: StyleSlot::Inherit,
510 close_style: Style::default(),
511 close_hover_style: Style::default(),
512 divider: '│',
513 border: false,
514 border_style: BorderStyle::Plain,
515 padding: Padding::default(),
516 width: Length::Flex(1),
517 height: Length::Auto,
518 variant: DraggableTabBarVariant::Bordered,
519 accent_symbol: '▏',
520 active_accent_symbol: '▎',
521 accent_style: Style::default(),
522 active_accent_style: Style::default(),
523 close_symbol: Arc::from(""),
524 show_close_buttons: true,
525 close_on_hover_only: false,
526 tab_max_width: None,
527 overflow: DraggableTabBarOverflow::Scroll,
528 scroll_wheel: true,
529 show_overflow_controls: true,
530 overflow_style: Style::default(),
531 overflow_hover_style: Style::default(),
532 overflow_labels: OverflowLabels::default(),
533 empty_text: None,
534 empty_text_style: Style::default(),
535 scroll_offset: 0,
536 show_file_icons: false,
537 file_icon_style: FileIconStyle::NerdFont,
538 file_icon_palette: FileIconPalette::default(),
539 file_icon_overrides: HashMap::new(),
540 bar_id: None,
541 drag_group: None,
542 draggable: true,
543 drag_preview: true,
544 reorder_mode: DragReorderMode::Live,
545 drag_threshold: 1,
546 on_change: None,
547 on_action: None,
548 on_close: None,
549 on_reorder: None,
550 on_transfer: None,
551 on_click: None,
552 on_key: None,
553 disabled: false,
554 disabled_style: Style::default(),
555 focusable: false,
556 tab_stop: true,
557 on_focus: None,
558 on_blur: None,
559 }
560 }
561}
562
563impl DraggableTabBar {
564 pub fn new() -> Self {
566 Self::default()
567 }
568
569 pub(crate) fn display_options(&self) -> TabDisplayOptions<'_> {
570 TabDisplayOptions {
571 variant: self.variant,
572 divider: self.divider,
573 accent_symbol: self.accent_symbol,
574 close_symbol: &self.close_symbol,
575 show_close_buttons: self.show_close_buttons,
576 tab_max_width: self.tab_max_width,
577 overflow: self.overflow,
578 show_file_icons: self.show_file_icons,
579 file_icon_style: self.file_icon_style,
580 file_icon_palette: &self.file_icon_palette,
581 file_icon_overrides: &self.file_icon_overrides,
582 width_lock: None,
583 }
584 }
585
586 pub fn tabs<I>(mut self, tabs: I) -> Self
588 where
589 I: IntoIterator<Item = DraggableTab>,
590 {
591 self.tabs = tabs.into_iter().collect::<Vec<_>>().into();
592 self
593 }
594
595 pub fn tab(mut self, tab: impl Into<DraggableTab>) -> Self {
597 let mut tabs = self.tabs.to_vec();
598 tabs.push(tab.into());
599 self.tabs = tabs.into();
600 self
601 }
602
603 pub fn active(mut self, active: usize) -> Self {
605 self.active = active;
606 self
607 }
608
609 pub fn style(mut self, style: Style) -> Self {
611 self.style = style;
612 self
613 }
614
615 pub fn focus_style(mut self, style: Style) -> Self {
617 self.focus_style = StyleSlot::Replace(style);
618 self
619 }
620
621 pub fn extend_focus_style(mut self, style: Style) -> Self {
623 self.focus_style = StyleSlot::Extend(style);
624 self
625 }
626
627 pub fn inherit_focus_style(mut self) -> Self {
629 self.focus_style = StyleSlot::Inherit;
630 self
631 }
632
633 pub fn hover_style(mut self, style: Style) -> Self {
635 self.hover_style = StyleSlot::Replace(style);
636 self
637 }
638
639 pub fn extend_hover_style(mut self, style: Style) -> Self {
641 self.hover_style = StyleSlot::Extend(style);
642 self
643 }
644
645 pub fn inherit_hover_style(mut self) -> Self {
647 self.hover_style = StyleSlot::Inherit;
648 self
649 }
650
651 pub fn tab_hover_style(mut self, style: Style) -> Self {
653 self.tab_hover_style = StyleSlot::Replace(style);
654 self
655 }
656
657 pub fn extend_tab_hover_style(mut self, style: Style) -> Self {
659 self.tab_hover_style = StyleSlot::Extend(style);
660 self
661 }
662
663 pub fn inherit_tab_hover_style(mut self) -> Self {
665 self.tab_hover_style = StyleSlot::Inherit;
666 self
667 }
668
669 pub fn active_style(mut self, style: Style) -> Self {
671 self.active_style = StyleSlot::Replace(style);
672 self
673 }
674
675 pub fn extend_active_style(mut self, style: Style) -> Self {
677 self.active_style = StyleSlot::Extend(style);
678 self
679 }
680
681 pub fn inherit_active_style(mut self) -> Self {
683 self.active_style = StyleSlot::Inherit;
684 self
685 }
686
687 pub fn close_style(mut self, style: Style) -> Self {
689 self.close_style = style;
690 self
691 }
692
693 pub fn close_hover_style(mut self, style: Style) -> Self {
695 self.close_hover_style = style;
696 self
697 }
698
699 pub fn divider(mut self, ch: char) -> Self {
701 self.divider = ch;
702 self
703 }
704
705 pub fn border(mut self, border: bool) -> Self {
707 self.border = border;
708 self
709 }
710
711 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
713 self.border_style = border_style;
714 self
715 }
716
717 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
719 self.padding = padding.into();
720 self
721 }
722
723 pub fn width(mut self, width: Length) -> Self {
725 self.width = width;
726 self
727 }
728
729 pub fn height(mut self, height: Length) -> Self {
731 self.height = height;
732 self
733 }
734
735 pub fn variant(mut self, variant: DraggableTabBarVariant) -> Self {
737 self.variant = variant;
738 self
739 }
740
741 pub fn accent_symbol(mut self, symbol: char) -> Self {
743 self.accent_symbol = symbol;
744 self
745 }
746
747 pub fn active_accent_symbol(mut self, symbol: char) -> Self {
749 self.active_accent_symbol = symbol;
750 self
751 }
752
753 pub fn accent_style(mut self, style: Style) -> Self {
755 self.accent_style = style;
756 self
757 }
758
759 pub fn active_accent_style(mut self, style: Style) -> Self {
761 self.active_accent_style = style;
762 self
763 }
764
765 pub fn close_symbol(mut self, symbol: impl Into<Arc<str>>) -> Self {
767 self.close_symbol = symbol.into();
768 self
769 }
770
771 pub fn show_close_buttons(mut self, show: bool) -> Self {
773 self.show_close_buttons = show;
774 self
775 }
776
777 pub fn close_on_hover_only(mut self, only_on_hover: bool) -> Self {
781 self.close_on_hover_only = only_on_hover;
782 self
783 }
784
785 pub fn tab_max_width(mut self, width: Option<u16>) -> Self {
787 self.tab_max_width = width;
788 self
789 }
790
791 pub fn overflow(mut self, overflow: DraggableTabBarOverflow) -> Self {
793 self.overflow = overflow;
794 self
795 }
796
797 pub fn scroll_wheel(mut self, enabled: bool) -> Self {
799 self.scroll_wheel = enabled;
800 self
801 }
802
803 pub fn show_overflow_controls(mut self, show: bool) -> Self {
805 self.show_overflow_controls = show;
806 self
807 }
808
809 pub fn overflow_style(mut self, style: Style) -> Self {
811 self.overflow_style = style;
812 self
813 }
814
815 pub fn overflow_hover_style(mut self, style: Style) -> Self {
817 self.overflow_hover_style = style;
818 self
819 }
820
821 pub fn overflow_left_label<F>(mut self, formatter: F) -> Self
827 where
828 F: Fn(usize) -> Arc<str> + 'static,
829 {
830 self.overflow_labels.left = Some(Arc::new(formatter));
831 self
832 }
833
834 pub fn overflow_right_label<F>(mut self, formatter: F) -> Self
839 where
840 F: Fn(usize) -> Arc<str> + 'static,
841 {
842 self.overflow_labels.right = Some(Arc::new(formatter));
843 self
844 }
845
846 pub fn empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
852 self.empty_text = Some(text.into());
853 self
854 }
855
856 pub fn empty_text_style(mut self, style: Style) -> Self {
860 self.empty_text_style = style;
861 self
862 }
863
864 pub fn scroll_offset(mut self, offset: usize) -> Self {
866 self.scroll_offset = offset;
867 self
868 }
869
870 pub fn show_file_icons(mut self, show: bool) -> Self {
872 self.show_file_icons = show;
873 self
874 }
875
876 pub fn file_icon_style(mut self, style: FileIconStyle) -> Self {
878 self.file_icon_style = style;
879 self
880 }
881
882 pub fn file_icon_palette(mut self, palette: FileIconPalette) -> Self {
884 self.file_icon_palette = palette;
885 self
886 }
887
888 pub fn file_icon_override(
890 mut self,
891 pattern: impl Into<Arc<str>>,
892 icon: impl Into<Arc<str>>,
893 color: Option<Color>,
894 ) -> Self {
895 self.file_icon_overrides.insert(
896 pattern.into(),
897 FileIconOverride {
898 icon: icon.into(),
899 color,
900 },
901 );
902 self
903 }
904
905 pub fn bar_id(mut self, id: impl Into<Arc<str>>) -> Self {
909 self.bar_id = Some(id.into());
910 self
911 }
912
913 pub fn drag_group(mut self, group: impl Into<Arc<str>>) -> Self {
917 self.drag_group = Some(group.into());
918 self
919 }
920
921 pub fn draggable(mut self, draggable: bool) -> Self {
923 self.draggable = draggable;
924 self
925 }
926
927 pub fn drag_preview(mut self, enabled: bool) -> Self {
929 self.drag_preview = enabled;
930 self
931 }
932
933 pub fn reorder_mode(mut self, mode: DragReorderMode) -> Self {
935 self.reorder_mode = mode;
936 self
937 }
938
939 pub fn drag_threshold(mut self, threshold: u16) -> Self {
941 self.drag_threshold = threshold;
942 self
943 }
944
945 pub fn on_change(mut self, cb: Callback<TabsEvent>) -> Self {
947 self.on_change = Some(cb);
948 self
949 }
950
951 pub fn on_action(mut self, cb: Callback<DraggableTabActionEvent>) -> Self {
953 self.on_action = Some(cb);
954 self
955 }
956
957 pub fn on_close(mut self, cb: Callback<DraggableTabCloseEvent>) -> Self {
959 self.on_close = Some(cb);
960 self
961 }
962
963 pub fn on_reorder(mut self, cb: Callback<DraggableTabReorderEvent>) -> Self {
965 self.on_reorder = Some(cb);
966 self
967 }
968
969 pub fn on_transfer(mut self, cb: Callback<DraggableTabTransferEvent>) -> Self {
971 self.on_transfer = Some(cb);
972 self
973 }
974
975 pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
977 self.on_click = Some(cb);
978 self
979 }
980
981 pub fn on_key(mut self, handler: KeyHandler) -> Self {
983 self.on_key = Some(handler);
984 self
985 }
986
987 pub fn disabled(mut self, disabled: bool) -> Self {
989 self.disabled = disabled;
990 self
991 }
992
993 pub fn disabled_style(mut self, style: Style) -> Self {
995 self.disabled_style = style;
996 self
997 }
998
999 pub fn focusable(mut self, focusable: bool) -> Self {
1001 self.focusable = focusable;
1002 self
1003 }
1004
1005 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
1007 self.tab_stop = tab_stop;
1008 self
1009 }
1010
1011 pub fn on_focus(mut self, cb: Callback<()>) -> Self {
1013 self.on_focus = Some(cb);
1014 self
1015 }
1016
1017 pub fn on_blur(mut self, cb: Callback<()>) -> Self {
1019 self.on_blur = Some(cb);
1020 self
1021 }
1022
1023 #[cfg(test)]
1024 pub(crate) fn content_width(
1025 tabs: &[DraggableTab],
1026 variant: DraggableTabBarVariant,
1027 divider: char,
1028 accent_symbol: char,
1029 close_symbol: &str,
1030 show_close_buttons: bool,
1031 ) -> usize {
1032 Self::content_width_with_options(
1033 tabs,
1034 &TabDisplayOptions {
1035 variant,
1036 divider,
1037 accent_symbol,
1038 close_symbol,
1039 show_close_buttons,
1040 tab_max_width: None,
1041 overflow: DraggableTabBarOverflow::Scroll,
1042 show_file_icons: false,
1043 file_icon_style: FileIconStyle::NerdFont,
1044 file_icon_palette: &FileIconPalette::default(),
1045 file_icon_overrides: &HashMap::new(),
1046 width_lock: None,
1047 },
1048 )
1049 }
1050
1051 pub(crate) fn content_width_with_options(
1052 tabs: &[DraggableTab],
1053 opts: &TabDisplayOptions<'_>,
1054 ) -> usize {
1055 let mut width = 0usize;
1056 for (i, tab) in tabs.iter().enumerate() {
1057 width = width.saturating_add(tab_metrics_with_options(tab, opts).width);
1058 if i + 1 < tabs.len() {
1059 width = width.saturating_add(separator_width(opts.variant, opts.divider));
1060 }
1061 }
1062 width
1063 }
1064
1065 pub(crate) fn content_width_for_viewport(
1066 tabs: &[DraggableTab],
1067 opts: &TabDisplayOptions<'_>,
1068 viewport_width: usize,
1069 ) -> usize {
1070 let metrics = tab_metrics_for_viewport(tabs, opts, Some(viewport_width));
1071 total_width_for_metrics(&metrics, opts)
1072 }
1073
1074 #[cfg(test)]
1075 pub(crate) fn hit_at_col(
1076 tabs: &[DraggableTab],
1077 variant: DraggableTabBarVariant,
1078 divider: char,
1079 accent_symbol: char,
1080 close_symbol: &str,
1081 show_close_buttons: bool,
1082 col: usize,
1083 ) -> Option<DraggableTabHit> {
1084 Self::hit_at_col_with_options(
1085 tabs,
1086 &TabDisplayOptions {
1087 variant,
1088 divider,
1089 accent_symbol,
1090 close_symbol,
1091 show_close_buttons,
1092 tab_max_width: None,
1093 overflow: DraggableTabBarOverflow::Scroll,
1094 show_file_icons: false,
1095 file_icon_style: FileIconStyle::NerdFont,
1096 file_icon_palette: &FileIconPalette::default(),
1097 file_icon_overrides: &HashMap::new(),
1098 width_lock: None,
1099 },
1100 col,
1101 )
1102 }
1103
1104 #[cfg(test)]
1105 pub(crate) fn hit_at_col_with_options(
1106 tabs: &[DraggableTab],
1107 opts: &TabDisplayOptions<'_>,
1108 col: usize,
1109 ) -> Option<DraggableTabHit> {
1110 let mut x = 0usize;
1111 for (i, tab) in tabs.iter().enumerate() {
1112 let metrics = tab_metrics_with_options(tab, opts);
1113 let end = x.saturating_add(metrics.width);
1114 if col < end {
1115 let part = if let (Some(close_start), Some(close_end)) =
1116 (metrics.close_start, metrics.close_end)
1117 {
1118 let c_start = x.saturating_add(close_start);
1119 let c_end = x.saturating_add(close_end);
1120 if col >= c_start && col < c_end {
1121 DraggableTabHitPart::Close
1122 } else {
1123 DraggableTabHitPart::Body
1124 }
1125 } else {
1126 DraggableTabHitPart::Body
1127 };
1128 return Some(DraggableTabHit { index: i, part });
1129 }
1130 x = end;
1131
1132 if i + 1 < tabs.len() {
1133 let sep_w = separator_width(opts.variant, opts.divider);
1134 if col < x.saturating_add(sep_w) {
1135 return match opts.variant {
1136 DraggableTabBarVariant::Bordered => None,
1137 DraggableTabBarVariant::FrameLine => Some(DraggableTabHit {
1138 index: i,
1139 part: DraggableTabHitPart::Body,
1140 }),
1141 };
1142 }
1143 x = x.saturating_add(sep_w);
1144 }
1145 }
1146
1147 None
1148 }
1149
1150 #[cfg(test)]
1151 pub(crate) fn reorder_index_at_col(
1152 tabs: &[DraggableTab],
1153 variant: DraggableTabBarVariant,
1154 divider: char,
1155 accent_symbol: char,
1156 close_symbol: &str,
1157 show_close_buttons: bool,
1158 col: usize,
1159 ) -> Option<usize> {
1160 Self::reorder_index_at_col_with_options(
1161 tabs,
1162 &TabDisplayOptions {
1163 variant,
1164 divider,
1165 accent_symbol,
1166 close_symbol,
1167 show_close_buttons,
1168 tab_max_width: None,
1169 overflow: DraggableTabBarOverflow::Scroll,
1170 show_file_icons: false,
1171 file_icon_style: FileIconStyle::NerdFont,
1172 file_icon_palette: &FileIconPalette::default(),
1173 file_icon_overrides: &HashMap::new(),
1174 width_lock: None,
1175 },
1176 col,
1177 )
1178 }
1179
1180 #[cfg(test)]
1181 pub(crate) fn reorder_index_at_col_with_options(
1182 tabs: &[DraggableTab],
1183 opts: &TabDisplayOptions<'_>,
1184 col: usize,
1185 ) -> Option<usize> {
1186 let metrics = tab_metrics_for_viewport(tabs, opts, None);
1187 Self::reorder_index_at_col_with_metrics(tabs, opts, &metrics, col)
1188 }
1189
1190 fn reorder_index_at_col_with_metrics(
1191 tabs: &[DraggableTab],
1192 opts: &TabDisplayOptions<'_>,
1193 metrics: &[TabMetrics],
1194 col: usize,
1195 ) -> Option<usize> {
1196 let mut x = 0usize;
1197 let len = tabs.len();
1198 for (i, metrics) in metrics.iter().enumerate() {
1199 let end = x.saturating_add(metrics.width);
1200 if col < end {
1201 return Some(i);
1202 }
1203 x = end;
1204
1205 if i + 1 < len {
1206 let sep_w = separator_width(opts.variant, opts.divider);
1207 if col < x.saturating_add(sep_w) {
1208 return Some((i + 1).min(len.saturating_sub(1)));
1209 }
1210 x = x.saturating_add(sep_w);
1211 }
1212 }
1213 None
1214 }
1215
1216 #[cfg(test)]
1217 pub(crate) fn adjacent_reorder_target(
1218 tabs: &[DraggableTab],
1219 opts: &TabDisplayOptions<'_>,
1220 current_index: usize,
1221 col: usize,
1222 ) -> Option<usize> {
1223 Self::adjacent_reorder_target_with_options(tabs, opts, current_index, col)
1224 }
1225
1226 #[cfg(test)]
1227 pub(crate) fn adjacent_reorder_target_with_options(
1228 tabs: &[DraggableTab],
1229 opts: &TabDisplayOptions<'_>,
1230 current_index: usize,
1231 col: usize,
1232 ) -> Option<usize> {
1233 let metrics = tab_metrics_for_viewport(tabs, opts, None);
1234 Self::adjacent_reorder_target_with_metrics(tabs, opts, &metrics, current_index, col)
1235 }
1236
1237 fn adjacent_reorder_target_with_metrics(
1238 tabs: &[DraggableTab],
1239 opts: &TabDisplayOptions<'_>,
1240 metrics: &[TabMetrics],
1241 current_index: usize,
1242 col: usize,
1243 ) -> Option<usize> {
1244 if tabs.is_empty() || current_index >= tabs.len() {
1245 return None;
1246 }
1247
1248 if !tabs.get(current_index).is_some_and(is_reorderable_tab) {
1249 return None;
1250 }
1251
1252 let mut starts = Vec::with_capacity(tabs.len());
1253 let mut widths = Vec::with_capacity(tabs.len());
1254 let mut reorder_indices = Vec::new();
1255 let mut x = 0usize;
1256 for (i, (tab, metrics)) in tabs.iter().zip(metrics).enumerate() {
1257 starts.push(x);
1258 widths.push(metrics.width);
1259 if is_reorderable_tab(tab) {
1260 reorder_indices.push(i);
1261 }
1262 x = x.saturating_add(metrics.width);
1263 if i + 1 < tabs.len() {
1264 x = x.saturating_add(separator_width(opts.variant, opts.divider));
1265 }
1266 }
1267
1268 let mut position = reorder_indices
1269 .iter()
1270 .position(|&index| index == current_index)?;
1271 let mut run_start = position;
1272 while run_start > 0 && reorder_indices[run_start - 1] + 1 == reorder_indices[run_start] {
1273 run_start -= 1;
1274 }
1275 let mut run_end = position;
1276 while run_end + 1 < reorder_indices.len()
1277 && reorder_indices[run_end] + 1 == reorder_indices[run_end + 1]
1278 {
1279 run_end += 1;
1280 }
1281
1282 let midpoint = |idx: usize| -> usize { starts[idx].saturating_add(widths[idx] / 2) };
1283
1284 while position < run_end && col >= midpoint(reorder_indices[position + 1]) {
1285 position += 1;
1286 }
1287
1288 while position > run_start && col < midpoint(reorder_indices[position - 1]) {
1289 position -= 1;
1290 }
1291
1292 let target = reorder_indices[position];
1293 (target != current_index).then_some(target)
1294 }
1295
1296 pub(crate) fn viewport_layout(
1297 tabs: &[DraggableTab],
1298 opts: &TabDisplayOptions<'_>,
1299 vp: &TabViewportOptions,
1300 ) -> TabViewportLayout {
1301 let len = tabs.len();
1302 if len == 0 || vp.viewport_width == 0 {
1303 return TabViewportLayout {
1304 offset: 0,
1305 visible_tabs: Vec::new(),
1306 hidden_left: 0,
1307 hidden_right: 0,
1308 content_start: 0,
1309 content_width: 0,
1310 left_control: None,
1311 right_control: None,
1312 };
1313 }
1314
1315 let (runs, total_width) = tab_runs_for_viewport(tabs, opts, Some(vp.viewport_width));
1316
1317 let requested_offset = vp.scroll_offset;
1318 let mut offset = vp.scroll_offset;
1319 let mut align_right = false;
1320 let mut left_width = 0usize;
1321 let mut right_width = 0usize;
1322 let mut visible_tabs = Vec::new();
1323 let mut hidden_left = 0usize;
1324 let mut hidden_right = 0usize;
1325
1326 for _ in 0..8 {
1327 visible_tabs.clear();
1328 let mut available = vp.viewport_width.saturating_sub(left_width + right_width);
1329 if available == 0 {
1330 if right_width > 0 {
1331 right_width = 0;
1332 available = vp.viewport_width.saturating_sub(left_width);
1333 } else if left_width > 0 {
1334 left_width = 0;
1335 available = vp.viewport_width;
1336 }
1337 }
1338
1339 let max_scroll = total_width.saturating_sub(available);
1340 if align_right || requested_offset >= max_scroll {
1341 align_right = true;
1342 offset = max_scroll;
1343 } else {
1344 offset = requested_offset.min(max_scroll);
1345 }
1346 let view_start = offset;
1347 let view_end = view_start.saturating_add(available);
1348
1349 hidden_left = 0;
1350 hidden_right = 0;
1351 for (idx, (start, end, metrics)) in runs.iter().enumerate() {
1352 if *end <= view_start {
1353 hidden_left = idx.saturating_add(1);
1354 continue;
1355 }
1356 if *start >= view_end {
1357 hidden_right = len.saturating_sub(idx);
1358 break;
1359 }
1360
1361 let visible_start = (*start).max(view_start);
1362 let visible_end = (*end).min(view_end);
1363 if visible_start >= visible_end {
1364 continue;
1365 }
1366
1367 if visible_start > *start {
1368 hidden_left = idx.saturating_add(1);
1369 }
1370
1371 visible_tabs.push(VisibleTab {
1372 index: idx,
1373 start: visible_start.saturating_sub(view_start),
1374 end: visible_end.saturating_sub(view_start),
1375 metrics: *metrics,
1376 clip_left: visible_start.saturating_sub(*start),
1377 });
1378
1379 if visible_end < *end {
1380 hidden_right = len.saturating_sub(idx);
1381 break;
1382 }
1383 }
1384
1385 if visible_tabs.is_empty() && offset > 0 {
1386 offset = offset.saturating_sub(1);
1387 continue;
1388 }
1389
1390 if hidden_right == 0 {
1391 hidden_right = if let Some(last) = visible_tabs.last() {
1392 len.saturating_sub(last.index + 1)
1393 } else {
1394 len.saturating_sub(offset)
1395 };
1396 }
1397
1398 let next_left = if vp.show_overflow_controls && hidden_left > 0 {
1399 vp.overflow_labels
1400 .width(OverflowControlSide::Left, hidden_left)
1401 } else {
1402 0
1403 };
1404 let next_right = if vp.show_overflow_controls && hidden_right > 0 {
1405 vp.overflow_labels
1406 .width(OverflowControlSide::Right, hidden_right)
1407 } else {
1408 0
1409 };
1410
1411 if next_left == left_width && next_right == right_width {
1412 break;
1413 }
1414
1415 left_width = next_left;
1416 right_width = next_right;
1417 }
1418
1419 let content_start = left_width.min(vp.viewport_width);
1420 let content_width = vp.viewport_width.saturating_sub(left_width + right_width);
1421
1422 let mut shifted_tabs = Vec::with_capacity(visible_tabs.len());
1423 for tab in visible_tabs {
1424 shifted_tabs.push(VisibleTab {
1425 start: tab.start.saturating_add(content_start),
1426 end: tab.end.saturating_add(content_start),
1427 ..tab
1428 });
1429 }
1430
1431 let left_control = if left_width > 0 {
1432 Some(OverflowControl {
1433 start: 0,
1434 end: left_width,
1435 label: vp
1436 .overflow_labels
1437 .label(OverflowControlSide::Left, hidden_left),
1438 })
1439 } else {
1440 None
1441 };
1442
1443 let right_control = if right_width > 0 {
1444 let start = vp.viewport_width.saturating_sub(right_width);
1445 Some(OverflowControl {
1446 start,
1447 end: vp.viewport_width,
1448 label: vp
1449 .overflow_labels
1450 .label(OverflowControlSide::Right, hidden_right),
1451 })
1452 } else {
1453 None
1454 };
1455
1456 TabViewportLayout {
1457 offset,
1458 visible_tabs: shifted_tabs,
1459 hidden_left,
1460 hidden_right,
1461 content_start,
1462 content_width,
1463 left_control,
1464 right_control,
1465 }
1466 }
1467
1468 pub(crate) fn hit_target_at_view_col(
1469 tabs: &[DraggableTab],
1470 opts: &TabDisplayOptions<'_>,
1471 vp: &TabViewportOptions,
1472 col: usize,
1473 ) -> Option<DraggableTabHitTarget> {
1474 let layout = Self::viewport_layout(tabs, opts, vp);
1475
1476 if let Some(left) = &layout.left_control
1477 && col >= left.start
1478 && col < left.end
1479 {
1480 return Some(DraggableTabHitTarget::Overflow(OverflowControlSide::Left));
1481 }
1482 if let Some(right) = &layout.right_control
1483 && col >= right.start
1484 && col < right.end
1485 {
1486 return Some(DraggableTabHitTarget::Overflow(OverflowControlSide::Right));
1487 }
1488
1489 for tab in &layout.visible_tabs {
1490 if col < tab.start || col >= tab.end {
1491 continue;
1492 }
1493 let local = col.saturating_sub(tab.start).saturating_add(tab.clip_left);
1494 let part = if let (Some(close_start), Some(close_end)) =
1495 (tab.metrics.close_start, tab.metrics.close_end)
1496 {
1497 if local >= close_start && local < close_end {
1498 DraggableTabHitPart::Close
1499 } else {
1500 DraggableTabHitPart::Body
1501 }
1502 } else {
1503 DraggableTabHitPart::Body
1504 };
1505 return Some(DraggableTabHitTarget::Tab(DraggableTabHit {
1506 index: tab.index,
1507 part,
1508 }));
1509 }
1510
1511 None
1512 }
1513
1514 pub(crate) fn global_col_from_view_col(
1515 tabs: &[DraggableTab],
1516 opts: &TabDisplayOptions<'_>,
1517 vp: &TabViewportOptions,
1518 col: usize,
1519 ) -> Option<usize> {
1520 let layout = Self::viewport_layout(tabs, opts, vp);
1521
1522 if col < layout.content_start
1523 || col >= layout.content_start.saturating_add(layout.content_width)
1524 {
1525 return None;
1526 }
1527
1528 let view_col = col.saturating_sub(layout.content_start);
1529 Some(layout.offset.saturating_add(view_col))
1530 }
1531
1532 pub(crate) fn reorder_index_at_view_col(
1533 tabs: &[DraggableTab],
1534 opts: &TabDisplayOptions<'_>,
1535 vp: &TabViewportOptions,
1536 col: usize,
1537 ) -> Option<usize> {
1538 let global_col = Self::global_col_from_view_col(tabs, opts, vp, col)?;
1539 let metrics = tab_metrics_for_viewport(tabs, opts, Some(vp.viewport_width));
1540 Self::reorder_index_at_col_with_metrics(tabs, opts, &metrics, global_col)
1541 }
1542
1543 pub(crate) fn adjacent_reorder_target_at_view_col(
1544 tabs: &[DraggableTab],
1545 opts: &TabDisplayOptions<'_>,
1546 vp: &TabViewportOptions,
1547 current_index: usize,
1548 col: usize,
1549 ) -> Option<usize> {
1550 let global_col = Self::global_col_from_view_col(tabs, opts, vp, col)?;
1551 let metrics = tab_metrics_for_viewport(tabs, opts, Some(vp.viewport_width));
1552 Self::adjacent_reorder_target_with_metrics(tabs, opts, &metrics, current_index, global_col)
1553 }
1554
1555 pub(crate) fn scroll_offset_for_step(
1556 tabs: &[DraggableTab],
1557 opts: &TabDisplayOptions<'_>,
1558 vp: &TabViewportOptions,
1559 step_right: bool,
1560 step_chars: usize,
1561 ) -> usize {
1562 if tabs.is_empty() || vp.viewport_width == 0 {
1563 return 0;
1564 }
1565
1566 let layout = Self::viewport_layout(tabs, opts, vp);
1567 let current = layout.offset;
1568
1569 if (step_right && layout.hidden_right == 0) || (!step_right && layout.hidden_left == 0) {
1570 return current;
1571 }
1572
1573 let step = step_chars.max(1);
1574 let requested = if step_right {
1575 current.saturating_add(step)
1576 } else {
1577 current.saturating_sub(step)
1578 };
1579
1580 let canonical = Self::viewport_layout(
1581 tabs,
1582 opts,
1583 &TabViewportOptions {
1584 scroll_offset: requested,
1585 ..vp.clone()
1586 },
1587 )
1588 .offset;
1589
1590 if canonical != current {
1591 return canonical;
1592 }
1593
1594 if step_right {
1595 let total_width = Self::content_width_for_viewport(tabs, opts, vp.viewport_width);
1596 return Self::viewport_layout(
1597 tabs,
1598 opts,
1599 &TabViewportOptions {
1600 scroll_offset: total_width.saturating_sub(1),
1601 ..vp.clone()
1602 },
1603 )
1604 .offset;
1605 }
1606
1607 0
1608 }
1609
1610 pub(crate) fn scroll_offset_to_reveal_tab(
1611 tabs: &[DraggableTab],
1612 opts: &TabDisplayOptions<'_>,
1613 vp: &TabViewportOptions,
1614 tab_index: usize,
1615 ) -> usize {
1616 if tabs.is_empty() || vp.viewport_width == 0 {
1617 return 0;
1618 }
1619 let target = tab_index.min(tabs.len().saturating_sub(1));
1620 let metrics = tab_metrics_for_viewport(tabs, opts, Some(vp.viewport_width));
1621 let target_start = tabs_prefix_width_from_metrics(&metrics, opts, target);
1622 let target_width = metrics[target].width;
1623 let target_end = target_start.saturating_add(target_width);
1624
1625 let mut offset = vp.scroll_offset;
1626 for _ in 0..8 {
1627 let layout = Self::viewport_layout(
1628 tabs,
1629 opts,
1630 &TabViewportOptions {
1631 scroll_offset: offset,
1632 ..vp.clone()
1633 },
1634 );
1635 let view_start = layout.offset;
1636 let view_end = view_start.saturating_add(layout.content_width);
1637
1638 let next = if target_width > layout.content_width || target_start < view_start {
1639 target_start
1640 } else if target_end > view_end {
1641 target_end.saturating_sub(layout.content_width)
1642 } else {
1643 return view_start;
1644 };
1645
1646 if next == offset {
1647 return layout.offset;
1648 }
1649 offset = next;
1650 }
1651
1652 offset
1653 }
1654}
1655
1656pub(crate) fn is_reorderable_tab(tab: &DraggableTab) -> bool {
1657 tab.kind == DraggableTabKind::Tab
1658}
1659
1660#[cfg(test)]
1661pub(crate) fn reorder_target_at_col_with_options(
1662 tabs: &[DraggableTab],
1663 opts: &TabDisplayOptions<'_>,
1664 col: usize,
1665) -> Option<usize> {
1666 let candidate = DraggableTabBar::reorder_index_at_col_with_options(tabs, opts, col)?;
1667 if tabs.get(candidate).is_some_and(is_reorderable_tab) {
1668 return Some(candidate);
1669 }
1670
1671 (0..candidate)
1672 .rev()
1673 .find(|&index| tabs.get(index).is_some_and(is_reorderable_tab))
1674 .or_else(|| {
1675 ((candidate + 1)..tabs.len())
1676 .find(|&index| tabs.get(index).is_some_and(is_reorderable_tab))
1677 })
1678}
1679
1680pub(crate) fn reorder_target_at_view_col_with_options(
1681 tabs: &[DraggableTab],
1682 opts: &TabDisplayOptions<'_>,
1683 vp: &TabViewportOptions,
1684 col: usize,
1685) -> Option<usize> {
1686 let candidate = DraggableTabBar::reorder_index_at_view_col(tabs, opts, vp, col)?;
1687 if tabs.get(candidate).is_some_and(is_reorderable_tab) {
1688 return Some(candidate);
1689 }
1690
1691 (0..candidate)
1692 .rev()
1693 .find(|&index| tabs.get(index).is_some_and(is_reorderable_tab))
1694 .or_else(|| {
1695 ((candidate + 1)..tabs.len())
1696 .find(|&index| tabs.get(index).is_some_and(is_reorderable_tab))
1697 })
1698}
1699
1700fn separator_width(variant: DraggableTabBarVariant, divider: char) -> usize {
1701 match variant {
1702 DraggableTabBarVariant::Bordered => UnicodeWidthChar::width(divider).unwrap_or(1),
1703 DraggableTabBarVariant::FrameLine => 0,
1704 }
1705}
1706
1707#[cfg(test)]
1708pub(crate) fn tab_metrics(
1709 tab: &DraggableTab,
1710 variant: DraggableTabBarVariant,
1711 accent_symbol: char,
1712 close_symbol: &str,
1713 show_close_buttons: bool,
1714) -> TabMetrics {
1715 tab_metrics_with_options(
1716 tab,
1717 &TabDisplayOptions {
1718 variant,
1719 divider: '|',
1720 accent_symbol,
1721 close_symbol,
1722 show_close_buttons,
1723 tab_max_width: None,
1724 overflow: DraggableTabBarOverflow::Scroll,
1725 show_file_icons: false,
1726 file_icon_style: FileIconStyle::NerdFont,
1727 file_icon_palette: &FileIconPalette::default(),
1728 file_icon_overrides: &HashMap::new(),
1729 width_lock: None,
1730 },
1731 )
1732}
1733
1734pub(crate) fn tab_metrics_with_options(
1735 tab: &DraggableTab,
1736 opts: &TabDisplayOptions<'_>,
1737) -> TabMetrics {
1738 let label_w = tab_label_width(tab, opts);
1739 tab_metrics_with_label_width(tab, opts, label_w)
1740}
1741
1742fn tab_label_width(tab: &DraggableTab, opts: &TabDisplayOptions<'_>) -> usize {
1743 let mut label_w = UnicodeWidthStr::width(tab.label.as_ref());
1744 if let Some(max) = opts.tab_max_width {
1745 let max = (max as usize).max(1);
1746 label_w = label_w.min(max);
1747 }
1748 label_w
1749}
1750
1751fn tab_metrics_with_label_width(
1752 tab: &DraggableTab,
1753 opts: &TabDisplayOptions<'_>,
1754 label_w: usize,
1755) -> TabMetrics {
1756 let icon_w = resolve_tab_icon(
1757 tab,
1758 opts.show_file_icons,
1759 opts.file_icon_style,
1760 opts.file_icon_palette,
1761 opts.file_icon_overrides,
1762 )
1763 .map(|icon| UnicodeWidthStr::width(icon.content.as_ref()).saturating_add(1))
1764 .unwrap_or(0);
1765
1766 let badge_w = tab
1767 .right_badge
1768 .as_ref()
1769 .map(|badge| UnicodeWidthStr::width(badge.content.as_ref()))
1770 .unwrap_or(0);
1771 let badge_gap_w = if badge_w > 0 { 1 } else { 0 };
1772
1773 let close_symbol_w = UnicodeWidthStr::width(opts.close_symbol).max(1);
1774 let has_close = opts.show_close_buttons && tab.closeable && is_reorderable_tab(tab);
1775 let close_gap_w = if has_close { 1 } else { 0 };
1776 let close_zone_w = if has_close { close_symbol_w } else { 0 };
1777
1778 match opts.variant {
1779 DraggableTabBarVariant::Bordered => {
1780 let close_start =
1781 has_close.then_some(1 + icon_w + label_w + badge_gap_w + badge_w + close_gap_w);
1782 let close_end = has_close.then_some(
1783 1 + icon_w + label_w + badge_gap_w + badge_w + close_gap_w + close_zone_w,
1784 );
1785 TabMetrics {
1786 width: 1
1787 + icon_w
1788 + label_w
1789 + badge_gap_w
1790 + badge_w
1791 + close_gap_w
1792 + close_zone_w
1793 + 1,
1794 close_start,
1795 close_end,
1796 label_width: label_w,
1797 }
1798 }
1799 DraggableTabBarVariant::FrameLine => {
1800 let accent_w = UnicodeWidthChar::width(opts.accent_symbol).unwrap_or(1);
1801 let close_start = has_close
1802 .then_some(accent_w + 1 + icon_w + label_w + badge_gap_w + badge_w + close_gap_w);
1803 let close_end = has_close.then_some(
1804 accent_w
1805 + 1
1806 + icon_w
1807 + label_w
1808 + badge_gap_w
1809 + badge_w
1810 + close_gap_w
1811 + close_zone_w,
1812 );
1813 TabMetrics {
1814 width: accent_w
1815 + 1
1816 + icon_w
1817 + label_w
1818 + badge_gap_w
1819 + badge_w
1820 + close_gap_w
1821 + close_zone_w
1822 + 1,
1823 close_start,
1824 close_end,
1825 label_width: label_w,
1826 }
1827 }
1828 }
1829}
1830
1831fn natural_tab_metrics(tabs: &[DraggableTab], opts: &TabDisplayOptions<'_>) -> Vec<TabMetrics> {
1832 tabs.iter()
1833 .map(|tab| tab_metrics_with_options(tab, opts))
1834 .collect()
1835}
1836
1837fn tab_metrics_for_viewport(
1838 tabs: &[DraggableTab],
1839 opts: &TabDisplayOptions<'_>,
1840 viewport_width: Option<usize>,
1841) -> Vec<TabMetrics> {
1842 let natural = natural_tab_metrics(tabs, opts);
1843 let Some(viewport_width) = viewport_width else {
1844 return apply_tab_width_lock(tabs, opts, natural);
1845 };
1846
1847 let DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width } = opts.overflow else {
1848 return apply_tab_width_lock(tabs, opts, natural);
1849 };
1850
1851 if tabs.is_empty() || viewport_width == 0 {
1852 return apply_tab_width_lock(tabs, opts, natural);
1853 }
1854
1855 let natural_total = total_width_for_metrics(&natural, opts);
1856 if natural_total <= viewport_width {
1857 return apply_tab_width_lock(tabs, opts, natural);
1858 }
1859
1860 let min_tab_width = (min_tab_width as usize).max(1);
1861 let min_label_widths = natural
1862 .iter()
1863 .map(|metrics| {
1864 let fixed_width = metrics.width.saturating_sub(metrics.label_width);
1865 metrics
1866 .label_width
1867 .min(min_tab_width.saturating_sub(fixed_width))
1868 })
1869 .collect::<Vec<_>>();
1870 let fixed_total = natural
1871 .iter()
1872 .map(|metrics| metrics.width.saturating_sub(metrics.label_width))
1873 .sum::<usize>()
1874 .saturating_add(separator_width(opts.variant, opts.divider) * tabs.len().saturating_sub(1));
1875 let min_total = fixed_total.saturating_add(min_label_widths.iter().sum::<usize>());
1876
1877 if min_total >= natural_total {
1878 return apply_tab_width_lock(tabs, opts, natural);
1879 }
1880
1881 if min_total > viewport_width {
1882 let metrics = tabs
1883 .iter()
1884 .zip(min_label_widths)
1885 .map(|(tab, label_width)| tab_metrics_with_label_width(tab, opts, label_width))
1886 .collect();
1887 return apply_tab_width_lock(tabs, opts, metrics);
1888 }
1889
1890 let max_label_width = natural
1891 .iter()
1892 .map(|metrics| metrics.label_width)
1893 .max()
1894 .unwrap_or(0);
1895 let mut low = 0usize;
1896 let mut high = max_label_width;
1897 while low < high {
1898 let mid = (low + high).div_ceil(2);
1899 let total = total_width_for_label_cap(&natural, &min_label_widths, fixed_total, mid);
1900 if total <= viewport_width {
1901 low = mid;
1902 } else {
1903 high = mid.saturating_sub(1);
1904 }
1905 }
1906
1907 let cap = low;
1908 let mut label_widths = natural
1909 .iter()
1910 .zip(&min_label_widths)
1911 .map(|(metrics, &min_label_width)| metrics.label_width.min(cap.max(min_label_width)))
1912 .collect::<Vec<_>>();
1913 let total = fixed_total.saturating_add(label_widths.iter().sum::<usize>());
1914 let mut spare = viewport_width.saturating_sub(total);
1915 if spare > 0 {
1916 for (label_width, natural_metrics) in label_widths.iter_mut().zip(&natural) {
1917 if spare == 0 {
1918 break;
1919 }
1920 if *label_width < natural_metrics.label_width {
1921 *label_width += 1;
1922 spare -= 1;
1923 }
1924 }
1925 }
1926
1927 let metrics = tabs
1928 .iter()
1929 .zip(label_widths)
1930 .map(|(tab, label_width)| tab_metrics_with_label_width(tab, opts, label_width))
1931 .collect();
1932 apply_tab_width_lock(tabs, opts, metrics)
1933}
1934
1935fn apply_tab_width_lock(
1936 tabs: &[DraggableTab],
1937 opts: &TabDisplayOptions<'_>,
1938 mut metrics: Vec<TabMetrics>,
1939) -> Vec<TabMetrics> {
1940 let Some(lock) = opts.width_lock else {
1941 return metrics;
1942 };
1943 let (Some(tab), Some(current)) = (tabs.get(lock.index), metrics.get(lock.index)) else {
1944 return metrics;
1945 };
1946 if !is_reorderable_tab(tab) {
1947 return metrics;
1948 }
1949 let fixed_width = current.width.saturating_sub(current.label_width);
1950 let label_width = lock.width.saturating_sub(fixed_width);
1951 metrics[lock.index] = tab_metrics_with_label_width(tab, opts, label_width);
1952 metrics
1953}
1954
1955fn total_width_for_label_cap(
1956 natural: &[TabMetrics],
1957 min_label_widths: &[usize],
1958 fixed_total: usize,
1959 cap: usize,
1960) -> usize {
1961 fixed_total.saturating_add(
1962 natural
1963 .iter()
1964 .zip(min_label_widths)
1965 .map(|(metrics, &min_label_width)| metrics.label_width.min(cap.max(min_label_width)))
1966 .sum::<usize>(),
1967 )
1968}
1969
1970fn total_width_for_metrics(metrics: &[TabMetrics], opts: &TabDisplayOptions<'_>) -> usize {
1971 let tabs_width = metrics.iter().map(|metrics| metrics.width).sum::<usize>();
1972 tabs_width.saturating_add(
1973 separator_width(opts.variant, opts.divider) * metrics.len().saturating_sub(1),
1974 )
1975}
1976
1977fn tab_runs_for_metrics(
1978 metrics: &[TabMetrics],
1979 opts: &TabDisplayOptions<'_>,
1980) -> (Vec<(usize, usize, TabMetrics)>, usize) {
1981 let mut runs = Vec::with_capacity(metrics.len());
1982 let mut total_width = 0usize;
1983 for (i, tab_metrics) in metrics.iter().copied().enumerate() {
1984 let start = total_width;
1985 let end = start.saturating_add(tab_metrics.width);
1986 runs.push((start, end, tab_metrics));
1987 total_width = end;
1988 if i + 1 < metrics.len() {
1989 total_width = total_width.saturating_add(separator_width(opts.variant, opts.divider));
1990 }
1991 }
1992 (runs, total_width)
1993}
1994
1995fn tab_runs_for_viewport(
1996 tabs: &[DraggableTab],
1997 opts: &TabDisplayOptions<'_>,
1998 viewport_width: Option<usize>,
1999) -> (Vec<(usize, usize, TabMetrics)>, usize) {
2000 let metrics = tab_metrics_for_viewport(tabs, opts, viewport_width);
2001 tab_runs_for_metrics(&metrics, opts)
2002}
2003
2004fn tabs_prefix_width_from_metrics(
2005 metrics: &[TabMetrics],
2006 opts: &TabDisplayOptions<'_>,
2007 offset: usize,
2008) -> usize {
2009 let upto = offset.min(metrics.len());
2010 let tabs_width = metrics
2011 .iter()
2012 .take(upto)
2013 .map(|metrics| metrics.width)
2014 .sum::<usize>();
2015 let separators = upto.min(metrics.len().saturating_sub(1));
2016 tabs_width.saturating_add(separator_width(opts.variant, opts.divider) * separators)
2017}
2018
2019#[cfg(test)]
2020fn tabs_prefix_width(tabs: &[DraggableTab], opts: &TabDisplayOptions<'_>, offset: usize) -> usize {
2021 let metrics = tab_metrics_for_viewport(tabs, opts, None);
2022 tabs_prefix_width_from_metrics(&metrics, opts, offset)
2023}
2024
2025pub(crate) fn tab_fully_visible_at_offset(
2026 tabs: &[DraggableTab],
2027 opts: &TabDisplayOptions<'_>,
2028 tab_index: usize,
2029 offset: usize,
2030 viewport_width: usize,
2031) -> bool {
2032 if tabs.is_empty() || tab_index >= tabs.len() || viewport_width == 0 {
2033 return false;
2034 }
2035 let metrics = tab_metrics_for_viewport(tabs, opts, Some(viewport_width));
2036 let tab_start = tabs_prefix_width_from_metrics(&metrics, opts, tab_index);
2037 let tab_width = metrics[tab_index].width;
2038 let tab_end = tab_start.saturating_add(tab_width);
2039 tab_start >= offset && tab_end <= offset.saturating_add(viewport_width)
2040}
2041
2042fn default_overflow_control_label(side: OverflowControlSide, hidden_count: usize) -> Arc<str> {
2043 match side {
2044 OverflowControlSide::Left => Arc::from(format!(" {} ", hidden_count)),
2045 OverflowControlSide::Right => Arc::from(format!(" {}", hidden_count)),
2046 }
2047}
2048
2049fn lookup_icon_override<'a>(
2050 key: &str,
2051 overrides: &'a HashMap<Arc<str>, FileIconOverride>,
2052) -> Option<&'a FileIconOverride> {
2053 let path = Path::new(key);
2054 if let Some(name) = path.file_name().and_then(|n| n.to_str())
2055 && let Some(override_icon) = overrides.get(name)
2056 {
2057 return Some(override_icon);
2058 }
2059 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
2060 return overrides.get(ext);
2061 }
2062 None
2063}
2064
2065pub(crate) fn resolve_tab_icon(
2066 tab: &DraggableTab,
2067 show_file_icons: bool,
2068 file_icon_style: FileIconStyle,
2069 file_icon_palette: &FileIconPalette,
2070 file_icon_overrides: &HashMap<Arc<str>, FileIconOverride>,
2071) -> Option<Span> {
2072 if let Some(leading) = &tab.leading {
2073 return Some(leading.to_span());
2074 }
2075
2076 if let Some(icon) = &tab.icon {
2077 return Some(icon.clone());
2078 }
2079
2080 if !show_file_icons {
2081 return None;
2082 }
2083
2084 let key = tab.path.as_deref().unwrap_or(tab.label.as_ref());
2085 if let Some(override_icon) = lookup_icon_override(key, file_icon_overrides) {
2086 let mut span = Span::new(override_icon.icon.clone());
2087 if let Some(color) = override_icon.color {
2088 span = span.fg(color);
2089 }
2090 return Some(span);
2091 }
2092
2093 match file_icon_style {
2094 FileIconStyle::Text => Some(Span::new("[F]")),
2095 FileIconStyle::NerdFont | FileIconStyle::NerdFontColored => {
2096 let (icon, color) = file_icon(key, file_icon_palette);
2097 let mut span = Span::new(icon);
2098 if matches!(file_icon_style, FileIconStyle::NerdFontColored)
2099 && let Some(color) = color
2100 {
2101 span = span.fg(color);
2102 }
2103 Some(span)
2104 }
2105 }
2106}
2107
2108impl From<DraggableTabBar> for Element {
2109 fn from(value: DraggableTabBar) -> Self {
2110 Element::new(ElementKind::DraggableTabBar(Box::new(value)))
2111 }
2112}
2113
2114impl crate::layout::hash::LayoutHash for DraggableTabBar {
2115 fn layout_hash(
2116 &self,
2117 hasher: &mut impl std::hash::Hasher,
2118 _recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
2119 ) -> Option<()> {
2120 use std::hash::Hash;
2121 self.width.hash(hasher);
2122 self.height.hash(hasher);
2123 self.border.hash(hasher);
2124 self.border_style.hash(hasher);
2125 self.padding.hash(hasher);
2126 self.tabs.len().hash(hasher);
2127 self.active.hash(hasher);
2128 self.divider.hash(hasher);
2129 self.close_symbol.hash(hasher);
2130 self.accent_symbol.hash(hasher);
2131 self.active_accent_symbol.hash(hasher);
2132 self.close_on_hover_only.hash(hasher);
2133 self.tab_max_width.hash(hasher);
2134 self.overflow.hash(hasher);
2135 self.show_overflow_controls.hash(hasher);
2136 self.overflow_labels.left.is_some().hash(hasher);
2137 self.overflow_labels.right.is_some().hash(hasher);
2138 self.empty_text.hash(hasher);
2139 self.scroll_offset.hash(hasher);
2140 self.show_file_icons.hash(hasher);
2141 self.file_icon_style.hash(hasher);
2142 self.variant.hash(hasher);
2143 self.show_close_buttons.hash(hasher);
2144 self.draggable.hash(hasher);
2145 self.drag_preview.hash(hasher);
2146 self.reorder_mode.hash(hasher);
2147 self.drag_threshold.hash(hasher);
2148 Some(())
2149 }
2150}
2151
2152#[cfg(test)]
2153mod tests {
2154 use std::collections::HashMap;
2155
2156 use crate::style::{FileIconPalette, Span, Style};
2157
2158 use super::{
2159 DraggableTab, DraggableTabBar, DraggableTabBarOverflow, DraggableTabBarVariant,
2160 DraggableTabHitPart,
2161 };
2162 use crate::widgets::FileIconStyle;
2163
2164 #[test]
2165 fn hit_at_col_detects_close_region() {
2166 let tabs = vec![DraggableTab::new("main.rs").closeable(true)];
2167 let label_w = "main.rs".chars().count();
2168 let hit = DraggableTabBar::hit_at_col(
2169 &tabs,
2170 DraggableTabBarVariant::Bordered,
2171 '|',
2172 '|',
2173 "x",
2174 true,
2175 1 + label_w + 1,
2176 )
2177 .expect("expected hit");
2178 assert_eq!(hit.index, 0);
2179 assert_eq!(hit.part, DraggableTabHitPart::Close);
2180 }
2181
2182 #[test]
2183 fn hit_at_col_detects_close_region_with_badge() {
2184 let tab = DraggableTab::new("main.rs")
2185 .right_badge(Span::new("M"))
2186 .closeable(true);
2187 let tabs = vec![tab];
2188 let metrics =
2189 super::tab_metrics(&tabs[0], DraggableTabBarVariant::Bordered, '|', "x", true);
2190 let close_col = metrics.close_start.expect("close start");
2191 let hit = DraggableTabBar::hit_at_col(
2192 &tabs,
2193 DraggableTabBarVariant::Bordered,
2194 '|',
2195 '|',
2196 "x",
2197 true,
2198 close_col,
2199 )
2200 .expect("expected hit");
2201 assert_eq!(hit.index, 0);
2202 assert_eq!(hit.part, DraggableTabHitPart::Close);
2203 }
2204
2205 #[test]
2206 fn action_tab_does_not_reserve_close_region() {
2207 let tab = DraggableTab::action("+").closeable(true);
2208 let metrics = super::tab_metrics(&tab, DraggableTabBarVariant::Bordered, '|', "x", true);
2209
2210 assert_eq!(metrics.close_start, None);
2211 assert_eq!(metrics.close_end, None);
2212 assert_eq!(metrics.width, 3);
2213 }
2214
2215 #[test]
2216 fn hit_at_col_returns_none_on_separator() {
2217 let tabs = vec![DraggableTab::new("a"), DraggableTab::new("b")];
2218 let separator_col = 3; let hit = DraggableTabBar::hit_at_col(
2220 &tabs,
2221 DraggableTabBarVariant::Bordered,
2222 '|',
2223 '|',
2224 "x",
2225 true,
2226 separator_col,
2227 );
2228 assert!(hit.is_none());
2229 }
2230
2231 #[test]
2232 fn frame_line_content_width_is_nonzero() {
2233 let tabs = vec![DraggableTab::new("file.rs").closeable(true)];
2234 let width = DraggableTabBar::content_width(
2235 &tabs,
2236 DraggableTabBarVariant::FrameLine,
2237 '|',
2238 '|',
2239 "x",
2240 true,
2241 );
2242 assert!(width > 0);
2243 }
2244
2245 #[test]
2246 fn reorder_index_maps_separator_to_adjacent_tab() {
2247 let tabs = vec![DraggableTab::new("a"), DraggableTab::new("b")];
2248 let separator_col = 3; let idx = DraggableTabBar::reorder_index_at_col(
2250 &tabs,
2251 DraggableTabBarVariant::Bordered,
2252 '|',
2253 '|',
2254 "x",
2255 true,
2256 separator_col,
2257 )
2258 .expect("expected mapped index");
2259 assert_eq!(idx, 1);
2260 }
2261
2262 #[test]
2263 fn adjacent_reorder_waits_until_midpoint_for_wider_neighbor() {
2264 let tabs = vec![DraggableTab::new("a"), DraggableTab::new("very-long-name")];
2265 let at_divider = 3; let opts = super::TabDisplayOptions {
2268 variant: DraggableTabBarVariant::Bordered,
2269 divider: '|',
2270 accent_symbol: '|',
2271 close_symbol: "x",
2272 show_close_buttons: false,
2273 tab_max_width: None,
2274 overflow: super::DraggableTabBarOverflow::Scroll,
2275 show_file_icons: false,
2276 file_icon_style: FileIconStyle::NerdFont,
2277 file_icon_palette: &FileIconPalette::default(),
2278 file_icon_overrides: &HashMap::new(),
2279 width_lock: None,
2280 };
2281 let target = DraggableTabBar::adjacent_reorder_target(&tabs, &opts, 0, at_divider);
2282 assert!(target.is_none());
2283
2284 let second_start =
2285 super::tab_metrics(&tabs[0], DraggableTabBarVariant::Bordered, '|', "x", false).width
2286 + super::separator_width(DraggableTabBarVariant::Bordered, '|');
2287 let second_mid = second_start
2288 + super::tab_metrics(&tabs[1], DraggableTabBarVariant::Bordered, '|', "x", false).width
2289 / 2;
2290 let target = DraggableTabBar::adjacent_reorder_target(&tabs, &opts, 0, second_mid);
2291 assert_eq!(target, Some(1));
2292 }
2293
2294 #[test]
2295 fn adjacent_reorder_does_not_target_trailing_action_tab() {
2296 let tabs = vec![
2297 DraggableTab::new("a"),
2298 DraggableTab::new("b"),
2299 DraggableTab::action("+"),
2300 ];
2301 let opts = super::TabDisplayOptions {
2302 variant: DraggableTabBarVariant::Bordered,
2303 divider: '|',
2304 accent_symbol: '|',
2305 close_symbol: "x",
2306 show_close_buttons: false,
2307 tab_max_width: None,
2308 overflow: super::DraggableTabBarOverflow::Scroll,
2309 show_file_icons: false,
2310 file_icon_style: FileIconStyle::NerdFont,
2311 file_icon_palette: &FileIconPalette::default(),
2312 file_icon_overrides: &HashMap::new(),
2313 width_lock: None,
2314 };
2315 let action_mid = super::tabs_prefix_width(&tabs, &opts, 2)
2316 + super::tab_metrics(&tabs[2], DraggableTabBarVariant::Bordered, '|', "x", false).width
2317 / 2;
2318
2319 let target = DraggableTabBar::adjacent_reorder_target(&tabs, &opts, 1, action_mid);
2320
2321 assert_eq!(target, None);
2322 }
2323
2324 #[test]
2325 fn tab_prefix_includes_the_separator_before_the_target() {
2326 let tabs = vec![DraggableTab::new("alpha"), DraggableTab::new("beta")];
2327 let overrides = HashMap::new();
2328 let opts = super::TabDisplayOptions {
2329 variant: DraggableTabBarVariant::Bordered,
2330 divider: '|',
2331 accent_symbol: '|',
2332 close_symbol: "x",
2333 show_close_buttons: false,
2334 tab_max_width: None,
2335 overflow: super::DraggableTabBarOverflow::Scroll,
2336 show_file_icons: false,
2337 file_icon_style: FileIconStyle::NerdFont,
2338 file_icon_palette: &FileIconPalette::default(),
2339 file_icon_overrides: &overrides,
2340 width_lock: None,
2341 };
2342 let first =
2343 super::tab_metrics(&tabs[0], DraggableTabBarVariant::Bordered, '|', "x", false).width;
2344
2345 assert_eq!(
2346 super::tabs_prefix_width(&tabs, &opts, 1),
2347 first + super::separator_width(DraggableTabBarVariant::Bordered, '|')
2348 );
2349 }
2350
2351 #[test]
2352 fn on_drop_reorder_maps_action_hit_to_previous_tab() {
2353 let tabs = vec![
2354 DraggableTab::new("a"),
2355 DraggableTab::new("b"),
2356 DraggableTab::action("+"),
2357 ];
2358 let opts = super::TabDisplayOptions {
2359 variant: DraggableTabBarVariant::Bordered,
2360 divider: '|',
2361 accent_symbol: '|',
2362 close_symbol: "x",
2363 show_close_buttons: false,
2364 tab_max_width: None,
2365 overflow: super::DraggableTabBarOverflow::Scroll,
2366 show_file_icons: false,
2367 file_icon_style: FileIconStyle::NerdFont,
2368 file_icon_palette: &FileIconPalette::default(),
2369 file_icon_overrides: &HashMap::new(),
2370 width_lock: None,
2371 };
2372 let action_col = super::tabs_prefix_width(&tabs, &opts, 2);
2373
2374 let raw_target =
2375 DraggableTabBar::reorder_index_at_col_with_options(&tabs, &opts, action_col);
2376 let reorder_target = super::reorder_target_at_col_with_options(&tabs, &opts, action_col);
2377
2378 assert_eq!(raw_target, Some(2));
2379 assert_eq!(reorder_target, Some(1));
2380 }
2381
2382 #[test]
2383 fn viewport_layout_keeps_partially_visible_tab() {
2384 let tabs = vec![
2385 DraggableTab::new("alpha"),
2386 DraggableTab::new("beta-gamma"),
2387 DraggableTab::new("delta"),
2388 ];
2389 let first =
2390 super::tab_metrics(&tabs[0], DraggableTabBarVariant::Bordered, '|', "x", false).width;
2391 let sep = super::separator_width(DraggableTabBarVariant::Bordered, '|');
2392 let second =
2393 super::tab_metrics(&tabs[1], DraggableTabBarVariant::Bordered, '|', "x", false).width;
2394 let viewport_width = first + sep + (second / 2).max(1);
2395
2396 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2397 let layout = DraggableTabBar::viewport_layout(
2398 &tabs,
2399 &super::TabDisplayOptions {
2400 variant: DraggableTabBarVariant::Bordered,
2401 divider: '|',
2402 accent_symbol: '|',
2403 close_symbol: "x",
2404 show_close_buttons: false,
2405 tab_max_width: None,
2406 overflow: super::DraggableTabBarOverflow::Scroll,
2407 show_file_icons: false,
2408 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2409 file_icon_palette: &FileIconPalette::default(),
2410 file_icon_overrides: &overrides,
2411 width_lock: None,
2412 },
2413 &super::TabViewportOptions {
2414 scroll_offset: 0,
2415 viewport_width,
2416 show_overflow_controls: true,
2417 overflow_labels: Default::default(),
2418 },
2419 );
2420
2421 assert_eq!(layout.visible_tabs.len(), 2);
2422 assert_eq!(layout.visible_tabs[0].index, 0);
2423 assert_eq!(layout.visible_tabs[1].index, 1);
2424 assert!(
2425 layout.visible_tabs[1]
2426 .end
2427 .saturating_sub(layout.visible_tabs[1].start)
2428 < second
2429 );
2430 assert_eq!(layout.hidden_right, 2);
2431 }
2432
2433 #[test]
2434 fn viewport_layout_can_clip_left_tab() {
2435 let tabs = vec![
2436 DraggableTab::new("alpha"),
2437 DraggableTab::new("beta"),
2438 DraggableTab::new("gamma"),
2439 ];
2440 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2441 let layout = DraggableTabBar::viewport_layout(
2442 &tabs,
2443 &super::TabDisplayOptions {
2444 variant: DraggableTabBarVariant::Bordered,
2445 divider: '|',
2446 accent_symbol: '|',
2447 close_symbol: "x",
2448 show_close_buttons: false,
2449 tab_max_width: None,
2450 overflow: super::DraggableTabBarOverflow::Scroll,
2451 show_file_icons: false,
2452 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2453 file_icon_palette: &FileIconPalette::default(),
2454 file_icon_overrides: &overrides,
2455 width_lock: None,
2456 },
2457 &super::TabViewportOptions {
2458 scroll_offset: 1,
2459 viewport_width: 10,
2460 show_overflow_controls: true,
2461 overflow_labels: Default::default(),
2462 },
2463 );
2464
2465 assert!(layout.hidden_left > 0);
2466 let first = layout.visible_tabs.first().expect("expected visible tabs");
2467 assert_eq!(first.index, 0);
2468 assert!(first.clip_left > 0);
2469 }
2470
2471 #[test]
2472 fn shrink_then_scroll_fits_tabs_before_scrolling() {
2473 let tabs = vec![
2474 DraggableTab::new("abcdef"),
2475 DraggableTab::new("abcdef"),
2476 DraggableTab::new("abcdef"),
2477 ];
2478 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2479 let opts = super::TabDisplayOptions {
2480 variant: DraggableTabBarVariant::Bordered,
2481 divider: '|',
2482 accent_symbol: '|',
2483 close_symbol: "x",
2484 show_close_buttons: false,
2485 tab_max_width: None,
2486 overflow: DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width: 5 },
2487 show_file_icons: false,
2488 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2489 file_icon_palette: &FileIconPalette::default(),
2490 file_icon_overrides: &overrides,
2491 width_lock: None,
2492 };
2493
2494 let layout = DraggableTabBar::viewport_layout(
2495 &tabs,
2496 &opts,
2497 &super::TabViewportOptions {
2498 scroll_offset: 0,
2499 viewport_width: 20,
2500 show_overflow_controls: true,
2501 overflow_labels: Default::default(),
2502 },
2503 );
2504
2505 assert_eq!(layout.hidden_left, 0);
2506 assert_eq!(layout.hidden_right, 0);
2507 assert!(layout.left_control.is_none());
2508 assert!(layout.right_control.is_none());
2509 assert_eq!(layout.visible_tabs.len(), 3);
2510 assert_eq!(layout.visible_tabs[0].metrics.label_width, 4);
2511 assert_eq!(layout.visible_tabs.last().expect("last tab").end, 20);
2512 }
2513
2514 #[test]
2515 fn shrink_then_scroll_scrolls_after_min_widths() {
2516 let tabs = vec![
2517 DraggableTab::new("abcdef"),
2518 DraggableTab::new("abcdef"),
2519 DraggableTab::new("abcdef"),
2520 ];
2521 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2522 let opts = super::TabDisplayOptions {
2523 variant: DraggableTabBarVariant::Bordered,
2524 divider: '|',
2525 accent_symbol: '|',
2526 close_symbol: "x",
2527 show_close_buttons: false,
2528 tab_max_width: None,
2529 overflow: DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width: 5 },
2530 show_file_icons: false,
2531 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2532 file_icon_palette: &FileIconPalette::default(),
2533 file_icon_overrides: &overrides,
2534 width_lock: None,
2535 };
2536
2537 let layout = DraggableTabBar::viewport_layout(
2538 &tabs,
2539 &opts,
2540 &super::TabViewportOptions {
2541 scroll_offset: 0,
2542 viewport_width: 14,
2543 show_overflow_controls: true,
2544 overflow_labels: Default::default(),
2545 },
2546 );
2547
2548 assert_eq!(layout.visible_tabs[0].metrics.width, 5);
2549 assert_eq!(layout.visible_tabs[0].metrics.label_width, 3);
2550 assert_eq!(layout.hidden_left, 0);
2551 assert!(layout.hidden_right > 0);
2552 assert!(layout.right_control.is_some());
2553 }
2554
2555 #[test]
2556 fn shrink_then_scroll_hit_testing_uses_shrunken_widths() {
2557 let tabs = vec![
2558 DraggableTab::new("abcdef"),
2559 DraggableTab::new("abcdef"),
2560 DraggableTab::new("abcdef"),
2561 ];
2562 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2563 let opts = super::TabDisplayOptions {
2564 variant: DraggableTabBarVariant::Bordered,
2565 divider: '|',
2566 accent_symbol: '|',
2567 close_symbol: "x",
2568 show_close_buttons: false,
2569 tab_max_width: None,
2570 overflow: DraggableTabBarOverflow::ShrinkThenScroll { min_tab_width: 5 },
2571 show_file_icons: false,
2572 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2573 file_icon_palette: &FileIconPalette::default(),
2574 file_icon_overrides: &overrides,
2575 width_lock: None,
2576 };
2577
2578 let hit = DraggableTabBar::hit_target_at_view_col(
2579 &tabs,
2580 &opts,
2581 &super::TabViewportOptions {
2582 scroll_offset: 0,
2583 viewport_width: 20,
2584 show_overflow_controls: true,
2585 overflow_labels: Default::default(),
2586 },
2587 7,
2588 );
2589
2590 assert!(matches!(
2591 hit,
2592 Some(super::DraggableTabHitTarget::Tab(super::DraggableTabHit {
2593 index: 1,
2594 part: DraggableTabHitPart::Body,
2595 }))
2596 ));
2597 }
2598
2599 #[test]
2600 fn overflow_right_label_has_left_padding() {
2601 assert_eq!(
2602 super::OverflowLabels::default()
2603 .label(super::OverflowControlSide::Right, 1)
2604 .as_ref(),
2605 " 1"
2606 );
2607 }
2608
2609 #[test]
2610 fn custom_overflow_labels_replace_defaults() {
2611 let labels = super::OverflowLabels {
2612 left: Some(std::sync::Arc::new(|hidden| {
2613 std::sync::Arc::from(format!("<{hidden}"))
2614 })),
2615 right: None,
2616 };
2617
2618 assert_eq!(
2619 labels.label(super::OverflowControlSide::Left, 3).as_ref(),
2620 "<3"
2621 );
2622 assert_eq!(labels.width(super::OverflowControlSide::Left, 3), 2);
2623 assert_eq!(
2624 labels.label(super::OverflowControlSide::Right, 3).as_ref(),
2625 super::OverflowLabels::default()
2626 .label(super::OverflowControlSide::Right, 3)
2627 .as_ref()
2628 );
2629 }
2630
2631 #[test]
2632 fn custom_overflow_labels_drive_layout_and_hit_testing() {
2633 let tabs = vec![
2634 DraggableTab::new("abcdef"),
2635 DraggableTab::new("abcdef"),
2636 DraggableTab::new("abcdef"),
2637 ];
2638 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2639 let opts = super::TabDisplayOptions {
2640 variant: DraggableTabBarVariant::Bordered,
2641 divider: '|',
2642 accent_symbol: '|',
2643 close_symbol: "x",
2644 show_close_buttons: false,
2645 tab_max_width: None,
2646 overflow: super::DraggableTabBarOverflow::Scroll,
2647 show_file_icons: false,
2648 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2649 file_icon_palette: &FileIconPalette::default(),
2650 file_icon_overrides: &overrides,
2651 width_lock: None,
2652 };
2653 let viewport = super::TabViewportOptions {
2655 scroll_offset: 0,
2656 viewport_width: 20,
2657 show_overflow_controls: true,
2658 overflow_labels: super::OverflowLabels {
2659 left: None,
2660 right: Some(std::sync::Arc::new(|hidden| {
2661 std::sync::Arc::from(format!(" +{hidden}> "))
2662 })),
2663 },
2664 };
2665
2666 let layout = DraggableTabBar::viewport_layout(&tabs, &opts, &viewport);
2667 let right = layout.right_control.clone().expect("right control");
2668 assert_eq!(right.label.as_ref(), " +2> ");
2669 assert_eq!(right.start, 15);
2670 assert_eq!(right.end, 20);
2671 assert_eq!(layout.content_width, 15);
2672
2673 assert!(matches!(
2674 DraggableTabBar::hit_target_at_view_col(&tabs, &opts, &viewport, 15),
2675 Some(super::DraggableTabHitTarget::Overflow(
2676 super::OverflowControlSide::Right
2677 ))
2678 ));
2679 assert!(!matches!(
2681 DraggableTabBar::hit_target_at_view_col(&tabs, &opts, &viewport, 14),
2682 Some(super::DraggableTabHitTarget::Overflow(_))
2683 ));
2684 }
2685
2686 #[test]
2687 fn overflow_label_builders_feed_the_node() {
2688 let bar = DraggableTabBar::new()
2689 .overflow_left_label(|hidden| std::sync::Arc::from(format!("[{hidden}")))
2690 .overflow_right_label(|hidden| std::sync::Arc::from(format!("{hidden}]")));
2691 let node: crate::widgets::internal::DraggableTabBarNode = bar.into();
2692 let viewport = node.viewport_options(20);
2693
2694 assert_eq!(
2695 viewport
2696 .overflow_labels
2697 .label(super::OverflowControlSide::Left, 4)
2698 .as_ref(),
2699 "[4"
2700 );
2701 assert_eq!(
2702 viewport
2703 .overflow_labels
2704 .label(super::OverflowControlSide::Right, 4)
2705 .as_ref(),
2706 "4]"
2707 );
2708 }
2709
2710 #[test]
2711 fn empty_text_builders_feed_the_node() {
2712 let bar = DraggableTabBar::new()
2713 .empty_text("No open tabs")
2714 .empty_text_style(Style::default().dim());
2715 let node: crate::widgets::internal::DraggableTabBarNode = bar.into();
2716
2717 assert_eq!(node.empty_text.as_deref(), Some("No open tabs"));
2718 assert_eq!(node.empty_text_style, Style::default().dim());
2719 }
2720
2721 #[test]
2722 fn stepping_right_reaches_visible_right_edge() {
2723 let tabs = vec![
2724 DraggableTab::new("alpha.rs"),
2725 DraggableTab::new("beta-long-file-name.rs"),
2726 DraggableTab::new("gamma.rs"),
2727 DraggableTab::new("delta.rs"),
2728 DraggableTab::new("epsilon.rs"),
2729 ];
2730 let overrides: HashMap<std::sync::Arc<str>, super::FileIconOverride> = HashMap::new();
2731 let mut offset = 0usize;
2732 let viewport_width = 24usize;
2733 let disp_opts = super::TabDisplayOptions {
2734 variant: DraggableTabBarVariant::Bordered,
2735 divider: '|',
2736 accent_symbol: '|',
2737 close_symbol: "x",
2738 show_close_buttons: false,
2739 tab_max_width: None,
2740 overflow: super::DraggableTabBarOverflow::Scroll,
2741 show_file_icons: false,
2742 file_icon_style: crate::widgets::FileIconStyle::NerdFont,
2743 file_icon_palette: &FileIconPalette::default(),
2744 file_icon_overrides: &overrides,
2745 width_lock: None,
2746 };
2747
2748 for _ in 0..64 {
2749 let next = DraggableTabBar::scroll_offset_for_step(
2750 &tabs,
2751 &disp_opts,
2752 &super::TabViewportOptions {
2753 scroll_offset: offset,
2754 viewport_width,
2755 show_overflow_controls: true,
2756 overflow_labels: Default::default(),
2757 },
2758 true,
2759 super::TAB_SCROLL_STEP_CHARS,
2760 );
2761 if next == offset {
2762 break;
2763 }
2764 offset = next;
2765 }
2766
2767 let layout = DraggableTabBar::viewport_layout(
2768 &tabs,
2769 &disp_opts,
2770 &super::TabViewportOptions {
2771 scroll_offset: offset,
2772 viewport_width,
2773 show_overflow_controls: true,
2774 overflow_labels: Default::default(),
2775 },
2776 );
2777
2778 assert_eq!(
2779 layout.hidden_right, 0,
2780 "offset={} content_width={} hidden_left={} layout={:?}",
2781 layout.offset, layout.content_width, layout.hidden_left, layout
2782 );
2783 }
2784}