1use std::rc::Rc;
28
29use iced::advanced::layout::{self, Layout};
30use iced::advanced::mouse::{self, click};
31use iced::advanced::overlay;
32use iced::advanced::renderer;
33use iced::advanced::widget::operation::scrollable::{AbsoluteOffset, RelativeOffset, Scrollable};
34use iced::advanced::widget::{Operation, Tree, Widget, tree};
35use iced::advanced::{Clipboard, Renderer as _, Shell};
36use iced::alignment::Vertical;
37use iced::border::Radius;
38use iced::keyboard::{self, key::Named};
39use iced::touch::{self, Finger};
40use iced::widget::{self, button::Status, container, row};
41use iced::{
42 Alignment, Background, Border, Color, Element, Event as Input, Length, Padding, Point,
43 Rectangle, Renderer, Size, Subscription, Theme, Vector,
44};
45
46use crate::data::ellipsis::ellipsis;
47use crate::icon::{Glyph, opacity, themed};
48use crate::keys::{self, Chord, Keymap};
49use crate::overlay::anchored::{Align, Placement, Side, anchored};
50use crate::overlay::context_menu::{self, KEYBOARD_GAP};
51use crate::overlay::dropdown_menu::{self, Entry, item, radio_item, separator};
52use crate::overlay::menu::{self, RowStatus};
53use crate::overlay::scrolling::{fitted, scrolling};
54use crate::overlay::tooltip;
55use crate::primitives::button::Size as ButtonSize;
56use crate::primitives::icon_button::icon_button;
57use crate::theme::{Tokens, fade, mix, radius, space, text_size};
58
59pub const HEIGHT: f32 = 36.0;
61
62pub const MAX_LABEL_WIDTH: f32 = 200.0;
64
65pub const PINNED_LABEL_WIDTH: f32 = 80.0;
67
68pub const OVERFLOW_WIDTH: f32 = 320.0;
70
71pub const DRAG_THRESHOLD: f32 = 4.0;
73
74const SLOT: f32 = 16.0;
76const CLOSE_ICON: f32 = 12.0;
77const ICON: f32 = 16.0;
78const DOT: f32 = 8.0;
79const PADDING_X: f32 = 12.0;
80const DIVIDER_HEIGHT: f32 = 16.0;
81const INDICATOR_WIDTH: f32 = 2.0;
82const WHEEL_LINE: f32 = 40.0;
84
85#[derive(Debug, Clone, PartialEq)]
87pub struct Document<Id> {
88 pub id: Id,
89 pub label: String,
90 pub icon: Option<Glyph>,
91 pub modified: bool,
93 pub pinned: bool,
94 pub closable: bool,
96 pub locked: bool,
98}
99
100pub fn document<Id>(id: Id, label: impl Into<String>) -> Document<Id> {
102 Document {
103 id,
104 label: label.into(),
105 icon: None,
106 modified: false,
107 pinned: false,
108 closable: true,
109 locked: false,
110 }
111}
112
113impl<Id> Document<Id> {
114 pub fn icon(mut self, glyph: Glyph) -> Self {
115 self.icon = Some(glyph);
116 self
117 }
118
119 pub fn modified(mut self, modified: bool) -> Self {
120 self.modified = modified;
121 self
122 }
123
124 pub fn pinned(mut self, pinned: bool) -> Self {
125 self.pinned = pinned;
126 self
127 }
128
129 pub fn closable(mut self, closable: bool) -> Self {
132 self.closable = closable;
133 self
134 }
135
136 pub fn locked(mut self, locked: bool) -> Self {
140 self.locked = locked;
141 self
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum MenuItem {
148 Close,
149 CloseOthers,
150 CloseToTheRight,
151 CloseSaved,
152 CloseAll,
153 Pin,
154 Unpin,
155 Custom(&'static str),
158}
159
160#[derive(Debug, Clone, Copy, PartialEq)]
162pub enum Event<Id> {
163 Activate(Id),
165 Next,
167 Previous,
169 RecentNext,
172 RecentPrevious,
174 EndCycle,
177 Close(Id),
179 CloseOthers(Id),
181 CloseToTheRight(Id),
183 CloseSaved,
186 CloseAll,
188 Move(Id, usize),
191 Pin(Id, bool),
194 New,
197 Menu(context_menu::Event<MenuItem, Id>),
199 Overflow(dropdown_menu::Event<Id>),
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
205pub struct CycleEnd;
206
207impl<Id> From<CycleEnd> for Event<Id> {
208 fn from(_: CycleEnd) -> Self {
209 Event::EndCycle
210 }
211}
212
213pub fn cycle_end() -> Subscription<CycleEnd> {
216 keyboard::listen().filter_map(|event| match event {
217 keyboard::Event::ModifiersChanged(modifiers) if modifiers.is_empty() => Some(CycleEnd),
218 _ => None,
219 })
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum Output<Id> {
225 Activated(Id),
227 CloseRequested(Vec<Id>),
231 Moved { id: Id, from: usize, to: usize },
233 Pinned(Id, bool),
235 NewTab,
237 Custom(&'static str, Id),
239}
240
241#[derive(Debug, Clone, PartialEq)]
243pub struct State<Id> {
244 documents: Vec<Document<Id>>,
245 active: Option<Id>,
246 recent: Vec<Id>,
248 cycle: Option<usize>,
250 menu: context_menu::State<MenuItem, Id>,
251 extra: Vec<Entry<MenuItem>>,
252 overflow: dropdown_menu::State<Id>,
253}
254
255impl<Id: Copy + PartialEq> State<Id> {
256 pub fn new(documents: impl IntoIterator<Item = Document<Id>>) -> Self {
259 let (mut documents, others): (Vec<_>, Vec<_>) =
260 documents.into_iter().partition(|document| document.pinned);
261 documents.extend(others);
262 let recent = documents.iter().map(|document| document.id).collect();
263 Self {
264 active: documents.first().map(|document| document.id),
265 documents,
266 recent,
267 cycle: None,
268 menu: context_menu::State::new([]),
269 extra: Vec::new(),
270 overflow: dropdown_menu::State::new([]),
271 }
272 }
273
274 pub fn with_active(mut self, id: Id) -> Self {
276 let _ = self.activate(id);
277 self
278 }
279
280 pub fn with_menu_items(mut self, entries: impl IntoIterator<Item = Entry<MenuItem>>) -> Self {
283 self.extra = entries.into_iter().collect();
284 self
285 }
286
287 pub fn documents(&self) -> &[Document<Id>] {
289 &self.documents
290 }
291
292 pub fn document(&self, id: Id) -> Option<&Document<Id>> {
293 self.documents.iter().find(|document| document.id == id)
294 }
295
296 pub fn active(&self) -> Option<Id> {
297 self.active
298 }
299
300 pub fn is_active(&self, id: Id) -> bool {
301 self.active == Some(id)
302 }
303
304 pub fn recent(&self) -> &[Id] {
306 &self.recent
307 }
308
309 pub fn menu(&self) -> &context_menu::State<MenuItem, Id> {
311 &self.menu
312 }
313
314 pub fn overflow(&self) -> &dropdown_menu::State<Id> {
316 &self.overflow
317 }
318
319 pub fn pinned_count(&self) -> usize {
321 self.documents
322 .iter()
323 .take_while(|document| document.pinned)
324 .count()
325 }
326
327 fn index_of(&self, id: Id) -> Option<usize> {
328 self.documents.iter().position(|document| document.id == id)
329 }
330
331 fn active_index(&self) -> Option<usize> {
332 self.active.and_then(|id| self.index_of(id))
333 }
334
335 pub fn open(&mut self, document: Document<Id>) {
338 self.end_cycle();
339 let id = document.id;
340 if self.index_of(id).is_none() {
341 let index = if document.pinned {
342 self.pinned_count()
343 } else {
344 self.documents.len()
345 };
346 self.documents.insert(index, document);
347 self.recent.push(id);
348 }
349 let _ = self.activate(id);
350 }
351
352 pub fn close(&mut self, ids: &[Id]) -> Option<Id> {
355 self.end_cycle();
356 self.documents
357 .retain(|document| !ids.contains(&document.id));
358 self.recent.retain(|id| !ids.contains(id));
359 let closed = self.active.is_some_and(|id| ids.contains(&id));
360 if !closed {
361 return None;
362 }
363 self.active = self.recent.first().copied();
364 self.active
365 }
366
367 pub fn set_modified(&mut self, id: Id, modified: bool) {
369 if let Some(document) = self.document_mut(id) {
370 document.modified = modified;
371 }
372 }
373
374 pub fn set_label(&mut self, id: Id, label: impl Into<String>) {
375 if let Some(document) = self.document_mut(id) {
376 document.label = label.into();
377 }
378 }
379
380 fn document_mut(&mut self, id: Id) -> Option<&mut Document<Id>> {
381 self.documents.iter_mut().find(|document| document.id == id)
382 }
383
384 pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
386 if !matches!(event, Event::RecentNext | Event::RecentPrevious) {
387 self.end_cycle();
388 }
389 match event {
390 Event::Activate(id) => self.activate(id),
391 Event::Next => self.step(true),
392 Event::Previous => self.step(false),
393 Event::RecentNext => self.walk(true),
394 Event::RecentPrevious => self.walk(false),
395 Event::EndCycle => None,
396 Event::Close(id) => self.request(|document| document.id == id && document.closable),
397 Event::CloseOthers(id) => {
398 self.request(|document| document.id != id && in_bulk_close(document))
399 }
400 Event::CloseToTheRight(id) => {
401 let index = self.index_of(id)?;
402 let right: Vec<Id> = self.documents[index + 1..]
403 .iter()
404 .filter(|document| in_bulk_close(document))
405 .map(|document| document.id)
406 .collect();
407 (!right.is_empty()).then_some(Output::CloseRequested(right))
408 }
409 Event::CloseSaved => {
410 self.request(|document| !document.modified && in_bulk_close(document))
411 }
412 Event::CloseAll => self.request(in_bulk_close),
413 Event::Move(id, to) => self.move_to(id, to),
414 Event::Pin(id, pinned) => self.pin(id, pinned),
415 Event::New => Some(Output::NewTab),
416 Event::Menu(event) => self.menu_event(event),
417 Event::Overflow(event) => self.overflow_event(event),
418 }
419 }
420
421 fn activate(&mut self, id: Id) -> Option<Output<Id>> {
422 self.index_of(id)?;
423 if self.active == Some(id) {
424 return None;
425 }
426 self.active = Some(id);
427 self.touch(id);
428 Some(Output::Activated(id))
429 }
430
431 fn touch(&mut self, id: Id) {
433 self.recent.retain(|recent| *recent != id);
434 self.recent.insert(0, id);
435 }
436
437 fn step(&mut self, forward: bool) -> Option<Output<Id>> {
438 let len = self.documents.len();
439 let current = self.active_index()?;
440 let next = if forward {
441 (current + 1) % len
442 } else {
443 (current + len - 1) % len
444 };
445 let id = self.documents[next].id;
446 self.activate(id)
447 }
448
449 fn walk(&mut self, forward: bool) -> Option<Output<Id>> {
450 let len = self.recent.len();
451 if len < 2 {
452 return None;
453 }
454 let current = self.cycle.unwrap_or(0);
455 let next = if forward {
456 (current + 1) % len
457 } else {
458 (current + len - 1) % len
459 };
460 self.cycle = Some(next);
461 let id = self.recent[next];
462 self.active = Some(id);
463 Some(Output::Activated(id))
464 }
465
466 fn end_cycle(&mut self) {
467 if self.cycle.take().is_none() {
468 return;
469 }
470 if let Some(id) = self.active {
471 self.touch(id);
472 }
473 }
474
475 fn request(&self, close: impl Fn(&Document<Id>) -> bool) -> Option<Output<Id>> {
476 let ids: Vec<Id> = self
477 .documents
478 .iter()
479 .filter(|document| close(document))
480 .map(|document| document.id)
481 .collect();
482 (!ids.is_empty()).then_some(Output::CloseRequested(ids))
483 }
484
485 fn move_to(&mut self, id: Id, to: usize) -> Option<Output<Id>> {
486 let from = self.index_of(id)?;
487 let pinned = self.pinned_count();
488 let (start, end) = if from < pinned {
489 (0, pinned - 1)
490 } else {
491 (pinned, self.documents.len() - 1)
492 };
493 let to = to.clamp(start, end);
494 if to == from {
495 return None;
496 }
497 let document = self.documents.remove(from);
498 self.documents.insert(to, document);
499 Some(Output::Moved { id, from, to })
500 }
501
502 fn pin(&mut self, id: Id, pinned: bool) -> Option<Output<Id>> {
503 let from = self.index_of(id)?;
504 let document = &self.documents[from];
505 if document.locked || document.pinned == pinned {
506 return None;
507 }
508 let mut document = self.documents.remove(from);
509 document.pinned = pinned;
510 let to = self.pinned_count();
511 self.documents.insert(to, document);
512 Some(Output::Pinned(id, pinned))
513 }
514
515 fn menu_event(&mut self, event: context_menu::Event<MenuItem, Id>) -> Option<Output<Id>> {
516 let event = match event {
517 context_menu::Event::Open(target, _) => {
520 context_menu::Event::Open(target, Point::new(0.0, HEIGHT + KEYBOARD_GAP))
521 }
522 event => event,
523 };
524 if let context_menu::Event::Open(target, _)
525 | context_menu::Event::OpenFromKeyboard(target) = event
526 {
527 self.menu = context_menu::State::new(self.menu_entries(target)?);
528 }
529 let dropdown_menu::Output::Activated(item) = self.menu.update(event)? else {
530 return None;
531 };
532 let target = *self.menu.target()?;
533 let event = match item {
534 MenuItem::Close => Event::Close(target),
535 MenuItem::CloseOthers => Event::CloseOthers(target),
536 MenuItem::CloseToTheRight => Event::CloseToTheRight(target),
537 MenuItem::CloseSaved => Event::CloseSaved,
538 MenuItem::CloseAll => Event::CloseAll,
539 MenuItem::Pin => Event::Pin(target, true),
540 MenuItem::Unpin => Event::Pin(target, false),
541 MenuItem::Custom(name) => return Some(Output::Custom(name, target)),
542 };
543 self.update(event)
544 }
545
546 fn menu_entries(&self, target: Id) -> Option<Vec<Entry<MenuItem>>> {
549 let index = self.index_of(target)?;
550 let document = &self.documents[index];
551 let ids = |range: &[Document<Id>], keep: &dyn Fn(&Document<Id>) -> bool| -> Vec<usize> {
552 range
553 .iter()
554 .enumerate()
555 .filter(|(_, d)| in_bulk_close(d) && keep(d))
556 .map(|(i, _)| i)
557 .collect()
558 };
559 let all = ids(&self.documents, &|_| true);
560 let right: Vec<usize> = ids(&self.documents[index + 1..], &|_| true)
561 .into_iter()
562 .map(|i| i + index + 1)
563 .collect();
564 let choices = [
566 (
567 MenuItem::Close,
568 "Close",
569 if document.closable {
570 vec![index]
571 } else {
572 Vec::new()
573 },
574 ),
575 (
576 MenuItem::CloseOthers,
577 "Close others",
578 all.iter().copied().filter(|i| *i != index).collect(),
579 ),
580 (MenuItem::CloseToTheRight, "Close to the right", right),
581 (
582 MenuItem::CloseSaved,
583 "Close saved",
584 ids(&self.documents, &|d| !d.modified),
585 ),
586 (MenuItem::CloseAll, "Close all", all),
587 ];
588 let close = offered_closes(&choices);
589 let pin = match (document.locked, document.pinned) {
590 (true, _) => Vec::new(),
591 (false, true) => vec![item(MenuItem::Unpin, "Unpin")],
592 (false, false) => vec![item(MenuItem::Pin, "Pin")],
593 };
594
595 let groups: Vec<Vec<Entry<MenuItem>>> = [close, pin, self.extra.clone()]
596 .into_iter()
597 .filter(|group| !group.is_empty())
598 .collect();
599 let entries = groups.join(&separator());
600 (!entries.is_empty()).then_some(entries)
601 }
602
603 fn overflow_event(&mut self, event: dropdown_menu::Event<Id>) -> Option<Output<Id>> {
604 if !self.overflow.is_open() {
605 self.overflow = dropdown_menu::State::new(self.documents.iter().map(|document| {
606 let entry = radio_item(
607 document.id,
608 document.label.clone(),
609 self.active == Some(document.id),
610 );
611 if document.modified {
612 entry.shortcut("Modified")
613 } else {
614 entry
615 }
616 }));
617 }
618 match self.overflow.update(event)? {
619 dropdown_menu::Output::Activated(id) | dropdown_menu::Output::Selected(id) => {
620 self.activate(id)
621 }
622 dropdown_menu::Output::Toggled(..) => None,
623 }
624 }
625}
626
627fn offered_closes(choices: &[(MenuItem, &str, Vec<usize>)]) -> Vec<Entry<MenuItem>> {
631 let priority = [
632 MenuItem::Close,
633 MenuItem::CloseAll,
634 MenuItem::CloseOthers,
635 MenuItem::CloseToTheRight,
636 MenuItem::CloseSaved,
637 ];
638 let mut kept: Vec<&(MenuItem, &str, Vec<usize>)> = Vec::new();
639 for wanted in priority {
640 let Some(choice) = choices.iter().find(|(id, ..)| *id == wanted) else {
641 continue;
642 };
643 let (_, _, closes) = choice;
644 if closes.is_empty() || kept.iter().any(|(_, _, other)| other == closes) {
645 continue;
646 }
647 kept.push(choice);
648 }
649 choices
650 .iter()
651 .filter(|choice| kept.iter().any(|kept| kept.0 == choice.0))
652 .map(|(id, label, _)| item(*id, *label))
653 .collect()
654}
655
656fn in_bulk_close<Id>(document: &Document<Id>) -> bool {
659 document.closable && !document.pinned
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
664pub enum Action {
665 RecentNext,
666 RecentPrevious,
667 Next,
668 Previous,
669 MoveLeft,
670 MoveRight,
671 Close,
672 New,
673 Tab(u8),
675 LastTab,
677 OpenMenu,
679}
680
681impl Action {
682 pub fn event<Id: Copy + PartialEq>(self, state: &State<Id>) -> Option<Event<Id>> {
685 let active = state.active;
686 let index = state.active_index();
687 match self {
688 Action::RecentNext => Some(Event::RecentNext),
689 Action::RecentPrevious => Some(Event::RecentPrevious),
690 Action::Next => Some(Event::Next),
691 Action::Previous => Some(Event::Previous),
692 Action::MoveLeft => {
693 let index = index?.checked_sub(1)?;
694 Some(Event::Move(active?, index))
695 }
696 Action::MoveRight => Some(Event::Move(active?, index? + 1)),
697 Action::Close => active
698 .filter(|id| state.document(*id).is_some_and(|d| d.closable))
699 .map(Event::Close),
700 Action::New => Some(Event::New),
701 Action::Tab(position) => {
702 let index = usize::from(position).checked_sub(1)?;
703 state
704 .documents
705 .get(index)
706 .map(|document| Event::Activate(document.id))
707 }
708 Action::LastTab => state
709 .documents
710 .last()
711 .map(|document| Event::Activate(document.id)),
712 Action::OpenMenu => {
713 active.map(|id| Event::Menu(context_menu::Event::OpenFromKeyboard(id)))
714 }
715 }
716 }
717}
718
719impl keys::Action for Action {
720 const ALL: &'static [Self] = &[
721 Action::RecentNext,
722 Action::RecentPrevious,
723 Action::Next,
724 Action::Previous,
725 Action::MoveLeft,
726 Action::MoveRight,
727 Action::Close,
728 Action::New,
729 Action::Tab(1),
730 Action::Tab(2),
731 Action::Tab(3),
732 Action::Tab(4),
733 Action::Tab(5),
734 Action::Tab(6),
735 Action::Tab(7),
736 Action::Tab(8),
737 Action::LastTab,
738 Action::OpenMenu,
739 ];
740
741 fn defaults() -> Keymap<Self> {
742 default_keymap()
743 }
744
745 fn name(self) -> &'static str {
746 match self {
747 Action::RecentNext => "RecentNext",
748 Action::RecentPrevious => "RecentPrevious",
749 Action::Next => "Next",
750 Action::Previous => "Previous",
751 Action::MoveLeft => "MoveLeft",
752 Action::MoveRight => "MoveRight",
753 Action::Close => "Close",
754 Action::New => "New",
755 Action::Tab(1) => "Tab1",
756 Action::Tab(2) => "Tab2",
757 Action::Tab(3) => "Tab3",
758 Action::Tab(4) => "Tab4",
759 Action::Tab(5) => "Tab5",
760 Action::Tab(6) => "Tab6",
761 Action::Tab(7) => "Tab7",
762 Action::Tab(8) => "Tab8",
763 Action::Tab(_) => "Tab",
764 Action::LastTab => "LastTab",
765 Action::OpenMenu => "OpenMenu",
766 }
767 }
768
769 fn description(self) -> &'static str {
770 match self {
771 Action::RecentNext => {
772 "Activates the previously used document. Pressed again while the modifier is held, goes further back."
773 }
774 Action::RecentPrevious => "Walks the recently used documents the other way.",
775 Action::Next => "Activates the tab to the right, wrapping at the end.",
776 Action::Previous => "Activates the tab to the left, wrapping at the start.",
777 Action::MoveLeft => "Moves the active tab one place left, within its group.",
778 Action::MoveRight => "Moves the active tab one place right, within its group.",
779 Action::Close => "Asks to close the active document.",
780 Action::New => "Asks for a new document.",
781 Action::Tab(1) => "Activates the first tab.",
782 Action::Tab(2) => "Activates the second tab.",
783 Action::Tab(3) => "Activates the third tab.",
784 Action::Tab(4) => "Activates the fourth tab.",
785 Action::Tab(5) => "Activates the fifth tab.",
786 Action::Tab(6) => "Activates the sixth tab.",
787 Action::Tab(7) => "Activates the seventh tab.",
788 Action::Tab(8) => "Activates the eighth tab.",
789 Action::Tab(_) => "Activates the tab at that position.",
790 Action::LastTab => "Activates the last tab.",
791 Action::OpenMenu => "Opens the active tab's context menu.",
792 }
793 }
794}
795
796pub fn default_keymap() -> Keymap<Action> {
834 let keymap = Keymap::new()
835 .bind(Chord::named(Named::Tab).ctrl(), Action::RecentNext)
836 .bind(
837 Chord::named(Named::Tab).ctrl().shift(),
838 Action::RecentPrevious,
839 )
840 .bind(Chord::named(Named::PageDown).ctrl(), Action::Next)
841 .bind(Chord::named(Named::PageUp).ctrl(), Action::Previous)
842 .bind(Chord::named(Named::PageUp).ctrl().shift(), Action::MoveLeft)
843 .bind(
844 Chord::named(Named::PageDown).ctrl().shift(),
845 Action::MoveRight,
846 )
847 .bind(Chord::character('w').command(), Action::Close)
848 .bind(Chord::character('n').command(), Action::New);
849 let keymap = ('1'..='8')
850 .zip(1..)
851 .fold(keymap, |keymap, (digit, position)| {
852 keymap.bind(Chord::character(digit).command(), Action::Tab(position))
853 });
854 keymap.bind(Chord::character('9').command(), Action::LastTab)
855}
856
857type OnEvent<'a, Id, Message> = dyn Fn(Event<Id>) -> Message + 'a;
858
859pub struct DocumentTabs<'a, Id, Message> {
861 state: &'a State<Id>,
862 new_tab: bool,
863 max_label_width: f32,
864 context_menu_keymap: Keymap<context_menu::Action>,
865 overflow_keymap: Keymap<dropdown_menu::Action>,
866 on_event: Option<Box<OnEvent<'a, Id, Message>>>,
867}
868
869pub fn document_tabs<Id, Message>(state: &State<Id>) -> DocumentTabs<'_, Id, Message> {
872 DocumentTabs {
873 state,
874 new_tab: false,
875 max_label_width: MAX_LABEL_WIDTH,
876 context_menu_keymap: context_menu::default_keymap(),
877 overflow_keymap: dropdown_menu::default_keymap(),
878 on_event: None,
879 }
880}
881
882impl<Id: std::fmt::Debug, Message> std::fmt::Debug for DocumentTabs<'_, Id, Message> {
883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884 f.debug_struct("DocumentTabs")
885 .field("state", self.state)
886 .field("new_tab", &self.new_tab)
887 .field("max_label_width", &self.max_label_width)
888 .finish_non_exhaustive()
889 }
890}
891
892impl<'a, Id, Message> DocumentTabs<'a, Id, Message> {
893 pub fn new_tab(mut self, new_tab: bool) -> Self {
896 self.new_tab = new_tab;
897 self
898 }
899
900 pub fn max_label_width(mut self, width: f32) -> Self {
903 self.max_label_width = width;
904 self
905 }
906
907 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
909 self.context_menu_keymap = keymap;
910 self
911 }
912
913 pub fn overflow_keymap(mut self, keymap: Keymap<dropdown_menu::Action>) -> Self {
915 self.overflow_keymap = keymap;
916 self
917 }
918
919 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
920 self.on_event = Some(Box::new(on_event));
921 self
922 }
923
924 pub fn is_enabled(&self) -> bool {
925 self.on_event.is_some()
926 }
927}
928
929#[derive(Debug, Clone, Copy, PartialEq, Eq)]
931struct Look {
932 active: bool,
933 hovered: bool,
934 enabled: bool,
935}
936
937impl<'a, Id, Message> From<DocumentTabs<'a, Id, Message>> for Element<'a, Message>
938where
939 Id: Copy + PartialEq + 'a,
940 Message: Clone + 'a,
941{
942 fn from(tabs: DocumentTabs<'a, Id, Message>) -> Self {
943 let DocumentTabs {
944 state,
945 new_tab,
946 max_label_width,
947 context_menu_keymap,
948 overflow_keymap,
949 on_event,
950 } = tabs;
951 let on_event: Option<Rc<OnEvent<'a, Id, Message>>> = on_event.map(Rc::from);
952 let enabled = on_event.is_some();
953
954 let tab = |document: &'a Document<Id>| -> Element<'a, Message> {
955 let active = state.is_active(document.id);
956 let on_close = on_event
957 .as_ref()
958 .map(|on_event| on_event(Event::Close(document.id)));
959 let look = Look {
960 active,
961 hovered: false,
962 enabled,
963 };
964 let base = tab_view(document, look, max_label_width, on_close.clone(), false);
965 let Some(on_event) = on_event.clone() else {
966 return base;
967 };
968 let hovered = Look {
969 hovered: true,
970 ..look
971 };
972 let top = tab_view(document, hovered, max_label_width, on_close, true);
973 context_menu::keyed(state.menu(), document.id, widget::hover(base, top))
974 .keymap(context_menu_keymap.clone())
975 .on_event(move |event| on_event(Event::Menu(event)))
976 .into()
977 };
978 let (pinned, unpinned) = state.documents.split_at(state.pinned_count());
979 let pinned_row = row(pinned.iter().map(&tab)).into();
980 let content = row(unpinned.iter().map(&tab)).into();
981
982 let trailing: Element<'a, Message> = if new_tab {
983 icon_button(crate::lucide!(Plus))
984 .label("New tab")
985 .tooltip(Some(tooltip::Position::Bottom))
986 .size(ButtonSize::Sm)
987 .on_press_maybe(on_event.as_ref().map(|on_event| on_event(Event::New)))
988 .into()
989 } else {
990 widget::space().into()
991 };
992
993 let open = state.overflow().is_open();
994 let toggle = on_event
995 .as_ref()
996 .map(|on_event| on_event(Event::Overflow(dropdown_menu::Event::Toggle)));
997 let trigger = icon_button(crate::lucide!(ChevronDown))
998 .label("All tabs")
999 .tooltip(None)
1000 .size(ButtonSize::Sm)
1001 .pressed(open)
1002 .on_press_maybe(toggle);
1003 let mut overflow = anchored(trigger)
1004 .content(open.then(|| overflow_panel(state, on_event.as_deref())))
1005 .placement(Placement::new(Side::Bottom, Align::End))
1006 .fit_height(true)
1007 .dismiss_keys([]);
1008 if let Some(on_event) = on_event.clone().filter(|_| open) {
1009 overflow = overflow
1010 .on_dismiss(on_event(Event::Overflow(dropdown_menu::Event::Close)))
1011 .on_key(move |key| {
1012 state
1013 .overflow()
1014 .key_event(&overflow_keymap, key)
1015 .map(|event| on_event(Event::Overflow(event)))
1016 });
1017 }
1018
1019 let ids: Vec<Id> = state.documents.iter().map(|document| document.id).collect();
1020 let on_gesture = on_event.map(|on_event| {
1021 Box::new(move |gesture: Gesture| {
1022 let event = match gesture {
1023 Gesture::Press(index) => Event::Activate(*ids.get(index)?),
1024 Gesture::Middle(index) => Event::Close(*ids.get(index)?),
1025 Gesture::Drop { from, to } => Event::Move(*ids.get(from)?, to),
1026 Gesture::DoubleClick if new_tab => Event::New,
1027 Gesture::DoubleClick => return None,
1028 };
1029 Some(on_event(event))
1030 }) as Box<dyn Fn(Gesture) -> Option<Message> + 'a>
1031 });
1032
1033 Element::new(Strip {
1034 pinned_row,
1035 content,
1036 trailing,
1037 overflow: overflow.into(),
1038 active: state.active_index(),
1039 count: state.documents.len(),
1040 pinned: state.pinned_count(),
1041 on_gesture,
1042 })
1043 }
1044}
1045
1046fn tab_view<'a, Id, Message>(
1047 document: &'a Document<Id>,
1048 look: Look,
1049 max_label_width: f32,
1050 on_close: Option<Message>,
1051 fill: bool,
1052) -> Element<'a, Message>
1053where
1054 Id: 'a,
1055 Message: Clone + 'a,
1056{
1057 let Look {
1058 active,
1059 hovered,
1060 enabled,
1061 } = look;
1062 let colour = move |theme: &Theme| label_colour(&Tokens::of(theme), active, hovered, enabled);
1063
1064 let mut content = row![].spacing(space::SM).align_y(Alignment::Center);
1065 if let Some(glyph) = document.icon {
1066 content = content.push(themed(glyph, ICON, opacity(enabled), colour));
1067 }
1068 if !document.pinned || document.icon.is_none() {
1069 let width = if document.pinned {
1070 PINNED_LABEL_WIDTH
1071 } else {
1072 max_label_width
1073 };
1074 let label = ellipsis(document.label.as_str())
1075 .width(Length::Shrink)
1076 .colour(colour);
1077 content = content.push(container(label).max_width(width));
1078 }
1079 if let Some(slot) = slot(document, look, on_close) {
1080 content = content.push(slot);
1081 }
1082
1083 container(content)
1084 .width(if fill { Length::Fill } else { Length::Shrink })
1085 .height(HEIGHT)
1086 .padding(Padding::from([0.0, PADDING_X]))
1087 .align_y(Vertical::Center)
1088 .style(move |theme| tab_style(&Tokens::of(theme), active, hovered))
1089 .into()
1090}
1091
1092fn overflow_panel<'a, Id, Message>(
1095 state: &'a State<Id>,
1096 on_event: Option<&OnEvent<'a, Id, Message>>,
1097) -> Element<'a, Message>
1098where
1099 Id: Copy + PartialEq + 'a,
1100 Message: Clone + 'a,
1101{
1102 let highlighted = state.overflow().highlighted();
1103 let rows = state.documents.iter().map(|document| {
1104 let status = match (on_event.is_some(), highlighted == Some(document.id)) {
1105 (false, _) => RowStatus::Disabled,
1106 (true, true) => RowStatus::Highlighted,
1107 (true, false) => RowStatus::Idle,
1108 };
1109 overflow_row(document, state.is_active(document.id), status, on_event)
1110 });
1111 let reveal = highlighted
1112 .and_then(|id| state.index_of(id))
1113 .or_else(|| state.active_index());
1114 menu::surface(scrolling(fitted(rows, OVERFLOW_WIDTH), reveal)).into()
1115}
1116
1117fn overflow_row<'a, Id, Message>(
1120 document: &'a Document<Id>,
1121 active: bool,
1122 status: RowStatus,
1123 on_event: Option<&OnEvent<'a, Id, Message>>,
1124) -> Element<'a, Message>
1125where
1126 Id: Copy + 'a,
1127 Message: Clone + 'a,
1128{
1129 let colours = move |theme: &Theme| menu::row_style(&Tokens::of(theme), status, false);
1130 let opacity = opacity(status != RowStatus::Disabled);
1131
1132 let mark: Element<'a, Message> = if active {
1133 themed(
1134 crate::lucide!(Check),
1135 menu::ICON_SIZE,
1136 opacity,
1137 move |theme| colours(theme).text,
1138 )
1139 .into()
1140 } else {
1141 widget::space().into()
1142 };
1143 let mut content = row![
1144 container(mark)
1145 .center_x(menu::ICON_SIZE)
1146 .center_y(menu::ICON_SIZE)
1147 ]
1148 .spacing(space::SM)
1149 .align_y(Alignment::Center);
1150 if let Some(glyph) = document.icon {
1151 content = content.push(themed(glyph, menu::ICON_SIZE, opacity, move |theme| {
1152 colours(theme).icon
1153 }));
1154 }
1155 content = content.push(
1156 ellipsis(document.label.as_str())
1157 .width(Length::Fill)
1158 .colour(move |theme| colours(theme).text),
1159 );
1160 if document.modified {
1161 content = content.push(
1162 widget::text("Modified")
1163 .size(text_size::XS)
1164 .wrapping(widget::text::Wrapping::None)
1165 .style(move |theme| widget::text::Style {
1166 color: Some(colours(theme).hint),
1167 }),
1168 );
1169 }
1170
1171 let body = container(content)
1172 .padding(menu::ROW_PADDING)
1173 .width(Length::Fill)
1174 .style(move |theme| menu::row_container_style(colours(theme)));
1175 let Some(on_event) = on_event.filter(|_| status != RowStatus::Disabled) else {
1176 return menu::inset(body);
1177 };
1178 let id = document.id;
1179 menu::inset(
1180 widget::mouse_area(body)
1181 .on_enter(on_event(Event::Overflow(dropdown_menu::Event::Highlight(
1182 id,
1183 ))))
1184 .on_press(on_event(Event::Overflow(dropdown_menu::Event::Activate(
1185 id,
1186 ))))
1187 .interaction(mouse::Interaction::Pointer),
1188 )
1189}
1190
1191fn slot<'a, Id, Message>(
1195 document: &Document<Id>,
1196 look: Look,
1197 on_close: Option<Message>,
1198) -> Option<Element<'a, Message>>
1199where
1200 Message: Clone + 'a,
1201{
1202 let active = look.active;
1203 let closable = in_bulk_close(document);
1204 if closable && (look.hovered || (active && !document.modified)) {
1205 return Some(close_button(on_close));
1206 }
1207 if document.modified {
1208 let dot = container(widget::space())
1209 .width(DOT)
1210 .height(DOT)
1211 .style(move |theme| dot_style(&Tokens::of(theme), active));
1212 return Some(container(dot).center(SLOT).into());
1213 }
1214 closable.then(|| widget::space().width(SLOT).height(SLOT).into())
1215}
1216
1217fn close_button<'a, Message: Clone + 'a>(on_close: Option<Message>) -> Element<'a, Message> {
1218 let enabled = on_close.is_some();
1219 widget::button(themed(
1220 crate::lucide!(X),
1221 CLOSE_ICON,
1222 opacity(enabled),
1223 |theme| Tokens::of(theme).foreground,
1224 ))
1225 .width(SLOT)
1226 .height(SLOT)
1227 .padding((SLOT - CLOSE_ICON) / 2.0)
1228 .on_press_maybe(on_close)
1229 .style(|theme, status| close_style(&Tokens::of(theme), status))
1230 .into()
1231}
1232
1233pub fn label_colour(tokens: &Tokens, active: bool, hovered: bool, enabled: bool) -> Color {
1235 let colour = if active || hovered {
1236 tokens.foreground
1237 } else {
1238 tokens.muted_foreground
1239 };
1240 if enabled { colour } else { fade(colour, 0.5) }
1241}
1242
1243pub fn tab_style(tokens: &Tokens, active: bool, hovered: bool) -> container::Style {
1246 let background = if active {
1247 Some(tokens.background)
1248 } else if hovered {
1249 Some(mix(tokens.muted, tokens.foreground, 0.06))
1250 } else {
1251 None
1252 };
1253 container::Style {
1254 background: background.map(Background::Color),
1255 text_color: Some(label_colour(tokens, active, hovered, true)),
1256 border: Border {
1257 radius: Radius {
1258 top_left: radius::MD,
1259 top_right: radius::MD,
1260 bottom_right: 0.0,
1261 bottom_left: 0.0,
1262 },
1263 ..Border::default()
1264 },
1265 ..container::Style::default()
1266 }
1267}
1268
1269pub fn close_style(tokens: &Tokens, status: Status) -> widget::button::Style {
1271 let background = match status {
1272 Status::Hovered | Status::Pressed => Some(fade(tokens.foreground, 0.1)),
1273 Status::Active | Status::Disabled => None,
1274 };
1275 widget::button::Style {
1276 background: background.map(Background::Color),
1277 text_color: tokens.foreground,
1278 border: Border {
1279 radius: radius::SM.into(),
1280 ..Border::default()
1281 },
1282 ..widget::button::Style::default()
1283 }
1284}
1285
1286pub fn dot_style(tokens: &Tokens, active: bool) -> container::Style {
1288 let colour = if active {
1289 tokens.foreground
1290 } else {
1291 tokens.muted_foreground
1292 };
1293 container::Style {
1294 background: Some(Background::Color(colour)),
1295 border: Border {
1296 radius: radius::FULL.into(),
1297 ..Border::default()
1298 },
1299 ..container::Style::default()
1300 }
1301}
1302
1303#[derive(Debug, Clone, Copy, PartialEq)]
1305pub struct StripStyle {
1306 pub background: Color,
1307 pub line: Color,
1309 pub divider: Color,
1311 pub indicator: Color,
1313}
1314
1315pub fn strip_style(tokens: &Tokens) -> StripStyle {
1316 StripStyle {
1317 background: tokens.muted,
1318 line: tokens.border,
1319 divider: tokens.border,
1320 indicator: tokens.primary,
1321 }
1322}
1323
1324fn gap_at(tabs: &[(f32, f32)], x: f32) -> usize {
1327 tabs.iter()
1328 .take_while(|(left, width)| left + width / 2.0 < x)
1329 .count()
1330}
1331
1332fn clamp_gap(from: usize, gap: usize, pinned: usize, count: usize) -> usize {
1335 if from < pinned {
1336 gap.min(pinned)
1337 } else {
1338 gap.clamp(pinned, count)
1339 }
1340}
1341
1342fn drop_index(from: usize, gap: usize) -> Option<usize> {
1345 let to = if gap > from { gap - 1 } else { gap };
1346 (to != from).then_some(to)
1347}
1348
1349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1351enum Gesture {
1352 Press(usize),
1353 Middle(usize),
1354 Drop { from: usize, to: usize },
1355 DoubleClick,
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq)]
1360struct Press {
1361 index: usize,
1362 origin: Point,
1363 dragging: bool,
1364 gap: Option<usize>,
1365}
1366
1367const MAX_PINNED_SHARE: f32 = 0.5;
1371
1372const PINNED_GAP: f32 = 6.0;
1374
1375fn pinned_stay(pinned: f32, room: f32, overflowing: bool) -> bool {
1378 !overflowing || pinned <= room * MAX_PINNED_SHARE
1379}
1380
1381#[derive(Debug, Clone, Default)]
1388struct Memory {
1389 offset: f32,
1390 content_width: f32,
1391 viewport_width: f32,
1392 fixed: f32,
1393 fixed_tabs: usize,
1394 overflowing: bool,
1395 revealed: Option<(Option<usize>, usize)>,
1397 measured: Option<(f32, f32)>,
1399 press: Option<Press>,
1400 last_click: Option<mouse::Click>,
1401 touch: Option<Touch>,
1402}
1403
1404#[derive(Debug, Clone, Copy, PartialEq)]
1407struct Touch {
1408 finger: Finger,
1409 last: f32,
1411 origin: Point,
1412 tab: Option<usize>,
1413}
1414
1415const TAP_SLOP: f32 = 10.0;
1417
1418impl Touch {
1419 fn tapped(&self, position: Point) -> Option<usize> {
1421 self.tab
1422 .filter(|_| self.origin.distance(position) <= TAP_SLOP)
1423 }
1424}
1425
1426impl Scrollable for Memory {
1427 fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1428 if let Some(x) = offset.x {
1429 self.offset = (x.clamp(0.0, 1.0) * self.max_offset()).round();
1430 }
1431 }
1432
1433 fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1434 if let Some(x) = offset.x {
1435 self.offset = x.clamp(0.0, self.max_offset());
1436 }
1437 }
1438
1439 fn scroll_by(&mut self, offset: AbsoluteOffset, _bounds: Rectangle, _content: Rectangle) {
1440 let _ = Memory::scroll_by(self, offset.x);
1441 }
1442}
1443
1444impl Memory {
1445 fn max_offset(&self) -> f32 {
1446 (self.content_width - self.viewport_width).max(0.0)
1447 }
1448
1449 fn scroll_by(&mut self, delta: f32) -> bool {
1450 let offset = (self.offset + delta).clamp(0.0, self.max_offset());
1451 let changed = offset != self.offset;
1452 self.offset = offset;
1453 changed
1454 }
1455
1456 fn reveal(&mut self, left: f32, right: f32) {
1459 if right > self.offset + self.viewport_width {
1460 self.offset = right - self.viewport_width;
1461 }
1462 if left < self.offset {
1463 self.offset = left;
1464 }
1465 self.offset = self.offset.clamp(0.0, self.max_offset());
1466 }
1467
1468 fn follow(
1475 &mut self,
1476 key: (Option<usize>, usize),
1477 widths: (f32, f32),
1478 active: Option<(f32, f32)>,
1479 ) {
1480 (self.content_width, self.viewport_width) = widths;
1481 let moved = self.revealed != Some(key);
1482 let resized = self.measured != Some(widths);
1483 self.revealed = Some(key);
1484 self.measured = Some(widths);
1485 if !moved && resized {
1486 self.offset = 0.0;
1487 }
1488 if let Some((left, width)) = active.filter(|_| moved || resized) {
1489 self.reveal(left, left + width);
1490 }
1491 self.offset = self.offset.clamp(0.0, self.max_offset());
1492 }
1493
1494 fn viewport(&self, bounds: Rectangle) -> Rectangle {
1496 Rectangle {
1497 x: bounds.x + self.fixed,
1498 width: self.viewport_width,
1499 ..bounds
1500 }
1501 }
1502
1503 fn shift(&self) -> Vector {
1504 Vector::new(self.offset, 0.0)
1505 }
1506
1507 fn scrolls(&self, index: usize) -> bool {
1508 index >= self.fixed_tabs
1509 }
1510
1511 fn scroll_span(&self, spans: &[(f32, f32)], index: usize) -> Option<(f32, f32)> {
1515 let (left, width) = spans.get(index).filter(|_| self.scrolls(index))?;
1516 Some((left - self.fixed, *width))
1517 }
1518
1519 fn pinned_frame(&self, bounds: Rectangle) -> (Rectangle, Vector) {
1522 if self.fixed_tabs == 0 {
1523 return (self.viewport(bounds), self.shift());
1524 }
1525 let clip = Rectangle {
1526 width: self.fixed,
1527 ..bounds
1528 };
1529 (clip, Vector::ZERO)
1530 }
1531
1532 fn locate(&self, x: f32) -> Option<f32> {
1535 if x < 0.0 {
1536 return None;
1537 }
1538 if x < self.fixed {
1539 return Some(x);
1540 }
1541 (x < self.fixed + self.viewport_width).then_some(x + self.offset)
1542 }
1543
1544 fn unscroll(&self, x: f32) -> f32 {
1546 if x < self.fixed { x } else { x + self.offset }
1547 }
1548
1549 fn visible_span(&self, spans: &[(f32, f32)], index: usize) -> Option<(f32, f32)> {
1552 let (left, width) = *spans.get(index)?;
1553 let (start, end, shift) = if self.scrolls(index) {
1554 (self.fixed, self.fixed + self.viewport_width, self.offset)
1555 } else {
1556 (0.0, self.fixed, 0.0)
1557 };
1558 let shown_left = (left - shift).max(start);
1559 let shown_right = (left - shift + width).min(end);
1560 (shown_right > shown_left).then_some((shown_left, shown_right))
1561 }
1562
1563 fn scroll_start(&self) -> f32 {
1567 if self.fixed_tabs == 0 || self.offset <= 0.0 {
1568 return self.fixed;
1569 }
1570 self.fixed + PINNED_GAP.min(self.viewport_width)
1571 }
1572
1573 fn scroll_clip(&self, bounds: Rectangle) -> Rectangle {
1575 let clip = self.viewport(bounds);
1576 let inset = self.scroll_start() - self.fixed;
1577 Rectangle {
1578 x: clip.x + inset,
1579 width: (clip.width - inset).max(0.0),
1580 ..clip
1581 }
1582 }
1583
1584 fn dividers(&self, spans: &[(f32, f32)], active: Option<usize>) -> Vec<f32> {
1588 let end = self.fixed + self.viewport_width;
1589 (1..spans.len())
1590 .filter_map(|index| {
1591 let boundary = index == self.fixed_tabs;
1592 let next_to_active = active == Some(index) && !(boundary && self.offset > 0.0);
1593 if active == Some(index - 1) || next_to_active {
1594 return None;
1595 }
1596 if boundary {
1597 return Some(self.fixed);
1598 }
1599 let left = spans[index].0;
1600 if !self.scrolls(index) {
1601 return Some(left);
1602 }
1603 let x = left - self.offset;
1604 (x > self.scroll_start() && x < end).then_some(x)
1605 })
1606 .collect()
1607 }
1608
1609 fn indicator_x(&self, spans: &[(f32, f32)], from: usize, gap: usize) -> Option<f32> {
1613 let scrolls = self.scrolls(from);
1614 let end = if scrolls {
1615 spans.len()
1616 } else {
1617 self.fixed_tabs
1618 };
1619 let edge = match spans.get(gap).filter(|_| gap < end) {
1620 Some((left, _)) => *left,
1621 None => {
1622 let (left, width) = spans.get(end.checked_sub(1)?)?;
1623 left + width
1624 }
1625 };
1626 let x = edge - INDICATOR_WIDTH / 2.0;
1627 if !scrolls {
1628 return Some(x.clamp(0.0, (self.fixed - INDICATOR_WIDTH).max(0.0)));
1629 }
1630 let start = self.fixed;
1631 let stop = (start + self.viewport_width - INDICATOR_WIDTH).max(start);
1632 Some((x - self.offset).clamp(start, stop))
1633 }
1634}
1635
1636fn inner_cursor(cursor: mouse::Cursor, clip: Rectangle, shift: Vector) -> mouse::Cursor {
1639 match cursor.position_over(clip) {
1640 Some(position) => mouse::Cursor::Available(position + shift),
1641 None => cursor.levitate() + shift,
1642 }
1643}
1644
1645struct Strip<'a, Message> {
1649 pinned_row: Element<'a, Message>,
1650 content: Element<'a, Message>,
1651 trailing: Element<'a, Message>,
1652 overflow: Element<'a, Message>,
1653 active: Option<usize>,
1654 count: usize,
1655 pinned: usize,
1656 on_gesture: Option<Box<dyn Fn(Gesture) -> Option<Message> + 'a>>,
1657}
1658
1659impl<Message> Strip<'_, Message> {
1660 fn publish(&self, shell: &mut Shell<'_, Message>, gesture: Gesture) {
1661 if let Some(message) = self.on_gesture.as_ref().and_then(|on| on(gesture)) {
1662 shell.publish(message);
1663 }
1664 }
1665}
1666
1667fn spans(origin: f32, pinned: Layout<'_>, content: Layout<'_>) -> Vec<(f32, f32)> {
1669 pinned
1670 .children()
1671 .chain(content.children())
1672 .map(|tab| {
1673 let bounds = tab.bounds();
1674 (bounds.x - origin, bounds.width)
1675 })
1676 .collect()
1677}
1678
1679struct Parts<'a> {
1681 pinned: Layout<'a>,
1682 content: Layout<'a>,
1683 trailing: Layout<'a>,
1684 overflow: Layout<'a>,
1685}
1686
1687fn parts(layout: Layout<'_>) -> Option<Parts<'_>> {
1688 let mut children = layout.children();
1689 Some(Parts {
1690 pinned: children.next()?,
1691 content: children.next()?,
1692 trailing: children.next()?,
1693 overflow: children.next()?,
1694 })
1695}
1696
1697impl<Message> Widget<Message, Theme, Renderer> for Strip<'_, Message> {
1698 fn size(&self) -> Size<Length> {
1699 Size::new(Length::Fill, Length::Shrink)
1700 }
1701
1702 fn tag(&self) -> tree::Tag {
1703 tree::Tag::of::<Memory>()
1704 }
1705
1706 fn state(&self) -> tree::State {
1707 tree::State::new(Memory::default())
1708 }
1709
1710 fn children(&self) -> Vec<Tree> {
1711 vec![
1712 Tree::new(&self.pinned_row),
1713 Tree::new(&self.content),
1714 Tree::new(&self.trailing),
1715 Tree::new(&self.overflow),
1716 ]
1717 }
1718
1719 fn diff(&self, tree: &mut Tree) {
1720 tree.diff_children(&[
1721 &self.pinned_row,
1722 &self.content,
1723 &self.trailing,
1724 &self.overflow,
1725 ]);
1726 }
1727
1728 fn layout(
1729 &mut self,
1730 tree: &mut Tree,
1731 renderer: &Renderer,
1732 limits: &layout::Limits,
1733 ) -> layout::Node {
1734 let max = limits.max();
1735 let [pinned_tree, content_tree, trailing_tree, overflow_tree] =
1736 tree.children.as_mut_slice()
1737 else {
1738 return layout::Node::new(Size::ZERO);
1739 };
1740 let unbounded = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, max.height));
1741 let pinned = self
1742 .pinned_row
1743 .as_widget_mut()
1744 .layout(pinned_tree, renderer, &unbounded);
1745 let content = self
1746 .content
1747 .as_widget_mut()
1748 .layout(content_tree, renderer, &unbounded);
1749 let loose = layout::Limits::new(Size::ZERO, max);
1750 let trailing = self
1751 .trailing
1752 .as_widget_mut()
1753 .layout(trailing_tree, renderer, &loose);
1754 let overflow = self
1755 .overflow
1756 .as_widget_mut()
1757 .layout(overflow_tree, renderer, &loose);
1758
1759 let pinned_size = pinned.size();
1760 let content_size = content.size();
1761 let trailing_size = trailing.size();
1762 let overflow_size = overflow.size();
1763 let tabs_width = pinned_size.width + content_size.width;
1764 let natural = tabs_width + trailing_size.width;
1765 let width = if max.width.is_finite() {
1766 max.width
1767 } else {
1768 natural
1769 };
1770 let height = pinned_size
1771 .height
1772 .max(content_size.height)
1773 .max(trailing_size.height)
1774 .max(overflow_size.height)
1775 .max(limits.min().height);
1776 let overflowing = natural > width + 0.5;
1777 let room = if overflowing {
1778 (width - trailing_size.width - overflow_size.width).max(0.0)
1779 } else {
1780 tabs_width
1781 };
1782 let (fixed, fixed_tabs) = if pinned_stay(pinned_size.width, room, overflowing) {
1783 (pinned_size.width, self.pinned)
1784 } else {
1785 (0.0, 0)
1786 };
1787 let viewport_width = (room - fixed).max(0.0);
1788 let centre = |size: Size| (height - size.height) / 2.0;
1789
1790 let spans: Vec<(f32, f32)> = pinned
1791 .children()
1792 .iter()
1793 .map(|tab| (tab.bounds().x, tab.bounds().width))
1794 .chain(
1795 content
1796 .children()
1797 .iter()
1798 .map(|tab| (pinned_size.width + tab.bounds().x, tab.bounds().width)),
1799 )
1800 .collect();
1801 let memory = tree.state.downcast_mut::<Memory>();
1802 memory.overflowing = overflowing;
1803 memory.fixed = fixed;
1804 memory.fixed_tabs = fixed_tabs;
1805 let active = self
1806 .active
1807 .and_then(|index| memory.scroll_span(&spans, index));
1808 memory.follow(
1809 (self.active, self.count),
1810 (tabs_width - fixed, viewport_width),
1811 active,
1812 );
1813
1814 let overflow_x = width - overflow_size.width;
1815 layout::Node::with_children(
1816 Size::new(width, height),
1817 vec![
1818 pinned.move_to(Point::new(0.0, centre(pinned_size))),
1819 content.move_to(Point::new(pinned_size.width, centre(content_size))),
1820 trailing.move_to(Point::new(fixed + viewport_width, centre(trailing_size))),
1821 overflow.move_to(Point::new(overflow_x, centre(overflow_size))),
1822 ],
1823 )
1824 }
1825
1826 fn draw(
1827 &self,
1828 tree: &Tree,
1829 renderer: &mut Renderer,
1830 theme: &Theme,
1831 style: &renderer::Style,
1832 layout: Layout<'_>,
1833 cursor: mouse::Cursor,
1834 viewport: &Rectangle,
1835 ) {
1836 let memory = tree.state.downcast_ref::<Memory>();
1837 let colours = strip_style(&Tokens::of(theme));
1838 let bounds = layout.bounds();
1839 let Some(Parts {
1840 pinned,
1841 content,
1842 trailing,
1843 overflow,
1844 }) = parts(layout)
1845 else {
1846 return;
1847 };
1848
1849 fill(renderer, bounds, colours.background);
1850
1851 let clip = memory.scroll_clip(bounds);
1852 let rows = [
1853 (
1854 &self.pinned_row,
1855 &tree.children[0],
1856 pinned,
1857 memory.pinned_frame(bounds),
1858 ),
1859 (
1860 &self.content,
1861 &tree.children[1],
1862 content,
1863 (clip, memory.shift()),
1864 ),
1865 ];
1866 for (row, row_tree, row_layout, (row_clip, shift)) in rows {
1867 renderer.with_layer(row_clip, |renderer| {
1868 renderer.with_translation(Vector::new(-shift.x, 0.0), |renderer| {
1869 row.as_widget().draw(
1870 row_tree,
1871 renderer,
1872 theme,
1873 style,
1874 row_layout,
1875 inner_cursor(cursor, row_clip, shift),
1876 &(row_clip + shift),
1877 );
1878 });
1879 });
1880 }
1881
1882 let spans = spans(bounds.x, pinned, content);
1883 let top = bounds.y + (bounds.height - DIVIDER_HEIGHT) / 2.0;
1884 let divider = |renderer: &mut Renderer, x: f32| {
1885 fill(
1886 renderer,
1887 Rectangle::new(
1888 Point::new((bounds.x + x).round(), top),
1889 Size::new(1.0, DIVIDER_HEIGHT),
1890 ),
1891 colours.divider,
1892 );
1893 };
1894 for x in memory.dividers(&spans, self.active) {
1895 divider(renderer, x);
1896 }
1897 if memory.overflowing {
1899 divider(renderer, memory.fixed + memory.viewport_width);
1900 }
1901 self.draw_indicator(renderer, memory, &spans, colours, bounds);
1902
1903 let line = Rectangle {
1905 y: bounds.y + bounds.height - 1.0,
1906 height: 1.0,
1907 ..bounds
1908 };
1909 let gap = self
1910 .active
1911 .and_then(|index| memory.visible_span(&spans, index));
1912 match gap {
1913 Some((left, right)) => {
1914 fill(
1915 renderer,
1916 Rectangle {
1917 width: left,
1918 ..line
1919 },
1920 colours.line,
1921 );
1922 fill(
1923 renderer,
1924 Rectangle {
1925 x: line.x + right,
1926 width: line.width - right,
1927 ..line
1928 },
1929 colours.line,
1930 );
1931 }
1932 None => fill(renderer, line, colours.line),
1933 }
1934
1935 self.trailing.as_widget().draw(
1936 &tree.children[2],
1937 renderer,
1938 theme,
1939 style,
1940 trailing,
1941 cursor,
1942 viewport,
1943 );
1944 if memory.overflowing {
1945 self.overflow.as_widget().draw(
1946 &tree.children[3],
1947 renderer,
1948 theme,
1949 style,
1950 overflow,
1951 cursor,
1952 viewport,
1953 );
1954 }
1955 }
1956
1957 fn operate(
1958 &mut self,
1959 tree: &mut Tree,
1960 layout: Layout<'_>,
1961 renderer: &Renderer,
1962 operation: &mut dyn Operation,
1963 ) {
1964 let bounds = layout.bounds();
1965 let Some(Parts {
1966 pinned,
1967 content,
1968 trailing,
1969 overflow,
1970 }) = parts(layout)
1971 else {
1972 return;
1973 };
1974 let [pinned_tree, content_tree, trailing_tree, overflow_tree] =
1975 tree.children.as_mut_slice()
1976 else {
1977 return;
1978 };
1979 let memory = tree.state.downcast_mut::<Memory>();
1980 let clip = memory.viewport(bounds);
1981 let shift = memory.shift();
1982 let pinned_scroll = memory.fixed_tabs == 0;
1983 let scrolled = Rectangle {
1984 x: clip.x,
1985 width: memory.content_width,
1986 ..content.bounds()
1987 };
1988 if !pinned_scroll {
1989 operation.traverse(&mut |operation| {
1990 self.pinned_row
1991 .as_widget_mut()
1992 .operate(pinned_tree, pinned, renderer, operation);
1993 });
1994 }
1995 operation.traverse(&mut |operation| {
1996 operation.scrollable(None, clip, scrolled, shift, memory);
1997 operation.traverse(&mut |operation| {
1998 if pinned_scroll {
1999 self.pinned_row.as_widget_mut().operate(
2000 pinned_tree,
2001 pinned,
2002 renderer,
2003 operation,
2004 );
2005 }
2006 self.content
2007 .as_widget_mut()
2008 .operate(content_tree, content, renderer, operation);
2009 });
2010 });
2011 self.trailing
2012 .as_widget_mut()
2013 .operate(trailing_tree, trailing, renderer, operation);
2014 self.overflow
2015 .as_widget_mut()
2016 .operate(overflow_tree, overflow, renderer, operation);
2017 }
2018
2019 fn update(
2020 &mut self,
2021 tree: &mut Tree,
2022 event: &Input,
2023 layout: Layout<'_>,
2024 cursor: mouse::Cursor,
2025 renderer: &Renderer,
2026 clipboard: &mut dyn Clipboard,
2027 shell: &mut Shell<'_, Message>,
2028 viewport: &Rectangle,
2029 ) {
2030 let bounds = layout.bounds();
2031 let Some(Parts {
2032 pinned,
2033 content,
2034 trailing,
2035 overflow,
2036 }) = parts(layout)
2037 else {
2038 return;
2039 };
2040 let spans = spans(bounds.x, pinned, content);
2041
2042 if self.drag(tree, event, cursor, bounds.x, &spans, shell) {
2043 return;
2044 }
2045
2046 let memory = tree.state.downcast_ref::<Memory>();
2047 let clip = memory.viewport(bounds);
2048 let frames = [memory.pinned_frame(bounds), (clip, memory.shift())];
2049 let overflowing = memory.overflowing;
2050 let [pinned_tree, content_tree, trailing_tree, overflow_tree] =
2051 tree.children.as_mut_slice()
2052 else {
2053 return;
2054 };
2055 let rows = [
2056 (&mut self.pinned_row, pinned_tree, pinned),
2057 (&mut self.content, content_tree, content),
2058 ];
2059 for ((row, row_tree, row_layout), (row_clip, shift)) in rows.into_iter().zip(frames) {
2060 row.as_widget_mut().update(
2061 row_tree,
2062 event,
2063 row_layout,
2064 inner_cursor(cursor, row_clip, shift),
2065 renderer,
2066 clipboard,
2067 shell,
2068 &(row_clip + shift),
2069 );
2070 }
2071 self.trailing.as_widget_mut().update(
2072 trailing_tree,
2073 event,
2074 trailing,
2075 cursor,
2076 renderer,
2077 clipboard,
2078 shell,
2079 viewport,
2080 );
2081 if overflowing {
2082 self.overflow.as_widget_mut().update(
2083 overflow_tree,
2084 event,
2085 overflow,
2086 cursor,
2087 renderer,
2088 clipboard,
2089 shell,
2090 viewport,
2091 );
2092 }
2093 if shell.is_event_captured() {
2094 return;
2095 }
2096
2097 let memory = tree.state.downcast_mut::<Memory>();
2098 self.pointer(memory, event, cursor, bounds, &spans, shell);
2099 }
2100
2101 fn mouse_interaction(
2102 &self,
2103 tree: &Tree,
2104 layout: Layout<'_>,
2105 cursor: mouse::Cursor,
2106 viewport: &Rectangle,
2107 renderer: &Renderer,
2108 ) -> mouse::Interaction {
2109 let memory = tree.state.downcast_ref::<Memory>();
2110 if memory.press.is_some_and(|press| press.dragging) {
2111 return mouse::Interaction::Grabbing;
2112 }
2113 let bounds = layout.bounds();
2114 let Some(Parts {
2115 pinned,
2116 content,
2117 trailing,
2118 overflow,
2119 }) = parts(layout)
2120 else {
2121 return mouse::Interaction::None;
2122 };
2123 let clip = memory.viewport(bounds);
2124 let rows = [
2125 (
2126 &self.pinned_row,
2127 &tree.children[0],
2128 pinned,
2129 memory.pinned_frame(bounds),
2130 ),
2131 (
2132 &self.content,
2133 &tree.children[1],
2134 content,
2135 (clip, memory.shift()),
2136 ),
2137 ];
2138 let interaction = rows
2139 .into_iter()
2140 .map(|(row, row_tree, row_layout, (row_clip, shift))| {
2141 row.as_widget().mouse_interaction(
2142 row_tree,
2143 row_layout,
2144 inner_cursor(cursor, row_clip, shift),
2145 &(row_clip + shift),
2146 renderer,
2147 )
2148 })
2149 .fold(mouse::Interaction::None, mouse::Interaction::max)
2150 .max(self.trailing.as_widget().mouse_interaction(
2151 &tree.children[2],
2152 trailing,
2153 cursor,
2154 viewport,
2155 renderer,
2156 ));
2157 if !memory.overflowing {
2158 return interaction;
2159 }
2160 interaction.max(self.overflow.as_widget().mouse_interaction(
2161 &tree.children[3],
2162 overflow,
2163 cursor,
2164 viewport,
2165 renderer,
2166 ))
2167 }
2168
2169 fn overlay<'b>(
2170 &'b mut self,
2171 tree: &'b mut Tree,
2172 layout: Layout<'b>,
2173 renderer: &Renderer,
2174 viewport: &Rectangle,
2175 translation: Vector,
2176 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
2177 let memory = tree.state.downcast_ref::<Memory>();
2178 let bounds = layout.bounds();
2179 let frames = [
2180 memory.pinned_frame(bounds),
2181 (memory.viewport(bounds), memory.shift()),
2182 ];
2183 let overflowing = memory.overflowing;
2184 let Parts {
2185 pinned,
2186 content,
2187 trailing,
2188 overflow,
2189 } = parts(layout)?;
2190 let [pinned_tree, content_tree, trailing_tree, overflow_tree] =
2191 tree.children.as_mut_slice()
2192 else {
2193 return None;
2194 };
2195
2196 let mut overlays = Vec::new();
2197 let rows = [
2198 (&mut self.pinned_row, pinned_tree, pinned),
2199 (&mut self.content, content_tree, content),
2200 ];
2201 for ((row, row_tree, row_layout), (row_clip, shift)) in rows.into_iter().zip(frames) {
2202 overlays.extend(row.as_widget_mut().overlay(
2203 row_tree,
2204 row_layout,
2205 renderer,
2206 &(row_clip + shift),
2207 translation - shift,
2208 ));
2209 }
2210 overlays.extend(self.trailing.as_widget_mut().overlay(
2211 trailing_tree,
2212 trailing,
2213 renderer,
2214 viewport,
2215 translation,
2216 ));
2217 if overflowing {
2218 overlays.extend(self.overflow.as_widget_mut().overlay(
2219 overflow_tree,
2220 overflow,
2221 renderer,
2222 viewport,
2223 translation,
2224 ));
2225 }
2226 match overlays.len() {
2227 0 => None,
2228 1 => overlays.pop(),
2229 _ => Some(overlay::Group::with_children(overlays).overlay()),
2230 }
2231 }
2232}
2233
2234fn fill(renderer: &mut Renderer, bounds: Rectangle, colour: Color) {
2235 if bounds.width <= 0.0 || bounds.height <= 0.0 {
2236 return;
2237 }
2238 renderer.fill_quad(
2239 renderer::Quad {
2240 bounds,
2241 snap: true,
2242 ..renderer::Quad::default()
2243 },
2244 Background::Color(colour),
2245 );
2246}
2247
2248impl<Message> Strip<'_, Message> {
2249 fn draw_indicator(
2251 &self,
2252 renderer: &mut Renderer,
2253 memory: &Memory,
2254 spans: &[(f32, f32)],
2255 colours: StripStyle,
2256 bounds: Rectangle,
2257 ) {
2258 let Some(x) = memory
2259 .press
2260 .filter(|press| press.dragging)
2261 .and_then(|press| {
2262 let gap = press.gap?;
2263 drop_index(press.index, gap)?;
2264 memory.indicator_x(spans, press.index, gap)
2265 })
2266 else {
2267 return;
2268 };
2269 fill(
2270 renderer,
2271 Rectangle::new(
2272 Point::new(bounds.x + x, bounds.y),
2273 Size::new(INDICATOR_WIDTH, bounds.height),
2274 ),
2275 colours.indicator,
2276 );
2277 }
2278
2279 fn drag(
2281 &self,
2282 tree: &mut Tree,
2283 event: &Input,
2284 cursor: mouse::Cursor,
2285 strip_x: f32,
2286 spans: &[(f32, f32)],
2287 shell: &mut Shell<'_, Message>,
2288 ) -> bool {
2289 let memory = tree.state.downcast_mut::<Memory>();
2290 let Some(mut press) = memory.press else {
2291 return false;
2292 };
2293 match event {
2294 Input::Mouse(mouse::Event::CursorMoved { .. }) => {
2295 let Some(position) = cursor.position() else {
2296 return false;
2297 };
2298 if !press.dragging {
2299 let far = press.origin.distance(position) > DRAG_THRESHOLD;
2300 if !far || self.on_gesture.is_none() {
2301 return false;
2302 }
2303 press.dragging = true;
2304 }
2305 let gap = gap_at(spans, memory.unscroll(position.x - strip_x));
2306 press.gap = Some(clamp_gap(press.index, gap, self.pinned, self.count));
2307 memory.press = Some(press);
2308 shell.request_redraw();
2309 shell.capture_event();
2310 true
2311 }
2312 Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
2313 memory.press = None;
2314 if !press.dragging {
2315 return false;
2316 }
2317 shell.request_redraw();
2318 shell.capture_event();
2319 let Some(to) = press.gap.and_then(|gap| drop_index(press.index, gap)) else {
2320 return true;
2321 };
2322 self.publish(
2323 shell,
2324 Gesture::Drop {
2325 from: press.index,
2326 to,
2327 },
2328 );
2329 true
2330 }
2331 _ => false,
2332 }
2333 }
2334
2335 fn pointer(
2338 &self,
2339 memory: &mut Memory,
2340 event: &Input,
2341 cursor: mouse::Cursor,
2342 bounds: Rectangle,
2343 spans: &[(f32, f32)],
2344 shell: &mut Shell<'_, Message>,
2345 ) {
2346 let tab_at = |memory: &Memory, cursor: mouse::Cursor| {
2347 let position = cursor.position_over(bounds)?;
2348 let x = memory.locate(position.x - bounds.x)?;
2349 spans
2350 .iter()
2351 .position(|(left, width)| x >= *left && x < left + width)
2352 };
2353 match event {
2354 Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
2355 let Some(position) = cursor.position_over(bounds) else {
2356 return;
2357 };
2358 if let Some(index) = tab_at(memory, cursor) {
2359 memory.press = Some(Press {
2360 index,
2361 origin: position,
2362 dragging: false,
2363 gap: None,
2364 });
2365 self.publish(shell, Gesture::Press(index));
2366 shell.capture_event();
2367 return;
2368 }
2369 let click = mouse::Click::new(position, mouse::Button::Left, memory.last_click);
2370 memory.last_click = Some(click);
2371 if click.kind() == click::Kind::Double {
2372 self.publish(shell, Gesture::DoubleClick);
2373 shell.capture_event();
2374 }
2375 }
2376 Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
2377 if let Some(index) = tab_at(memory, cursor) {
2378 self.publish(shell, Gesture::Middle(index));
2379 shell.capture_event();
2380 }
2381 }
2382 Input::Mouse(mouse::Event::WheelScrolled { delta }) => {
2383 if !cursor.is_over(bounds) {
2384 return;
2385 }
2386 let step = match *delta {
2387 mouse::ScrollDelta::Lines { x, y } => {
2388 (if x != 0.0 { x } else { y }) * WHEEL_LINE
2389 }
2390 mouse::ScrollDelta::Pixels { x, y } => {
2391 if x.abs() > y.abs() {
2392 x
2393 } else {
2394 y
2395 }
2396 }
2397 };
2398 if memory.scroll_by(-step) {
2399 shell.request_redraw();
2400 shell.capture_event();
2401 }
2402 }
2403 Input::Touch(touch::Event::FingerPressed { id, position }) => {
2404 if !bounds.contains(*position) {
2405 return;
2406 }
2407 let tab = tab_at(memory, cursor);
2408 memory.touch = Some(Touch {
2409 finger: *id,
2410 last: position.x,
2411 origin: *position,
2412 tab,
2413 });
2414 if tab.is_some() {
2415 shell.capture_event();
2416 }
2417 }
2418 Input::Touch(touch::Event::FingerMoved { id, position }) => {
2419 let Some(touch) = memory.touch.filter(|touch| touch.finger == *id) else {
2420 return;
2421 };
2422 memory.touch = Some(Touch {
2423 last: position.x,
2424 ..touch
2425 });
2426 if memory.scroll_by(touch.last - position.x) {
2427 shell.request_redraw();
2428 shell.capture_event();
2429 }
2430 }
2431 Input::Touch(touch::Event::FingerLifted { id, position }) => {
2432 let Some(touch) = memory.touch.filter(|touch| touch.finger == *id) else {
2433 return;
2434 };
2435 memory.touch = None;
2436 if let Some(index) = touch.tapped(*position) {
2437 self.publish(shell, Gesture::Press(index));
2438 shell.capture_event();
2439 }
2440 }
2441 Input::Touch(touch::Event::FingerLost { id, .. })
2442 if memory.touch.is_some_and(|touch| touch.finger == *id) =>
2443 {
2444 memory.touch = None;
2445 }
2446 _ => {}
2447 }
2448 }
2449}
2450
2451#[cfg(test)]
2452mod tests {
2453 use super::*;
2454 use crate::theme::{dark, light};
2455 use iced::keyboard::{Key, Modifiers};
2456
2457 fn state() -> State<u8> {
2458 State::new([
2459 document(1, "main.rs"),
2460 document(2, "lib.rs").modified(true),
2461 document(3, "README.md").pinned(true),
2462 document(4, "Cargo.toml"),
2463 ])
2464 }
2465
2466 fn ids(state: &State<u8>) -> Vec<u8> {
2467 state
2468 .documents()
2469 .iter()
2470 .map(|document| document.id)
2471 .collect()
2472 }
2473
2474 #[test]
2475 fn new_puts_pinned_documents_first_and_activates_the_first() {
2476 let state = state();
2477 assert_eq!(ids(&state), [3, 1, 2, 4]);
2478 assert_eq!(state.active(), Some(3));
2479 assert_eq!(state.recent(), [3, 1, 2, 4]);
2480 assert_eq!(state.pinned_count(), 1);
2481 assert!(state.is_active(3));
2482 assert!(!state.menu().is_open());
2483 assert!(!state.overflow().is_open());
2484 }
2485
2486 #[test]
2487 fn empty_state_has_nothing_to_do() {
2488 let mut state: State<u8> = State::new([]);
2489 assert_eq!(state.active(), None);
2490 assert_eq!(state.update(Event::Next), None);
2491 assert_eq!(state.update(Event::RecentNext), None);
2492 assert_eq!(state.update(Event::CloseAll), None);
2493 assert_eq!(Action::Close.event(&state), None);
2494 assert_eq!(Action::LastTab.event(&state), None);
2495 }
2496
2497 #[test]
2498 fn activate_reports_changes_only() {
2499 let mut state = state();
2500 assert_eq!(state.update(Event::Activate(2)), Some(Output::Activated(2)));
2501 assert_eq!(state.update(Event::Activate(2)), None);
2502 assert_eq!(state.update(Event::Activate(9)), None);
2503 assert_eq!(state.recent()[0], 2);
2504 assert_eq!(state.clone().with_active(4).active(), Some(4));
2505 }
2506
2507 #[test]
2508 fn next_and_previous_wrap_in_strip_order() {
2509 let mut state = state().with_active(4);
2510 assert_eq!(state.update(Event::Next), Some(Output::Activated(3)));
2511 assert_eq!(state.update(Event::Previous), Some(Output::Activated(4)));
2512 assert_eq!(state.update(Event::Previous), Some(Output::Activated(2)));
2513 }
2514
2515 #[test]
2516 fn recent_walks_until_the_cycle_ends() {
2517 let mut state = state();
2518 for id in [1, 2, 4] {
2519 let _ = state.update(Event::Activate(id));
2520 }
2521 assert_eq!(state.recent(), [4, 2, 1, 3]);
2522 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(2)));
2523 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(1)));
2524 assert_eq!(state.recent(), [4, 2, 1, 3], "the order holds mid-walk");
2525 assert_eq!(state.update(Event::EndCycle), None);
2526 assert_eq!(state.recent(), [1, 4, 2, 3]);
2527 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(4)));
2528 assert_eq!(state.update(Event::from(CycleEnd)), None);
2529 assert_eq!(state.recent(), [4, 1, 2, 3]);
2530 }
2531
2532 #[test]
2533 fn recent_previous_starts_from_the_oldest_and_other_events_end_the_walk() {
2534 let mut state = state();
2535 assert_eq!(
2536 state.update(Event::RecentPrevious),
2537 Some(Output::Activated(4))
2538 );
2539 assert_eq!(state.update(Event::Activate(2)), Some(Output::Activated(2)));
2540 assert_eq!(state.recent(), [2, 4, 3, 1]);
2541 let mut single = State::new([document(1, "a")]);
2542 assert_eq!(single.update(Event::RecentNext), None);
2543 }
2544
2545 #[test]
2546 fn close_events_request_without_closing() {
2547 let mut state = state();
2548 assert_eq!(
2549 state.update(Event::Close(3)),
2550 Some(Output::CloseRequested(vec![3]))
2551 );
2552 assert_eq!(state.update(Event::Close(9)), None);
2553 assert_eq!(
2554 state.update(Event::CloseOthers(1)),
2555 Some(Output::CloseRequested(vec![2, 4]))
2556 );
2557 assert_eq!(
2558 state.update(Event::CloseToTheRight(1)),
2559 Some(Output::CloseRequested(vec![2, 4]))
2560 );
2561 assert_eq!(state.update(Event::CloseToTheRight(4)), None);
2562 assert_eq!(
2563 state.update(Event::CloseSaved),
2564 Some(Output::CloseRequested(vec![1, 4]))
2565 );
2566 assert_eq!(
2567 state.update(Event::CloseAll),
2568 Some(Output::CloseRequested(vec![1, 2, 4]))
2569 );
2570 assert_eq!(state.documents().len(), 4);
2571 }
2572
2573 #[test]
2574 fn closing_the_active_document_activates_the_most_recent() {
2575 let mut state = state();
2576 let _ = state.update(Event::Activate(4));
2577 let _ = state.update(Event::Activate(1));
2578 assert_eq!(state.close(&[1]), Some(4));
2579 assert_eq!(ids(&state), [3, 2, 4]);
2580 assert_eq!(state.close(&[2]), None, "the active one stays");
2581 assert_eq!(state.active(), Some(4));
2582 assert_eq!(state.close(&[3, 4]), None);
2583 assert_eq!(state.active(), None);
2584 assert!(state.recent().is_empty());
2585 }
2586
2587 #[test]
2588 fn open_adds_to_the_end_of_its_group_or_activates() {
2589 let mut state = state();
2590 state.open(document(5, "new.rs"));
2591 assert_eq!(ids(&state), [3, 1, 2, 4, 5]);
2592 assert_eq!(state.active(), Some(5));
2593 state.open(document(6, "notes.md").pinned(true));
2594 assert_eq!(ids(&state), [3, 6, 1, 2, 4, 5]);
2595 state.open(document(1, "ignored"));
2596 assert_eq!(state.active(), Some(1));
2597 assert_eq!(state.document(1).map(|d| d.label.as_str()), Some("main.rs"));
2598 assert_eq!(state.recent()[..3], [1, 6, 5]);
2599 }
2600
2601 #[test]
2602 fn modified_and_label_change_in_place() {
2603 let mut state = state();
2604 state.set_modified(1, true);
2605 state.set_label(1, "main2.rs");
2606 let main = state.document(1).cloned();
2607 assert_eq!(
2608 main,
2609 Some(document(1, "main2.rs").modified(true)),
2610 "{main:?}"
2611 );
2612 state.set_modified(9, true);
2613 }
2614
2615 #[test]
2616 fn moves_stay_within_the_group() {
2617 let mut state = state();
2618 assert_eq!(
2619 state.update(Event::Move(1, 3)),
2620 Some(Output::Moved {
2621 id: 1,
2622 from: 1,
2623 to: 3
2624 })
2625 );
2626 assert_eq!(ids(&state), [3, 2, 4, 1]);
2627 assert_eq!(
2628 state.update(Event::Move(1, 0)),
2629 Some(Output::Moved {
2630 id: 1,
2631 from: 3,
2632 to: 1
2633 })
2634 );
2635 assert_eq!(state.update(Event::Move(1, 1)), None);
2636 assert_eq!(state.update(Event::Move(3, 5)), None, "pinned stays first");
2637 assert_eq!(state.update(Event::Move(4, 99)), None, "already last");
2638 }
2639
2640 #[test]
2641 fn pinning_moves_to_the_group_boundary() {
2642 let mut state = state();
2643 assert_eq!(
2644 state.update(Event::Pin(4, true)),
2645 Some(Output::Pinned(4, true))
2646 );
2647 assert_eq!(ids(&state), [3, 4, 1, 2]);
2648 assert_eq!(state.update(Event::Pin(4, true)), None);
2649 assert_eq!(
2650 state.update(Event::Pin(3, false)),
2651 Some(Output::Pinned(3, false))
2652 );
2653 assert_eq!(ids(&state), [4, 3, 1, 2]);
2654 assert_eq!(state.pinned_count(), 1);
2655 }
2656
2657 fn items(state: &State<u8>) -> Vec<(MenuItem, bool)> {
2658 state
2659 .menu()
2660 .menu()
2661 .entries()
2662 .iter()
2663 .filter_map(|entry| entry.as_item())
2664 .map(|item| (item.id, item.disabled))
2665 .collect()
2666 }
2667
2668 #[test]
2669 fn the_context_menu_is_built_for_the_tab_it_opens_on() {
2670 let mut state = state().with_menu_items([item(MenuItem::Custom("copy-path"), "Copy path")]);
2671 let _ = state.update(Event::Menu(context_menu::Event::Open(4, Point::ORIGIN)));
2672 assert!(state.menu().is_open_on(&4));
2673 assert_eq!(
2674 items(&state),
2675 [
2676 (MenuItem::Close, false),
2677 (MenuItem::CloseOthers, false),
2678 (MenuItem::CloseSaved, false),
2679 (MenuItem::CloseAll, false),
2680 (MenuItem::Pin, false),
2681 (MenuItem::Custom("copy-path"), false),
2682 ],
2683 "the last tab has nothing to its right"
2684 );
2685 let _ = state.update(Event::Menu(context_menu::Event::OpenFromKeyboard(3)));
2686 assert!(items(&state).contains(&(MenuItem::Unpin, false)));
2687 assert_eq!(
2688 state.update(Event::Menu(context_menu::Event::Open(9, Point::ORIGIN))),
2689 None
2690 );
2691 }
2692
2693 #[test]
2694 fn menu_items_map_to_outputs() {
2695 let choose = |target: u8, item: MenuItem| {
2696 let mut state =
2697 state().with_menu_items([dropdown_menu::item(MenuItem::Custom("copy"), "Copy")]);
2698 let _ = state.update(Event::Menu(context_menu::Event::OpenFromKeyboard(target)));
2699 let output = state.update(Event::Menu(context_menu::Event::Menu(
2700 dropdown_menu::Event::Activate(item),
2701 )));
2702 assert!(!state.menu().is_open());
2703 (output, ids(&state))
2704 };
2705 assert_eq!(
2706 choose(1, MenuItem::Close).0,
2707 Some(Output::CloseRequested(vec![1]))
2708 );
2709 assert_eq!(
2710 choose(2, MenuItem::CloseOthers).0,
2711 Some(Output::CloseRequested(vec![1, 4]))
2712 );
2713 assert_eq!(
2714 choose(2, MenuItem::CloseToTheRight).0,
2715 Some(Output::CloseRequested(vec![4]))
2716 );
2717 assert_eq!(
2718 choose(1, MenuItem::CloseSaved).0,
2719 Some(Output::CloseRequested(vec![1, 4]))
2720 );
2721 assert_eq!(
2722 choose(1, MenuItem::CloseAll).0,
2723 Some(Output::CloseRequested(vec![1, 2, 4]))
2724 );
2725 assert_eq!(
2726 choose(4, MenuItem::Pin),
2727 (Some(Output::Pinned(4, true)), vec![3, 4, 1, 2])
2728 );
2729 assert_eq!(choose(3, MenuItem::Unpin).0, Some(Output::Pinned(3, false)));
2730 assert_eq!(
2731 choose(2, MenuItem::Custom("copy")).0,
2732 Some(Output::Custom("copy", 2))
2733 );
2734 }
2735
2736 #[test]
2737 fn the_overflow_menu_lists_every_document_and_activates_one() {
2738 let mut state = state();
2739 let _ = state.update(Event::Overflow(dropdown_menu::Event::Toggle));
2740 assert!(state.overflow().is_open());
2741 assert_eq!(state.overflow().entries().len(), 4);
2742 assert!(state.overflow().is_checked(3));
2743 let lib = state.overflow().item(2).cloned();
2744 assert_eq!(lib.and_then(|item| item.shortcut), Some("Modified".into()));
2745 assert_eq!(
2746 state.update(Event::Overflow(dropdown_menu::Event::Activate(4))),
2747 Some(Output::Activated(4))
2748 );
2749 assert!(!state.overflow().is_open());
2750 assert_eq!(state.update(Event::New), Some(Output::NewTab));
2751 }
2752
2753 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2754 let chord: Chord = chord.parse().unwrap();
2755 keymap.resolve(chord.key(), chord.modifiers())
2756 }
2757
2758 #[test]
2759 fn default_keymap_covers_every_action() {
2760 let keymap = default_keymap();
2761 assert_eq!(press(&keymap, "Ctrl+Tab"), Some(Action::RecentNext));
2762 assert_eq!(
2763 press(&keymap, "Ctrl+Shift+Tab"),
2764 Some(Action::RecentPrevious)
2765 );
2766 assert_eq!(press(&keymap, "Ctrl+PageDown"), Some(Action::Next));
2767 assert_eq!(press(&keymap, "Ctrl+PageUp"), Some(Action::Previous));
2768 assert_eq!(press(&keymap, "Ctrl+Shift+PageUp"), Some(Action::MoveLeft));
2769 assert_eq!(
2770 press(&keymap, "Ctrl+Shift+PageDown"),
2771 Some(Action::MoveRight)
2772 );
2773 assert_eq!(press(&keymap, "Mod+W"), Some(Action::Close));
2774 assert_eq!(press(&keymap, "Mod+N"), Some(Action::New));
2775 assert_eq!(press(&keymap, "Mod+1"), Some(Action::Tab(1)));
2776 assert_eq!(press(&keymap, "Mod+8"), Some(Action::Tab(8)));
2777 assert_eq!(press(&keymap, "Mod+9"), Some(Action::LastTab));
2778 assert_eq!(press(&keymap, "Tab"), None);
2779 assert_eq!(press(&keymap, "PageDown"), None);
2780 for action in <Action as keys::Action>::ALL {
2781 let unbound = *action == Action::OpenMenu;
2782 assert_eq!(keymap.chords(action).is_empty(), unbound, "{action:?}");
2783 assert!(keys::Action::description(*action).ends_with('.'));
2784 }
2785 assert_eq!(Keymap::<Action>::defaults(), keymap);
2786 }
2787
2788 #[test]
2789 fn opening_the_tab_menu_is_left_to_context_menus_by_default() {
2790 let keymap = default_keymap();
2791 for chord in ["Shift+F10", "Menu"] {
2792 assert_eq!(press(&keymap, chord), None, "{chord}");
2793 let context = context_menu::default_keymap();
2794 assert_eq!(
2795 press_menu(&context, chord),
2796 Some(context_menu::Action::Open)
2797 );
2798 }
2799 let bound = keymap.bind("Shift+F10".parse().unwrap(), Action::OpenMenu);
2800 assert_eq!(press(&bound, "Shift+F10"), Some(Action::OpenMenu));
2801 }
2802
2803 fn press_menu(
2804 keymap: &Keymap<context_menu::Action>,
2805 chord: &str,
2806 ) -> Option<context_menu::Action> {
2807 let chord: Chord = chord.parse().unwrap();
2808 keymap.resolve(chord.key(), chord.modifiers())
2809 }
2810
2811 #[test]
2812 fn default_keymap_leaves_plain_and_alt_keys_to_other_components() {
2813 let keymap = default_keymap();
2814 for chord in ["ArrowLeft", "ArrowRight", "Home", "End", "Alt+1", "Escape"] {
2815 assert_eq!(press(&keymap, chord), None, "{chord}");
2816 }
2817 }
2818
2819 #[test]
2820 fn keymap_overrides_and_unbinding() {
2821 let keymap = default_keymap()
2822 .unbind(&"Ctrl+Tab".parse().unwrap())
2823 .bind("Alt+PageDown".parse().unwrap(), Action::Next);
2824 assert_eq!(press(&keymap, "Ctrl+Tab"), None);
2825 assert_eq!(press(&keymap, "Alt+PageDown"), Some(Action::Next));
2826 assert_eq!(press(&keymap, "Ctrl+PageDown"), Some(Action::Next));
2827 let event = keys::Event {
2828 key: Key::Character("w".into()),
2829 modifiers: Modifiers::COMMAND,
2830 };
2831 assert_eq!(keymap.resolve_event(&event), Some(Action::Close));
2832 }
2833
2834 #[test]
2835 fn actions_map_to_events() {
2836 let state = state().with_active(1);
2837 assert_eq!(Action::RecentNext.event(&state), Some(Event::RecentNext));
2838 assert_eq!(
2839 Action::RecentPrevious.event(&state),
2840 Some(Event::RecentPrevious)
2841 );
2842 assert_eq!(Action::Next.event(&state), Some(Event::Next));
2843 assert_eq!(Action::Previous.event(&state), Some(Event::Previous));
2844 assert_eq!(Action::MoveLeft.event(&state), Some(Event::Move(1, 0)));
2845 assert_eq!(Action::MoveRight.event(&state), Some(Event::Move(1, 2)));
2846 assert_eq!(Action::Close.event(&state), Some(Event::Close(1)));
2847 assert_eq!(Action::New.event(&state), Some(Event::New));
2848 assert_eq!(Action::Tab(1).event(&state), Some(Event::Activate(3)));
2849 assert_eq!(Action::Tab(4).event(&state), Some(Event::Activate(4)));
2850 assert_eq!(Action::Tab(5).event(&state), None);
2851 assert_eq!(Action::Tab(0).event(&state), None);
2852 assert_eq!(Action::LastTab.event(&state), Some(Event::Activate(4)));
2853 assert_eq!(
2854 Action::OpenMenu.event(&state),
2855 Some(Event::Menu(context_menu::Event::OpenFromKeyboard(1)))
2856 );
2857 let first = state.clone().with_active(3);
2858 assert_eq!(Action::MoveLeft.event(&first), None);
2859
2860 let mut state = state;
2861 let event = Action::MoveRight.event(&state).unwrap();
2862 assert!(state.update(event).is_some());
2863 assert_eq!(ids(&state), [3, 2, 1, 4]);
2864 }
2865
2866 #[test]
2867 fn gaps_follow_tab_centres() {
2868 let tabs = [(0.0, 100.0), (100.0, 60.0), (160.0, 80.0)];
2869 assert_eq!(gap_at(&tabs, -5.0), 0);
2870 assert_eq!(gap_at(&tabs, 49.0), 0);
2871 assert_eq!(gap_at(&tabs, 51.0), 1);
2872 assert_eq!(gap_at(&tabs, 131.0), 2);
2873 assert_eq!(gap_at(&tabs, 500.0), 3);
2874 }
2875
2876 #[test]
2877 fn gaps_are_kept_within_the_group() {
2878 assert_eq!(clamp_gap(0, 4, 2, 5), 2);
2879 assert_eq!(clamp_gap(3, 0, 2, 5), 2);
2880 assert_eq!(clamp_gap(3, 5, 2, 5), 5);
2881 assert_eq!(clamp_gap(1, 0, 0, 3), 0);
2882 }
2883
2884 #[test]
2885 fn dropping_next_to_itself_does_not_move() {
2886 assert_eq!(drop_index(2, 2), None);
2887 assert_eq!(drop_index(2, 3), None);
2888 assert_eq!(drop_index(2, 0), Some(0));
2889 assert_eq!(drop_index(0, 3), Some(2));
2890 }
2891
2892 #[test]
2893 fn memory_reveals_and_clamps() {
2894 let mut memory = Memory {
2895 content_width: 600.0,
2896 viewport_width: 200.0,
2897 ..Memory::default()
2898 };
2899 memory.reveal(450.0, 550.0);
2900 assert_eq!(memory.offset, 350.0);
2901 memory.reveal(100.0, 180.0);
2902 assert_eq!(memory.offset, 100.0);
2903 assert!(memory.scroll_by(1000.0));
2904 assert_eq!(memory.offset, 400.0);
2905 assert!(!memory.scroll_by(10.0));
2906 assert!(memory.scroll_by(-1000.0));
2907 assert_eq!(memory.offset, 0.0);
2908 }
2909
2910 fn offset_after(widths: &[f32]) -> f32 {
2913 let mut memory = Memory::default();
2914 for width in widths {
2915 memory.follow((Some(4), 6), (600.0, *width), Some((450.0, 100.0)));
2916 }
2917 memory.offset
2918 }
2919
2920 #[test]
2921 fn the_first_offset_does_not_depend_on_earlier_sizes() {
2922 assert_eq!(offset_after(&[300.0]), 250.0, "active tab ends at the edge");
2923 assert_eq!(offset_after(&[1000.0, 300.0]), 250.0);
2924 assert_eq!(offset_after(&[0.0, 300.0]), 250.0);
2925 assert_eq!(offset_after(&[200.0, 300.0]), 250.0);
2926 assert_eq!(offset_after(&[200.0, 300.0, 300.0]), 250.0);
2927 }
2928
2929 #[test]
2930 fn a_new_active_tab_scrolls_the_least_distance_and_keeps_wheel_scrolling() {
2931 let mut memory = Memory::default();
2932 memory.follow((Some(4), 6), (600.0, 300.0), Some((450.0, 100.0)));
2933 memory.follow((Some(3), 6), (600.0, 300.0), Some((350.0, 100.0)));
2934 assert_eq!(memory.offset, 250.0, "already in view");
2935 memory.follow((Some(0), 6), (600.0, 300.0), Some((0.0, 100.0)));
2936 assert_eq!(memory.offset, 0.0);
2937 let _ = memory.scroll_by(120.0);
2938 memory.follow((Some(0), 6), (600.0, 300.0), Some((0.0, 100.0)));
2939 assert_eq!(memory.offset, 120.0, "a relayout leaves the wheel offset");
2940 }
2941
2942 #[test]
2943 fn pointer_opens_place_the_menu_below_the_tab() {
2944 let mut state = state();
2945 let _ = state.update(Event::Menu(context_menu::Event::Open(
2946 1,
2947 Point::new(40.0, 12.0),
2948 )));
2949 assert!(state.menu().is_open_on(&1));
2950 assert_eq!(
2951 state.menu().position(),
2952 Some(Point::new(0.0, HEIGHT + KEYBOARD_GAP))
2953 );
2954 assert_eq!(state.menu().menu().highlighted(), None);
2955 }
2956
2957 #[test]
2958 fn active_tab_joins_the_page_and_others_sit_on_the_strip() {
2959 for theme in [light(), dark()] {
2960 let tokens = Tokens::of(&theme);
2961 let active = tab_style(&tokens, true, false);
2962 let idle = tab_style(&tokens, false, false);
2963 let hovered = tab_style(&tokens, false, true);
2964 assert_eq!(
2965 active.background,
2966 Some(Background::Color(tokens.background))
2967 );
2968 assert_eq!(idle.background, None);
2969 assert!(hovered.background.is_some());
2970 assert_ne!(hovered.background, active.background);
2971 assert_eq!(active.text_color, Some(tokens.foreground));
2972 assert_eq!(idle.text_color, Some(tokens.muted_foreground));
2973 assert_eq!(hovered.text_color, Some(tokens.foreground));
2974 assert_eq!(active.border.radius.bottom_left, 0.0);
2975 assert_eq!(active.border.radius.top_left, radius::MD);
2976 assert_eq!(strip_style(&tokens).background, tokens.muted);
2977 assert_ne!(strip_style(&tokens).background, tokens.background);
2978 }
2979 }
2980
2981 #[test]
2982 fn disabled_labels_fade() {
2983 let tokens = Tokens::of(&light());
2984 let on = label_colour(&tokens, true, false, true);
2985 let off = label_colour(&tokens, true, false, false);
2986 assert!(off.a < on.a);
2987 }
2988
2989 #[test]
2990 fn close_button_shows_a_surface_on_hover_only() {
2991 for theme in [light(), dark()] {
2992 let tokens = Tokens::of(&theme);
2993 assert!(close_style(&tokens, Status::Active).background.is_none());
2994 assert!(close_style(&tokens, Status::Disabled).background.is_none());
2995 assert!(close_style(&tokens, Status::Hovered).background.is_some());
2996 assert!(close_style(&tokens, Status::Pressed).background.is_some());
2997 }
2998 }
2999
3000 #[test]
3001 fn modified_dot_is_stronger_on_the_active_tab() {
3002 let tokens = Tokens::of(&dark());
3003 assert_eq!(
3004 dot_style(&tokens, true).background,
3005 Some(Background::Color(tokens.foreground))
3006 );
3007 assert_eq!(
3008 dot_style(&tokens, false).background,
3009 Some(Background::Color(tokens.muted_foreground))
3010 );
3011 }
3012
3013 #[test]
3014 fn slots_show_close_dot_or_space() {
3015 let look = |active, hovered| Look {
3016 active,
3017 hovered,
3018 enabled: true,
3019 };
3020 let saved = document(1, "a");
3021 let modified = document(1, "a").modified(true);
3022 let pinned = document(1, "a").pinned(true);
3023 let has_slot =
3024 |document: &Document<u8>, look| slot::<u8, ()>(document, look, None).is_some();
3025 assert!(has_slot(&saved, look(false, false)));
3026 assert!(has_slot(&modified, look(true, false)));
3027 assert!(!has_slot(&pinned, look(true, true)));
3028 assert!(has_slot(&pinned.clone().modified(true), look(false, false)));
3029 }
3030
3031 #[test]
3032 fn builder_defaults() {
3033 let state = state();
3034 let tabs: DocumentTabs<'_, u8, ()> = document_tabs(&state);
3035 assert!(!tabs.new_tab);
3036 assert_eq!(tabs.max_label_width, MAX_LABEL_WIDTH);
3037 assert_eq!(tabs.context_menu_keymap, context_menu::default_keymap());
3038 assert!(!tabs.is_enabled());
3039 let tabs: DocumentTabs<'_, u8, ()> = document_tabs(&state)
3040 .new_tab(true)
3041 .max_label_width(120.0)
3042 .context_menu_keymap(Keymap::new())
3043 .overflow_keymap(Keymap::new())
3044 .on_event(|_| ());
3045 assert!(tabs.new_tab);
3046 assert_eq!(tabs.max_label_width, 120.0);
3047 assert!(tabs.context_menu_keymap.is_empty());
3048 assert!(tabs.overflow_keymap.is_empty());
3049 assert!(tabs.is_enabled());
3050 }
3051
3052 #[test]
3053 fn documents_are_closable_and_unlocked_by_default() {
3054 let plain = document(1, "a");
3055 assert!(plain.closable);
3056 assert!(!plain.locked);
3057 let kept = plain.closable(false).locked(true);
3058 assert!(!kept.closable);
3059 assert!(kept.locked);
3060 }
3061
3062 fn kept() -> State<u8> {
3064 State::new([
3065 document(0, "All jobs")
3066 .pinned(true)
3067 .closable(false)
3068 .locked(true),
3069 document(1, "a.rs"),
3070 document(2, "b.rs"),
3071 document(3, "c.rs"),
3072 ])
3073 }
3074
3075 fn offered(state: &mut State<u8>, target: u8) -> Vec<MenuItem> {
3078 let _ = state.update(Event::Menu(context_menu::Event::OpenFromKeyboard(target)));
3079 if !state.menu().is_open() {
3080 return Vec::new();
3081 }
3082 let entries = state.menu().menu().entries();
3083 assert!(
3084 entries
3085 .first()
3086 .is_some_and(|entry| entry.as_item().is_some())
3087 );
3088 assert!(
3089 entries
3090 .last()
3091 .is_some_and(|entry| entry.as_item().is_some())
3092 );
3093 let has_double = entries
3094 .windows(2)
3095 .any(|pair| pair[0].as_item().is_none() && pair[1].as_item().is_none());
3096 assert!(!has_double, "two separators in a row");
3097 items(state)
3098 .into_iter()
3099 .map(|(id, disabled)| {
3100 assert!(!disabled, "{id:?} is disabled");
3101 id
3102 })
3103 .collect()
3104 }
3105
3106 #[test]
3107 fn the_menu_hides_close_and_pin_on_a_kept_tab() {
3108 let mut state = kept();
3109 assert_eq!(
3110 offered(&mut state, 0),
3111 [MenuItem::CloseAll],
3112 "Close others and Close to the right would close the same tabs"
3113 );
3114 assert_eq!(
3115 offered(&mut state, 1),
3116 [
3117 MenuItem::Close,
3118 MenuItem::CloseOthers,
3119 MenuItem::CloseAll,
3120 MenuItem::Pin,
3121 ],
3122 "Close to the right would close what Close others does"
3123 );
3124 assert_eq!(
3125 offered(&mut state, 3),
3126 [
3127 MenuItem::Close,
3128 MenuItem::CloseOthers,
3129 MenuItem::CloseAll,
3130 MenuItem::Pin
3131 ]
3132 );
3133 }
3134
3135 #[test]
3136 fn a_lone_tab_beside_a_kept_one_offers_close_without_close_all() {
3137 let mut state = State::new([
3138 document(0, "All jobs")
3139 .pinned(true)
3140 .closable(false)
3141 .locked(true),
3142 document(1, "orders-etl"),
3143 ]);
3144 assert_eq!(offered(&mut state, 1), [MenuItem::Close, MenuItem::Pin]);
3145 assert_eq!(offered(&mut state, 0), [MenuItem::CloseAll]);
3146 }
3147
3148 #[test]
3149 fn a_pinned_closable_tab_offers_close_and_unpin() {
3150 let mut state = State::new([document(0, "notes").pinned(true), document(1, "a.rs")]);
3151 assert_eq!(
3152 offered(&mut state, 0),
3153 [MenuItem::Close, MenuItem::CloseAll, MenuItem::Unpin],
3154 "the bulk closes all close just a.rs"
3155 );
3156 }
3157
3158 #[test]
3159 fn a_menu_with_nothing_to_offer_does_not_open() {
3160 let mut alone = State::new([document(0, "All jobs")
3161 .pinned(true)
3162 .closable(false)
3163 .locked(true)]);
3164 assert!(offered(&mut alone, 0).is_empty());
3165 assert!(!alone.menu().is_open());
3166
3167 let mut extra = alone.with_menu_items([item(MenuItem::Custom("refresh"), "Refresh")]);
3168 assert_eq!(offered(&mut extra, 0), [MenuItem::Custom("refresh")]);
3169 assert_eq!(extra.menu().menu().entries().len(), 1, "no separators");
3170 }
3171
3172 #[test]
3173 fn close_saved_shows_only_when_it_differs_from_close_all() {
3174 let mut state = kept();
3175 assert!(!offered(&mut state, 1).contains(&MenuItem::CloseSaved));
3176
3177 state.set_modified(2, true);
3178 assert!(offered(&mut state, 1).contains(&MenuItem::CloseSaved));
3179
3180 for id in [1, 3] {
3181 state.set_modified(id, true);
3182 }
3183 assert!(
3184 !offered(&mut state, 1).contains(&MenuItem::CloseSaved),
3185 "every tab it would look at is modified"
3186 );
3187
3188 let mut pinned_only = kept();
3189 pinned_only.set_modified(0, true);
3190 assert!(
3191 !offered(&mut pinned_only, 1).contains(&MenuItem::CloseSaved),
3192 "a modified pinned tab is never closed in bulk"
3193 );
3194 }
3195
3196 #[test]
3197 fn close_requests_skip_unclosable_documents() {
3198 let mut state = State::new([
3199 document(0, "All jobs").pinned(true).closable(false),
3200 document(1, "log").closable(false),
3201 document(2, "a.rs"),
3202 document(3, "b.rs").modified(true),
3203 ]);
3204 assert_eq!(state.update(Event::Close(0)), None);
3205 assert_eq!(state.update(Event::Close(1)), None);
3206 assert_eq!(
3207 state.update(Event::Close(2)),
3208 Some(Output::CloseRequested(vec![2]))
3209 );
3210 assert_eq!(
3211 state.update(Event::CloseOthers(2)),
3212 Some(Output::CloseRequested(vec![3]))
3213 );
3214 assert_eq!(
3215 state.update(Event::CloseToTheRight(0)),
3216 Some(Output::CloseRequested(vec![2, 3]))
3217 );
3218 assert_eq!(
3219 state.update(Event::CloseSaved),
3220 Some(Output::CloseRequested(vec![2]))
3221 );
3222 assert_eq!(
3223 state.update(Event::CloseAll),
3224 Some(Output::CloseRequested(vec![2, 3]))
3225 );
3226 assert!(!offered(&mut state, 1).contains(&MenuItem::Close));
3227
3228 assert_eq!(Action::Close.event(&state.clone().with_active(1)), None);
3229 assert_eq!(Action::Close.event(&state.clone().with_active(0)), None);
3230 assert_eq!(
3231 Action::Close.event(&state.with_active(2)),
3232 Some(Event::Close(2))
3233 );
3234 }
3235
3236 #[test]
3237 fn unclosable_tabs_have_no_close_slot() {
3238 let look = Look {
3239 active: true,
3240 hovered: true,
3241 enabled: true,
3242 };
3243 let has_slot = |document: &Document<u8>| slot::<u8, ()>(document, look, None).is_some();
3244 assert!(has_slot(&document(1, "a")));
3245 assert!(!has_slot(&document(1, "a").closable(false)));
3246 assert!(has_slot(&document(1, "a").closable(false).modified(true)));
3247 }
3248
3249 #[test]
3250 fn pin_events_leave_locked_documents_alone() {
3251 let mut state = kept();
3252 let mut locked = State::new([document(5, "x").locked(true), document(6, "y")]);
3253 assert_eq!(state.update(Event::Pin(0, false)), None);
3254 assert_eq!(state.pinned_count(), 1);
3255 assert_eq!(locked.update(Event::Pin(5, true)), None);
3256 assert_eq!(locked.pinned_count(), 0);
3257 assert_eq!(
3258 locked.update(Event::Pin(6, true)),
3259 Some(Output::Pinned(6, true))
3260 );
3261 }
3262
3263 #[test]
3264 fn locked_tabs_still_move_within_their_group() {
3265 let mut state = State::new([
3266 document(0, "jobs").pinned(true).locked(true),
3267 document(1, "notes").pinned(true),
3268 document(2, "a.rs"),
3269 ]);
3270 assert_eq!(
3271 state.update(Event::Move(0, 9)),
3272 Some(Output::Moved {
3273 id: 0,
3274 from: 0,
3275 to: 1
3276 }),
3277 "clamped to the end of the pinned tabs"
3278 );
3279 assert_eq!(ids(&state), [1, 0, 2]);
3280 assert_eq!(state.update(Event::Move(2, 0)), None);
3281 assert_eq!(
3282 clamp_gap(1, 3, 2, 3),
3283 2,
3284 "a dragged pinned tab stays pinned"
3285 );
3286 assert_eq!(clamp_gap(2, 0, 2, 3), 2, "an unpinned tab stays unpinned");
3287 }
3288
3289 #[test]
3290 fn pinned_tabs_stay_unless_they_take_most_of_a_narrow_strip() {
3291 assert!(pinned_stay(100.0, 250.0, true));
3292 assert!(pinned_stay(125.0, 250.0, true));
3293 assert!(!pinned_stay(126.0, 250.0, true));
3294 assert!(pinned_stay(400.0, 250.0, false), "everything fits");
3295 assert!(pinned_stay(0.0, 0.0, true));
3296 }
3297
3298 fn split() -> (Memory, Vec<(f32, f32)>) {
3301 let spans: Vec<(f32, f32)> = (0..7).map(|index| (index as f32 * 100.0, 100.0)).collect();
3302 let mut memory = Memory {
3303 fixed: 100.0,
3304 fixed_tabs: 1,
3305 ..Memory::default()
3306 };
3307 let active = memory.scroll_span(&spans, 6);
3308 memory.follow((Some(6), 7), (600.0, 150.0), active);
3309 (memory, spans)
3310 }
3311
3312 #[test]
3313 fn the_active_tab_is_revealed_after_the_pinned_tabs() {
3314 let (mut memory, spans) = split();
3315 assert_eq!(memory.scroll_span(&spans, 6), Some((500.0, 100.0)));
3316 assert_eq!(memory.offset, 450.0);
3317 assert_eq!(memory.visible_span(&spans, 6), Some((150.0, 250.0)));
3318 assert_eq!(memory.visible_span(&spans, 0), Some((0.0, 100.0)));
3319 assert_eq!(memory.visible_span(&spans, 1), None, "scrolled away");
3320 assert_eq!(
3321 memory
3322 .viewport(Rectangle::new(
3323 Point::new(10.0, 0.0),
3324 Size::new(300.0, 36.0)
3325 ))
3326 .x,
3327 110.0
3328 );
3329
3330 assert_eq!(memory.scroll_span(&spans, 0), None);
3331 memory.follow((Some(0), 7), (600.0, 150.0), None);
3332 assert_eq!(memory.offset, 450.0, "a pinned tab is already in view");
3333 memory.follow((Some(1), 7), (600.0, 150.0), memory.scroll_span(&spans, 1));
3334 assert_eq!(memory.offset, 0.0);
3335 assert_eq!(memory.visible_span(&spans, 1), Some((100.0, 200.0)));
3336 }
3337
3338 #[test]
3339 fn pointers_find_pinned_tabs_in_place_and_others_scrolled() {
3340 let (memory, _) = split();
3341 assert_eq!(memory.locate(-1.0), None);
3342 assert_eq!(memory.locate(50.0), Some(50.0));
3343 assert_eq!(memory.locate(105.0), Some(555.0));
3344 assert_eq!(memory.locate(250.0), None, "past the scrolling tabs");
3345 assert_eq!(memory.unscroll(50.0), 50.0);
3346 assert_eq!(memory.unscroll(400.0), 850.0);
3347
3348 let fallback = Memory {
3349 offset: 30.0,
3350 viewport_width: 200.0,
3351 ..Memory::default()
3352 };
3353 assert_eq!(fallback.locate(50.0), Some(80.0), "everything scrolls");
3354 assert_eq!(
3355 fallback.pinned_frame(Rectangle::default()).1,
3356 Vector::new(30.0, 0.0)
3357 );
3358 }
3359
3360 #[test]
3361 fn scrolled_tabs_keep_clear_of_the_pinned_divider() {
3362 let (memory, spans) = split();
3363 assert_eq!(memory.scroll_start(), 100.0 + PINNED_GAP);
3364 let clip = memory.scroll_clip(Rectangle::new(Point::ORIGIN, Size::new(400.0, 36.0)));
3365 assert_eq!(
3366 (clip.x, clip.width),
3367 (100.0 + PINNED_GAP, 150.0 - PINNED_GAP)
3368 );
3369
3370 let still = Memory {
3371 offset: 0.0,
3372 ..memory
3373 };
3374 assert_eq!(still.scroll_start(), 100.0, "nothing is cut off yet");
3375
3376 let close = Memory {
3378 offset: 497.0,
3379 ..memory
3380 };
3381 assert!(
3382 !close
3383 .dividers(&spans, Some(0))
3384 .iter()
3385 .any(|x| *x > 100.0 && *x < 100.0 + PINNED_GAP),
3386 "no second line right beside the pinned one"
3387 );
3388 }
3389
3390 #[test]
3391 fn dividers_mark_the_end_of_the_pinned_tabs() {
3392 let (memory, spans) = split();
3393 assert_eq!(memory.dividers(&spans, Some(6)), [100.0]);
3394 assert_eq!(memory.dividers(&spans, Some(0)), [150.0]);
3395 assert_eq!(
3396 memory.dividers(&spans, Some(1)),
3397 [100.0, 150.0],
3398 "the active tab is scrolled away from the edge"
3399 );
3400 let still = Memory {
3401 offset: 0.0,
3402 ..memory
3403 };
3404 assert_eq!(still.dividers(&spans, Some(1)), Vec::<f32>::new());
3405 }
3406
3407 #[test]
3408 fn the_drop_indicator_stays_in_the_dragged_tab_region() {
3409 let (memory, spans) = split();
3410 let half = INDICATOR_WIDTH / 2.0;
3411 assert_eq!(memory.indicator_x(&spans, 0, 0), Some(0.0));
3412 assert_eq!(
3413 memory.indicator_x(&spans, 0, 1),
3414 Some(100.0 - INDICATOR_WIDTH),
3415 "the end of the pinned tabs"
3416 );
3417 assert_eq!(
3418 memory.indicator_x(&spans, 6, 1),
3419 Some(100.0),
3420 "the first unpinned gap is scrolled away, so it sits at the edge"
3421 );
3422 assert_eq!(memory.indicator_x(&spans, 6, 6), Some(150.0 - half));
3423 assert_eq!(
3424 memory.indicator_x(&spans, 6, 7),
3425 Some(250.0 - INDICATOR_WIDTH)
3426 );
3427 assert_eq!(memory.indicator_x(&[], 0, 0), None);
3428 }
3429}