1use std::rc::Rc;
25
26use iced::advanced::layout::{self, Layout};
27use iced::advanced::mouse::{self, click};
28use iced::advanced::overlay;
29use iced::advanced::renderer;
30use iced::advanced::widget::operation::scrollable::{AbsoluteOffset, RelativeOffset, Scrollable};
31use iced::advanced::widget::{Operation, Tree, Widget, tree};
32use iced::advanced::{Clipboard, Renderer as _, Shell};
33use iced::alignment::Vertical;
34use iced::border::Radius;
35use iced::keyboard::{self, key::Named};
36use iced::touch::{self, Finger};
37use iced::widget::{self, button::Status, container, row};
38use iced::{
39 Alignment, Background, Border, Color, Element, Event as Input, Length, Padding, Point,
40 Rectangle, Renderer, Size, Subscription, Theme, Vector,
41};
42
43use crate::data::ellipsis::ellipsis;
44use crate::icon::{Glyph, opacity, themed};
45use crate::keys::{self, Chord, Keymap};
46use crate::overlay::anchored::{Align, Placement, Side, anchored};
47use crate::overlay::context_menu::{self, KEYBOARD_GAP};
48use crate::overlay::dropdown_menu::{self, Entry, item, radio_item, separator};
49use crate::overlay::menu::{self, RowStatus};
50use crate::overlay::scrolling::{fitted, scrolling};
51use crate::overlay::tooltip;
52use crate::primitives::button::Size as ButtonSize;
53use crate::primitives::icon_button::icon_button;
54use crate::theme::{Tokens, fade, mix, radius, space, text_size};
55
56pub const HEIGHT: f32 = 36.0;
58
59pub const MAX_LABEL_WIDTH: f32 = 200.0;
61
62pub const PINNED_LABEL_WIDTH: f32 = 80.0;
64
65pub const OVERFLOW_WIDTH: f32 = 320.0;
67
68pub const DRAG_THRESHOLD: f32 = 4.0;
70
71const SLOT: f32 = 16.0;
73const CLOSE_ICON: f32 = 12.0;
74const ICON: f32 = 16.0;
75const DOT: f32 = 8.0;
76const PADDING_X: f32 = 12.0;
77const DIVIDER_HEIGHT: f32 = 16.0;
78const INDICATOR_WIDTH: f32 = 2.0;
79const WHEEL_LINE: f32 = 40.0;
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct Document<Id> {
85 pub id: Id,
86 pub label: String,
87 pub icon: Option<Glyph>,
88 pub modified: bool,
90 pub pinned: bool,
91}
92
93pub fn document<Id>(id: Id, label: impl Into<String>) -> Document<Id> {
95 Document {
96 id,
97 label: label.into(),
98 icon: None,
99 modified: false,
100 pinned: false,
101 }
102}
103
104impl<Id> Document<Id> {
105 pub fn icon(mut self, glyph: Glyph) -> Self {
106 self.icon = Some(glyph);
107 self
108 }
109
110 pub fn modified(mut self, modified: bool) -> Self {
111 self.modified = modified;
112 self
113 }
114
115 pub fn pinned(mut self, pinned: bool) -> Self {
116 self.pinned = pinned;
117 self
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum MenuItem {
124 Close,
125 CloseOthers,
126 CloseToTheRight,
127 CloseSaved,
128 CloseAll,
129 Pin,
130 Unpin,
131 Custom(&'static str),
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
138pub enum Event<Id> {
139 Activate(Id),
141 Next,
143 Previous,
145 RecentNext,
148 RecentPrevious,
150 EndCycle,
153 Close(Id),
155 CloseOthers(Id),
157 CloseToTheRight(Id),
159 CloseSaved,
161 CloseAll,
163 Move(Id, usize),
166 Pin(Id, bool),
169 New,
172 Menu(context_menu::Event<MenuItem, Id>),
174 Overflow(dropdown_menu::Event<Id>),
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180pub struct CycleEnd;
181
182impl<Id> From<CycleEnd> for Event<Id> {
183 fn from(_: CycleEnd) -> Self {
184 Event::EndCycle
185 }
186}
187
188pub fn cycle_end() -> Subscription<CycleEnd> {
191 keyboard::listen().filter_map(|event| match event {
192 keyboard::Event::ModifiersChanged(modifiers) if modifiers.is_empty() => Some(CycleEnd),
193 _ => None,
194 })
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum Output<Id> {
200 Activated(Id),
202 CloseRequested(Vec<Id>),
206 Moved { id: Id, from: usize, to: usize },
208 Pinned(Id, bool),
210 NewTab,
212 Custom(&'static str, Id),
214}
215
216#[derive(Debug, Clone, PartialEq)]
218pub struct State<Id> {
219 documents: Vec<Document<Id>>,
220 active: Option<Id>,
221 recent: Vec<Id>,
223 cycle: Option<usize>,
225 menu: context_menu::State<MenuItem, Id>,
226 extra: Vec<Entry<MenuItem>>,
227 overflow: dropdown_menu::State<Id>,
228}
229
230impl<Id: Copy + PartialEq> State<Id> {
231 pub fn new(documents: impl IntoIterator<Item = Document<Id>>) -> Self {
234 let (mut documents, others): (Vec<_>, Vec<_>) =
235 documents.into_iter().partition(|document| document.pinned);
236 documents.extend(others);
237 let recent = documents.iter().map(|document| document.id).collect();
238 Self {
239 active: documents.first().map(|document| document.id),
240 documents,
241 recent,
242 cycle: None,
243 menu: context_menu::State::new([]),
244 extra: Vec::new(),
245 overflow: dropdown_menu::State::new([]),
246 }
247 }
248
249 pub fn with_active(mut self, id: Id) -> Self {
251 let _ = self.activate(id);
252 self
253 }
254
255 pub fn with_menu_items(mut self, entries: impl IntoIterator<Item = Entry<MenuItem>>) -> Self {
258 self.extra = entries.into_iter().collect();
259 self
260 }
261
262 pub fn documents(&self) -> &[Document<Id>] {
264 &self.documents
265 }
266
267 pub fn document(&self, id: Id) -> Option<&Document<Id>> {
268 self.documents.iter().find(|document| document.id == id)
269 }
270
271 pub fn active(&self) -> Option<Id> {
272 self.active
273 }
274
275 pub fn is_active(&self, id: Id) -> bool {
276 self.active == Some(id)
277 }
278
279 pub fn recent(&self) -> &[Id] {
281 &self.recent
282 }
283
284 pub fn menu(&self) -> &context_menu::State<MenuItem, Id> {
286 &self.menu
287 }
288
289 pub fn overflow(&self) -> &dropdown_menu::State<Id> {
291 &self.overflow
292 }
293
294 pub fn pinned_count(&self) -> usize {
296 self.documents
297 .iter()
298 .take_while(|document| document.pinned)
299 .count()
300 }
301
302 fn index_of(&self, id: Id) -> Option<usize> {
303 self.documents.iter().position(|document| document.id == id)
304 }
305
306 fn active_index(&self) -> Option<usize> {
307 self.active.and_then(|id| self.index_of(id))
308 }
309
310 pub fn open(&mut self, document: Document<Id>) {
313 self.end_cycle();
314 let id = document.id;
315 if self.index_of(id).is_none() {
316 let index = if document.pinned {
317 self.pinned_count()
318 } else {
319 self.documents.len()
320 };
321 self.documents.insert(index, document);
322 self.recent.push(id);
323 }
324 let _ = self.activate(id);
325 }
326
327 pub fn close(&mut self, ids: &[Id]) -> Option<Id> {
330 self.end_cycle();
331 self.documents
332 .retain(|document| !ids.contains(&document.id));
333 self.recent.retain(|id| !ids.contains(id));
334 let closed = self.active.is_some_and(|id| ids.contains(&id));
335 if !closed {
336 return None;
337 }
338 self.active = self.recent.first().copied();
339 self.active
340 }
341
342 pub fn set_modified(&mut self, id: Id, modified: bool) {
344 if let Some(document) = self.document_mut(id) {
345 document.modified = modified;
346 }
347 }
348
349 pub fn set_label(&mut self, id: Id, label: impl Into<String>) {
350 if let Some(document) = self.document_mut(id) {
351 document.label = label.into();
352 }
353 }
354
355 fn document_mut(&mut self, id: Id) -> Option<&mut Document<Id>> {
356 self.documents.iter_mut().find(|document| document.id == id)
357 }
358
359 pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
361 if !matches!(event, Event::RecentNext | Event::RecentPrevious) {
362 self.end_cycle();
363 }
364 match event {
365 Event::Activate(id) => self.activate(id),
366 Event::Next => self.step(true),
367 Event::Previous => self.step(false),
368 Event::RecentNext => self.walk(true),
369 Event::RecentPrevious => self.walk(false),
370 Event::EndCycle => None,
371 Event::Close(id) => self.request(|document| document.id == id),
372 Event::CloseOthers(id) => {
373 self.request(|document| document.id != id && !document.pinned)
374 }
375 Event::CloseToTheRight(id) => {
376 let index = self.index_of(id)?;
377 let right: Vec<Id> = self.documents[index + 1..]
378 .iter()
379 .filter(|document| !document.pinned)
380 .map(|document| document.id)
381 .collect();
382 (!right.is_empty()).then_some(Output::CloseRequested(right))
383 }
384 Event::CloseSaved => self.request(|document| !document.modified && !document.pinned),
385 Event::CloseAll => self.request(|document| !document.pinned),
386 Event::Move(id, to) => self.move_to(id, to),
387 Event::Pin(id, pinned) => self.pin(id, pinned),
388 Event::New => Some(Output::NewTab),
389 Event::Menu(event) => self.menu_event(event),
390 Event::Overflow(event) => self.overflow_event(event),
391 }
392 }
393
394 fn activate(&mut self, id: Id) -> Option<Output<Id>> {
395 self.index_of(id)?;
396 if self.active == Some(id) {
397 return None;
398 }
399 self.active = Some(id);
400 self.touch(id);
401 Some(Output::Activated(id))
402 }
403
404 fn touch(&mut self, id: Id) {
406 self.recent.retain(|recent| *recent != id);
407 self.recent.insert(0, id);
408 }
409
410 fn step(&mut self, forward: bool) -> Option<Output<Id>> {
411 let len = self.documents.len();
412 let current = self.active_index()?;
413 let next = if forward {
414 (current + 1) % len
415 } else {
416 (current + len - 1) % len
417 };
418 let id = self.documents[next].id;
419 self.activate(id)
420 }
421
422 fn walk(&mut self, forward: bool) -> Option<Output<Id>> {
423 let len = self.recent.len();
424 if len < 2 {
425 return None;
426 }
427 let current = self.cycle.unwrap_or(0);
428 let next = if forward {
429 (current + 1) % len
430 } else {
431 (current + len - 1) % len
432 };
433 self.cycle = Some(next);
434 let id = self.recent[next];
435 self.active = Some(id);
436 Some(Output::Activated(id))
437 }
438
439 fn end_cycle(&mut self) {
440 if self.cycle.take().is_none() {
441 return;
442 }
443 if let Some(id) = self.active {
444 self.touch(id);
445 }
446 }
447
448 fn request(&self, close: impl Fn(&Document<Id>) -> bool) -> Option<Output<Id>> {
449 let ids: Vec<Id> = self
450 .documents
451 .iter()
452 .filter(|document| close(document))
453 .map(|document| document.id)
454 .collect();
455 (!ids.is_empty()).then_some(Output::CloseRequested(ids))
456 }
457
458 fn move_to(&mut self, id: Id, to: usize) -> Option<Output<Id>> {
459 let from = self.index_of(id)?;
460 let pinned = self.pinned_count();
461 let (start, end) = if from < pinned {
462 (0, pinned - 1)
463 } else {
464 (pinned, self.documents.len() - 1)
465 };
466 let to = to.clamp(start, end);
467 if to == from {
468 return None;
469 }
470 let document = self.documents.remove(from);
471 self.documents.insert(to, document);
472 Some(Output::Moved { id, from, to })
473 }
474
475 fn pin(&mut self, id: Id, pinned: bool) -> Option<Output<Id>> {
476 let from = self.index_of(id)?;
477 if self.documents[from].pinned == pinned {
478 return None;
479 }
480 let mut document = self.documents.remove(from);
481 document.pinned = pinned;
482 let to = self.pinned_count();
483 self.documents.insert(to, document);
484 Some(Output::Pinned(id, pinned))
485 }
486
487 fn menu_event(&mut self, event: context_menu::Event<MenuItem, Id>) -> Option<Output<Id>> {
488 let event = match event {
489 context_menu::Event::Open(target, _) => {
492 context_menu::Event::Open(target, Point::new(0.0, HEIGHT + KEYBOARD_GAP))
493 }
494 event => event,
495 };
496 if let context_menu::Event::Open(target, _)
497 | context_menu::Event::OpenFromKeyboard(target) = event
498 {
499 self.menu = context_menu::State::new(self.menu_entries(target)?);
500 }
501 let dropdown_menu::Output::Activated(item) = self.menu.update(event)? else {
502 return None;
503 };
504 let target = *self.menu.target()?;
505 let event = match item {
506 MenuItem::Close => Event::Close(target),
507 MenuItem::CloseOthers => Event::CloseOthers(target),
508 MenuItem::CloseToTheRight => Event::CloseToTheRight(target),
509 MenuItem::CloseSaved => Event::CloseSaved,
510 MenuItem::CloseAll => Event::CloseAll,
511 MenuItem::Pin => Event::Pin(target, true),
512 MenuItem::Unpin => Event::Pin(target, false),
513 MenuItem::Custom(name) => return Some(Output::Custom(name, target)),
514 };
515 self.update(event)
516 }
517
518 fn menu_entries(&self, target: Id) -> Option<Vec<Entry<MenuItem>>> {
521 let index = self.index_of(target)?;
522 let unpinned = |document: &&Document<Id>| !document.pinned;
523 let others = self
524 .documents
525 .iter()
526 .filter(unpinned)
527 .any(|document| document.id != target);
528 let right = self.documents[index + 1..].iter().any(|d| !d.pinned);
529 let saved = self
530 .documents
531 .iter()
532 .filter(unpinned)
533 .any(|document| !document.modified);
534 let any = self.documents.iter().any(|document| !document.pinned);
535 let pin = if self.documents[index].pinned {
536 item(MenuItem::Unpin, "Unpin")
537 } else {
538 item(MenuItem::Pin, "Pin")
539 };
540
541 let mut entries = vec![
542 item(MenuItem::Close, "Close"),
543 item(MenuItem::CloseOthers, "Close others").disabled(!others),
544 item(MenuItem::CloseToTheRight, "Close to the right").disabled(!right),
545 item(MenuItem::CloseSaved, "Close saved").disabled(!saved),
546 item(MenuItem::CloseAll, "Close all").disabled(!any),
547 separator(),
548 pin,
549 ];
550 if !self.extra.is_empty() {
551 entries.push(separator());
552 entries.extend(self.extra.iter().cloned());
553 }
554 Some(entries)
555 }
556
557 fn overflow_event(&mut self, event: dropdown_menu::Event<Id>) -> Option<Output<Id>> {
558 if !self.overflow.is_open() {
559 self.overflow = dropdown_menu::State::new(self.documents.iter().map(|document| {
560 let entry = radio_item(
561 document.id,
562 document.label.clone(),
563 self.active == Some(document.id),
564 );
565 if document.modified {
566 entry.shortcut("Modified")
567 } else {
568 entry
569 }
570 }));
571 }
572 match self.overflow.update(event)? {
573 dropdown_menu::Output::Activated(id) | dropdown_menu::Output::Selected(id) => {
574 self.activate(id)
575 }
576 dropdown_menu::Output::Toggled(..) => None,
577 }
578 }
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
583pub enum Action {
584 RecentNext,
585 RecentPrevious,
586 Next,
587 Previous,
588 MoveLeft,
589 MoveRight,
590 Close,
591 New,
592 Tab(u8),
594 LastTab,
596 OpenMenu,
598}
599
600impl Action {
601 pub fn event<Id: Copy + PartialEq>(self, state: &State<Id>) -> Option<Event<Id>> {
604 let active = state.active;
605 let index = state.active_index();
606 match self {
607 Action::RecentNext => Some(Event::RecentNext),
608 Action::RecentPrevious => Some(Event::RecentPrevious),
609 Action::Next => Some(Event::Next),
610 Action::Previous => Some(Event::Previous),
611 Action::MoveLeft => {
612 let index = index?.checked_sub(1)?;
613 Some(Event::Move(active?, index))
614 }
615 Action::MoveRight => Some(Event::Move(active?, index? + 1)),
616 Action::Close => active.map(Event::Close),
617 Action::New => Some(Event::New),
618 Action::Tab(position) => {
619 let index = usize::from(position).checked_sub(1)?;
620 state
621 .documents
622 .get(index)
623 .map(|document| Event::Activate(document.id))
624 }
625 Action::LastTab => state
626 .documents
627 .last()
628 .map(|document| Event::Activate(document.id)),
629 Action::OpenMenu => {
630 active.map(|id| Event::Menu(context_menu::Event::OpenFromKeyboard(id)))
631 }
632 }
633 }
634}
635
636impl keys::Action for Action {
637 const ALL: &'static [Self] = &[
638 Action::RecentNext,
639 Action::RecentPrevious,
640 Action::Next,
641 Action::Previous,
642 Action::MoveLeft,
643 Action::MoveRight,
644 Action::Close,
645 Action::New,
646 Action::Tab(1),
647 Action::Tab(2),
648 Action::Tab(3),
649 Action::Tab(4),
650 Action::Tab(5),
651 Action::Tab(6),
652 Action::Tab(7),
653 Action::Tab(8),
654 Action::LastTab,
655 Action::OpenMenu,
656 ];
657
658 fn defaults() -> Keymap<Self> {
659 default_keymap()
660 }
661
662 fn name(self) -> &'static str {
663 match self {
664 Action::RecentNext => "RecentNext",
665 Action::RecentPrevious => "RecentPrevious",
666 Action::Next => "Next",
667 Action::Previous => "Previous",
668 Action::MoveLeft => "MoveLeft",
669 Action::MoveRight => "MoveRight",
670 Action::Close => "Close",
671 Action::New => "New",
672 Action::Tab(1) => "Tab1",
673 Action::Tab(2) => "Tab2",
674 Action::Tab(3) => "Tab3",
675 Action::Tab(4) => "Tab4",
676 Action::Tab(5) => "Tab5",
677 Action::Tab(6) => "Tab6",
678 Action::Tab(7) => "Tab7",
679 Action::Tab(8) => "Tab8",
680 Action::Tab(_) => "Tab",
681 Action::LastTab => "LastTab",
682 Action::OpenMenu => "OpenMenu",
683 }
684 }
685
686 fn description(self) -> &'static str {
687 match self {
688 Action::RecentNext => {
689 "Activates the previously used document. Pressed again while the modifier is held, goes further back."
690 }
691 Action::RecentPrevious => "Walks the recently used documents the other way.",
692 Action::Next => "Activates the tab to the right, wrapping at the end.",
693 Action::Previous => "Activates the tab to the left, wrapping at the start.",
694 Action::MoveLeft => "Moves the active tab one place left, within its group.",
695 Action::MoveRight => "Moves the active tab one place right, within its group.",
696 Action::Close => "Asks to close the active document.",
697 Action::New => "Asks for a new document.",
698 Action::Tab(1) => "Activates the first tab.",
699 Action::Tab(2) => "Activates the second tab.",
700 Action::Tab(3) => "Activates the third tab.",
701 Action::Tab(4) => "Activates the fourth tab.",
702 Action::Tab(5) => "Activates the fifth tab.",
703 Action::Tab(6) => "Activates the sixth tab.",
704 Action::Tab(7) => "Activates the seventh tab.",
705 Action::Tab(8) => "Activates the eighth tab.",
706 Action::Tab(_) => "Activates the tab at that position.",
707 Action::LastTab => "Activates the last tab.",
708 Action::OpenMenu => "Opens the active tab's context menu.",
709 }
710 }
711}
712
713pub fn default_keymap() -> Keymap<Action> {
751 let keymap = Keymap::new()
752 .bind(Chord::named(Named::Tab).ctrl(), Action::RecentNext)
753 .bind(
754 Chord::named(Named::Tab).ctrl().shift(),
755 Action::RecentPrevious,
756 )
757 .bind(Chord::named(Named::PageDown).ctrl(), Action::Next)
758 .bind(Chord::named(Named::PageUp).ctrl(), Action::Previous)
759 .bind(Chord::named(Named::PageUp).ctrl().shift(), Action::MoveLeft)
760 .bind(
761 Chord::named(Named::PageDown).ctrl().shift(),
762 Action::MoveRight,
763 )
764 .bind(Chord::character('w').command(), Action::Close)
765 .bind(Chord::character('n').command(), Action::New);
766 let keymap = ('1'..='8')
767 .zip(1..)
768 .fold(keymap, |keymap, (digit, position)| {
769 keymap.bind(Chord::character(digit).command(), Action::Tab(position))
770 });
771 keymap.bind(Chord::character('9').command(), Action::LastTab)
772}
773
774type OnEvent<'a, Id, Message> = dyn Fn(Event<Id>) -> Message + 'a;
775
776pub struct DocumentTabs<'a, Id, Message> {
778 state: &'a State<Id>,
779 new_tab: bool,
780 max_label_width: f32,
781 context_menu_keymap: Keymap<context_menu::Action>,
782 overflow_keymap: Keymap<dropdown_menu::Action>,
783 on_event: Option<Box<OnEvent<'a, Id, Message>>>,
784}
785
786pub fn document_tabs<Id, Message>(state: &State<Id>) -> DocumentTabs<'_, Id, Message> {
789 DocumentTabs {
790 state,
791 new_tab: false,
792 max_label_width: MAX_LABEL_WIDTH,
793 context_menu_keymap: context_menu::default_keymap(),
794 overflow_keymap: dropdown_menu::default_keymap(),
795 on_event: None,
796 }
797}
798
799impl<Id: std::fmt::Debug, Message> std::fmt::Debug for DocumentTabs<'_, Id, Message> {
800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801 f.debug_struct("DocumentTabs")
802 .field("state", self.state)
803 .field("new_tab", &self.new_tab)
804 .field("max_label_width", &self.max_label_width)
805 .finish_non_exhaustive()
806 }
807}
808
809impl<'a, Id, Message> DocumentTabs<'a, Id, Message> {
810 pub fn new_tab(mut self, new_tab: bool) -> Self {
813 self.new_tab = new_tab;
814 self
815 }
816
817 pub fn max_label_width(mut self, width: f32) -> Self {
820 self.max_label_width = width;
821 self
822 }
823
824 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
826 self.context_menu_keymap = keymap;
827 self
828 }
829
830 pub fn overflow_keymap(mut self, keymap: Keymap<dropdown_menu::Action>) -> Self {
832 self.overflow_keymap = keymap;
833 self
834 }
835
836 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
837 self.on_event = Some(Box::new(on_event));
838 self
839 }
840
841 pub fn is_enabled(&self) -> bool {
842 self.on_event.is_some()
843 }
844}
845
846#[derive(Debug, Clone, Copy, PartialEq, Eq)]
848struct Look {
849 active: bool,
850 hovered: bool,
851 enabled: bool,
852}
853
854impl<'a, Id, Message> From<DocumentTabs<'a, Id, Message>> for Element<'a, Message>
855where
856 Id: Copy + PartialEq + 'a,
857 Message: Clone + 'a,
858{
859 fn from(tabs: DocumentTabs<'a, Id, Message>) -> Self {
860 let DocumentTabs {
861 state,
862 new_tab,
863 max_label_width,
864 context_menu_keymap,
865 overflow_keymap,
866 on_event,
867 } = tabs;
868 let on_event: Option<Rc<OnEvent<'a, Id, Message>>> = on_event.map(Rc::from);
869 let enabled = on_event.is_some();
870
871 let tabs: Vec<Element<'a, Message>> = state
872 .documents
873 .iter()
874 .map(|document| {
875 let active = state.is_active(document.id);
876 let on_close = on_event
877 .as_ref()
878 .map(|on_event| on_event(Event::Close(document.id)));
879 let look = Look {
880 active,
881 hovered: false,
882 enabled,
883 };
884 let base = tab_view(document, look, max_label_width, on_close.clone(), false);
885 let Some(on_event) = on_event.clone() else {
886 return base;
887 };
888 let hovered = Look {
889 hovered: true,
890 ..look
891 };
892 let top = tab_view(document, hovered, max_label_width, on_close, true);
893 context_menu::keyed(state.menu(), document.id, widget::hover(base, top))
894 .keymap(context_menu_keymap.clone())
895 .on_event(move |event| on_event(Event::Menu(event)))
896 .into()
897 })
898 .collect();
899
900 let trailing: Element<'a, Message> = if new_tab {
901 icon_button(crate::lucide!(Plus))
902 .label("New tab")
903 .tooltip(Some(tooltip::Position::Bottom))
904 .size(ButtonSize::Sm)
905 .on_press_maybe(on_event.as_ref().map(|on_event| on_event(Event::New)))
906 .into()
907 } else {
908 widget::space().into()
909 };
910
911 let open = state.overflow().is_open();
912 let toggle = on_event
913 .as_ref()
914 .map(|on_event| on_event(Event::Overflow(dropdown_menu::Event::Toggle)));
915 let trigger = icon_button(crate::lucide!(ChevronDown))
916 .label("All tabs")
917 .tooltip(None)
918 .size(ButtonSize::Sm)
919 .pressed(open)
920 .on_press_maybe(toggle);
921 let mut overflow = anchored(trigger)
922 .content(open.then(|| overflow_panel(state, on_event.as_deref())))
923 .placement(Placement::new(Side::Bottom, Align::End))
924 .fit_height(true)
925 .dismiss_keys([]);
926 if let Some(on_event) = on_event.clone().filter(|_| open) {
927 overflow = overflow
928 .on_dismiss(on_event(Event::Overflow(dropdown_menu::Event::Close)))
929 .on_key(move |key| {
930 state
931 .overflow()
932 .key_event(&overflow_keymap, key)
933 .map(|event| on_event(Event::Overflow(event)))
934 });
935 }
936
937 let ids: Vec<Id> = state.documents.iter().map(|document| document.id).collect();
938 let on_gesture = on_event.map(|on_event| {
939 Box::new(move |gesture: Gesture| {
940 let event = match gesture {
941 Gesture::Press(index) => Event::Activate(*ids.get(index)?),
942 Gesture::Middle(index) => Event::Close(*ids.get(index)?),
943 Gesture::Drop { from, to } => Event::Move(*ids.get(from)?, to),
944 Gesture::DoubleClick if new_tab => Event::New,
945 Gesture::DoubleClick => return None,
946 };
947 Some(on_event(event))
948 }) as Box<dyn Fn(Gesture) -> Option<Message> + 'a>
949 });
950
951 Element::new(Strip {
952 content: row(tabs).into(),
953 trailing,
954 overflow: overflow.into(),
955 active: state.active_index(),
956 count: state.documents.len(),
957 pinned: state.pinned_count(),
958 on_gesture,
959 })
960 }
961}
962
963fn tab_view<'a, Id, Message>(
964 document: &'a Document<Id>,
965 look: Look,
966 max_label_width: f32,
967 on_close: Option<Message>,
968 fill: bool,
969) -> Element<'a, Message>
970where
971 Id: 'a,
972 Message: Clone + 'a,
973{
974 let Look {
975 active,
976 hovered,
977 enabled,
978 } = look;
979 let colour = move |theme: &Theme| label_colour(&Tokens::of(theme), active, hovered, enabled);
980
981 let mut content = row![].spacing(space::SM).align_y(Alignment::Center);
982 if let Some(glyph) = document.icon {
983 content = content.push(themed(glyph, ICON, opacity(enabled), colour));
984 }
985 if !document.pinned || document.icon.is_none() {
986 let width = if document.pinned {
987 PINNED_LABEL_WIDTH
988 } else {
989 max_label_width
990 };
991 let label = ellipsis(document.label.as_str())
992 .width(Length::Shrink)
993 .colour(colour);
994 content = content.push(container(label).max_width(width));
995 }
996 if let Some(slot) = slot(document, look, on_close) {
997 content = content.push(slot);
998 }
999
1000 container(content)
1001 .width(if fill { Length::Fill } else { Length::Shrink })
1002 .height(HEIGHT)
1003 .padding(Padding::from([0.0, PADDING_X]))
1004 .align_y(Vertical::Center)
1005 .style(move |theme| tab_style(&Tokens::of(theme), active, hovered))
1006 .into()
1007}
1008
1009fn overflow_panel<'a, Id, Message>(
1012 state: &'a State<Id>,
1013 on_event: Option<&OnEvent<'a, Id, Message>>,
1014) -> Element<'a, Message>
1015where
1016 Id: Copy + PartialEq + 'a,
1017 Message: Clone + 'a,
1018{
1019 let highlighted = state.overflow().highlighted();
1020 let rows = state.documents.iter().map(|document| {
1021 let status = match (on_event.is_some(), highlighted == Some(document.id)) {
1022 (false, _) => RowStatus::Disabled,
1023 (true, true) => RowStatus::Highlighted,
1024 (true, false) => RowStatus::Idle,
1025 };
1026 overflow_row(document, state.is_active(document.id), status, on_event)
1027 });
1028 let reveal = highlighted
1029 .and_then(|id| state.index_of(id))
1030 .or_else(|| state.active_index());
1031 menu::surface(scrolling(fitted(rows, OVERFLOW_WIDTH), reveal)).into()
1032}
1033
1034fn overflow_row<'a, Id, Message>(
1037 document: &'a Document<Id>,
1038 active: bool,
1039 status: RowStatus,
1040 on_event: Option<&OnEvent<'a, Id, Message>>,
1041) -> Element<'a, Message>
1042where
1043 Id: Copy + 'a,
1044 Message: Clone + 'a,
1045{
1046 let colours = move |theme: &Theme| menu::row_style(&Tokens::of(theme), status, false);
1047 let opacity = opacity(status != RowStatus::Disabled);
1048
1049 let mark: Element<'a, Message> = if active {
1050 themed(
1051 crate::lucide!(Check),
1052 menu::ICON_SIZE,
1053 opacity,
1054 move |theme| colours(theme).text,
1055 )
1056 .into()
1057 } else {
1058 widget::space().into()
1059 };
1060 let mut content = row![
1061 container(mark)
1062 .center_x(menu::ICON_SIZE)
1063 .center_y(menu::ICON_SIZE)
1064 ]
1065 .spacing(space::SM)
1066 .align_y(Alignment::Center);
1067 if let Some(glyph) = document.icon {
1068 content = content.push(themed(glyph, menu::ICON_SIZE, opacity, move |theme| {
1069 colours(theme).icon
1070 }));
1071 }
1072 content = content.push(
1073 ellipsis(document.label.as_str())
1074 .width(Length::Fill)
1075 .colour(move |theme| colours(theme).text),
1076 );
1077 if document.modified {
1078 content = content.push(
1079 widget::text("Modified")
1080 .size(text_size::XS)
1081 .wrapping(widget::text::Wrapping::None)
1082 .style(move |theme| widget::text::Style {
1083 color: Some(colours(theme).hint),
1084 }),
1085 );
1086 }
1087
1088 let body = container(content)
1089 .padding(menu::ROW_PADDING)
1090 .width(Length::Fill)
1091 .style(move |theme| menu::row_container_style(colours(theme)));
1092 let Some(on_event) = on_event.filter(|_| status != RowStatus::Disabled) else {
1093 return menu::inset(body);
1094 };
1095 let id = document.id;
1096 menu::inset(
1097 widget::mouse_area(body)
1098 .on_enter(on_event(Event::Overflow(dropdown_menu::Event::Highlight(
1099 id,
1100 ))))
1101 .on_press(on_event(Event::Overflow(dropdown_menu::Event::Activate(
1102 id,
1103 ))))
1104 .interaction(mouse::Interaction::Pointer),
1105 )
1106}
1107
1108fn slot<'a, Id, Message>(
1112 document: &Document<Id>,
1113 look: Look,
1114 on_close: Option<Message>,
1115) -> Option<Element<'a, Message>>
1116where
1117 Message: Clone + 'a,
1118{
1119 let active = look.active;
1120 let close = !document.pinned && (look.hovered || (active && !document.modified));
1121 if close {
1122 return Some(close_button(on_close));
1123 }
1124 if document.modified {
1125 let dot = container(widget::space())
1126 .width(DOT)
1127 .height(DOT)
1128 .style(move |theme| dot_style(&Tokens::of(theme), active));
1129 return Some(container(dot).center(SLOT).into());
1130 }
1131 (!document.pinned).then(|| widget::space().width(SLOT).height(SLOT).into())
1132}
1133
1134fn close_button<'a, Message: Clone + 'a>(on_close: Option<Message>) -> Element<'a, Message> {
1135 let enabled = on_close.is_some();
1136 widget::button(themed(
1137 crate::lucide!(X),
1138 CLOSE_ICON,
1139 opacity(enabled),
1140 |theme| Tokens::of(theme).foreground,
1141 ))
1142 .width(SLOT)
1143 .height(SLOT)
1144 .padding((SLOT - CLOSE_ICON) / 2.0)
1145 .on_press_maybe(on_close)
1146 .style(|theme, status| close_style(&Tokens::of(theme), status))
1147 .into()
1148}
1149
1150pub fn label_colour(tokens: &Tokens, active: bool, hovered: bool, enabled: bool) -> Color {
1152 let colour = if active || hovered {
1153 tokens.foreground
1154 } else {
1155 tokens.muted_foreground
1156 };
1157 if enabled { colour } else { fade(colour, 0.5) }
1158}
1159
1160pub fn tab_style(tokens: &Tokens, active: bool, hovered: bool) -> container::Style {
1163 let background = if active {
1164 Some(tokens.background)
1165 } else if hovered {
1166 Some(mix(tokens.muted, tokens.foreground, 0.06))
1167 } else {
1168 None
1169 };
1170 container::Style {
1171 background: background.map(Background::Color),
1172 text_color: Some(label_colour(tokens, active, hovered, true)),
1173 border: Border {
1174 radius: Radius {
1175 top_left: radius::MD,
1176 top_right: radius::MD,
1177 bottom_right: 0.0,
1178 bottom_left: 0.0,
1179 },
1180 ..Border::default()
1181 },
1182 ..container::Style::default()
1183 }
1184}
1185
1186pub fn close_style(tokens: &Tokens, status: Status) -> widget::button::Style {
1188 let background = match status {
1189 Status::Hovered | Status::Pressed => Some(fade(tokens.foreground, 0.1)),
1190 Status::Active | Status::Disabled => None,
1191 };
1192 widget::button::Style {
1193 background: background.map(Background::Color),
1194 text_color: tokens.foreground,
1195 border: Border {
1196 radius: radius::SM.into(),
1197 ..Border::default()
1198 },
1199 ..widget::button::Style::default()
1200 }
1201}
1202
1203pub fn dot_style(tokens: &Tokens, active: bool) -> container::Style {
1205 let colour = if active {
1206 tokens.foreground
1207 } else {
1208 tokens.muted_foreground
1209 };
1210 container::Style {
1211 background: Some(Background::Color(colour)),
1212 border: Border {
1213 radius: radius::FULL.into(),
1214 ..Border::default()
1215 },
1216 ..container::Style::default()
1217 }
1218}
1219
1220#[derive(Debug, Clone, Copy, PartialEq)]
1222pub struct StripStyle {
1223 pub background: Color,
1224 pub line: Color,
1226 pub divider: Color,
1228 pub indicator: Color,
1230}
1231
1232pub fn strip_style(tokens: &Tokens) -> StripStyle {
1233 StripStyle {
1234 background: tokens.muted,
1235 line: tokens.border,
1236 divider: tokens.border,
1237 indicator: tokens.primary,
1238 }
1239}
1240
1241fn gap_at(tabs: &[(f32, f32)], x: f32) -> usize {
1244 tabs.iter()
1245 .take_while(|(left, width)| left + width / 2.0 < x)
1246 .count()
1247}
1248
1249fn clamp_gap(from: usize, gap: usize, pinned: usize, count: usize) -> usize {
1252 if from < pinned {
1253 gap.min(pinned)
1254 } else {
1255 gap.clamp(pinned, count)
1256 }
1257}
1258
1259fn drop_index(from: usize, gap: usize) -> Option<usize> {
1262 let to = if gap > from { gap - 1 } else { gap };
1263 (to != from).then_some(to)
1264}
1265
1266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1268enum Gesture {
1269 Press(usize),
1270 Middle(usize),
1271 Drop { from: usize, to: usize },
1272 DoubleClick,
1273}
1274
1275#[derive(Debug, Clone, Copy, PartialEq)]
1277struct Press {
1278 index: usize,
1279 origin: Point,
1280 dragging: bool,
1281 gap: Option<usize>,
1282}
1283
1284#[derive(Debug, Clone, Default)]
1286struct Memory {
1287 offset: f32,
1288 content_width: f32,
1289 viewport_width: f32,
1290 overflowing: bool,
1291 revealed: Option<(Option<usize>, usize)>,
1293 measured: Option<(f32, f32)>,
1295 press: Option<Press>,
1296 last_click: Option<mouse::Click>,
1297 touch: Option<(Finger, f32)>,
1299}
1300
1301impl Scrollable for Memory {
1302 fn snap_to(&mut self, offset: RelativeOffset<Option<f32>>) {
1303 if let Some(x) = offset.x {
1304 self.offset = (x.clamp(0.0, 1.0) * self.max_offset()).round();
1305 }
1306 }
1307
1308 fn scroll_to(&mut self, offset: AbsoluteOffset<Option<f32>>) {
1309 if let Some(x) = offset.x {
1310 self.offset = x.clamp(0.0, self.max_offset());
1311 }
1312 }
1313
1314 fn scroll_by(&mut self, offset: AbsoluteOffset, _bounds: Rectangle, _content: Rectangle) {
1315 let _ = Memory::scroll_by(self, offset.x);
1316 }
1317}
1318
1319impl Memory {
1320 fn max_offset(&self) -> f32 {
1321 (self.content_width - self.viewport_width).max(0.0)
1322 }
1323
1324 fn scroll_by(&mut self, delta: f32) -> bool {
1325 let offset = (self.offset + delta).clamp(0.0, self.max_offset());
1326 let changed = offset != self.offset;
1327 self.offset = offset;
1328 changed
1329 }
1330
1331 fn reveal(&mut self, left: f32, right: f32) {
1334 if right > self.offset + self.viewport_width {
1335 self.offset = right - self.viewport_width;
1336 }
1337 if left < self.offset {
1338 self.offset = left;
1339 }
1340 self.offset = self.offset.clamp(0.0, self.max_offset());
1341 }
1342
1343 fn follow(
1350 &mut self,
1351 key: (Option<usize>, usize),
1352 widths: (f32, f32),
1353 active: Option<(f32, f32)>,
1354 ) {
1355 (self.content_width, self.viewport_width) = widths;
1356 let moved = self.revealed != Some(key);
1357 let resized = self.measured != Some(widths);
1358 self.revealed = Some(key);
1359 self.measured = Some(widths);
1360 if !moved && resized {
1361 self.offset = 0.0;
1362 }
1363 if let Some((left, width)) = active.filter(|_| moved || resized) {
1364 self.reveal(left, left + width);
1365 }
1366 self.offset = self.offset.clamp(0.0, self.max_offset());
1367 }
1368
1369 fn viewport(&self, bounds: Rectangle) -> Rectangle {
1370 Rectangle {
1371 width: self.viewport_width,
1372 ..bounds
1373 }
1374 }
1375
1376 fn shift(&self) -> Vector {
1377 Vector::new(self.offset, 0.0)
1378 }
1379
1380 fn inner_cursor(&self, cursor: mouse::Cursor, viewport: Rectangle) -> mouse::Cursor {
1382 match cursor.position_over(viewport) {
1383 Some(position) => mouse::Cursor::Available(position + self.shift()),
1384 None => cursor.levitate() + self.shift(),
1385 }
1386 }
1387}
1388
1389struct Strip<'a, Message> {
1392 content: Element<'a, Message>,
1393 trailing: Element<'a, Message>,
1394 overflow: Element<'a, Message>,
1395 active: Option<usize>,
1396 count: usize,
1397 pinned: usize,
1398 on_gesture: Option<Box<dyn Fn(Gesture) -> Option<Message> + 'a>>,
1399}
1400
1401impl<Message> Strip<'_, Message> {
1402 fn publish(&self, shell: &mut Shell<'_, Message>, gesture: Gesture) {
1403 if let Some(message) = self.on_gesture.as_ref().and_then(|on| on(gesture)) {
1404 shell.publish(message);
1405 }
1406 }
1407}
1408
1409fn spans(content: Layout<'_>) -> Vec<(f32, f32)> {
1411 let origin = content.bounds().x;
1412 content
1413 .children()
1414 .map(|tab| {
1415 let bounds = tab.bounds();
1416 (bounds.x - origin, bounds.width)
1417 })
1418 .collect()
1419}
1420
1421impl<Message> Widget<Message, Theme, Renderer> for Strip<'_, Message> {
1422 fn size(&self) -> Size<Length> {
1423 Size::new(Length::Fill, Length::Shrink)
1424 }
1425
1426 fn tag(&self) -> tree::Tag {
1427 tree::Tag::of::<Memory>()
1428 }
1429
1430 fn state(&self) -> tree::State {
1431 tree::State::new(Memory::default())
1432 }
1433
1434 fn children(&self) -> Vec<Tree> {
1435 vec![
1436 Tree::new(&self.content),
1437 Tree::new(&self.trailing),
1438 Tree::new(&self.overflow),
1439 ]
1440 }
1441
1442 fn diff(&self, tree: &mut Tree) {
1443 tree.diff_children(&[&self.content, &self.trailing, &self.overflow]);
1444 }
1445
1446 fn layout(
1447 &mut self,
1448 tree: &mut Tree,
1449 renderer: &Renderer,
1450 limits: &layout::Limits,
1451 ) -> layout::Node {
1452 let max = limits.max();
1453 let [content_tree, trailing_tree, overflow_tree] = tree.children.as_mut_slice() else {
1454 return layout::Node::new(Size::ZERO);
1455 };
1456 let unbounded = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, max.height));
1457 let content = self
1458 .content
1459 .as_widget_mut()
1460 .layout(content_tree, renderer, &unbounded);
1461 let loose = layout::Limits::new(Size::ZERO, max);
1462 let trailing = self
1463 .trailing
1464 .as_widget_mut()
1465 .layout(trailing_tree, renderer, &loose);
1466 let overflow = self
1467 .overflow
1468 .as_widget_mut()
1469 .layout(overflow_tree, renderer, &loose);
1470
1471 let content_size = content.size();
1472 let trailing_size = trailing.size();
1473 let overflow_size = overflow.size();
1474 let natural = content_size.width + trailing_size.width;
1475 let width = if max.width.is_finite() {
1476 max.width
1477 } else {
1478 natural
1479 };
1480 let height = content_size
1481 .height
1482 .max(trailing_size.height)
1483 .max(overflow_size.height)
1484 .max(limits.min().height);
1485 let overflowing = natural > width + 0.5;
1486 let viewport_width = if overflowing {
1487 (width - trailing_size.width - overflow_size.width).max(0.0)
1488 } else {
1489 content_size.width
1490 };
1491 let centre = |size: Size| (height - size.height) / 2.0;
1492
1493 let spans: Vec<(f32, f32)> = content
1494 .children()
1495 .iter()
1496 .map(|tab| (tab.bounds().x, tab.bounds().width))
1497 .collect();
1498
1499 let memory = tree.state.downcast_mut::<Memory>();
1500 memory.overflowing = overflowing;
1501 memory.follow(
1502 (self.active, self.count),
1503 (content_size.width, viewport_width),
1504 self.active.and_then(|index| spans.get(index).copied()),
1505 );
1506
1507 let overflow_x = width - overflow_size.width;
1508 layout::Node::with_children(
1509 Size::new(width, height),
1510 vec![
1511 content.move_to(Point::new(0.0, centre(content_size))),
1512 trailing.move_to(Point::new(viewport_width, centre(trailing_size))),
1513 overflow.move_to(Point::new(overflow_x, centre(overflow_size))),
1514 ],
1515 )
1516 }
1517
1518 fn draw(
1519 &self,
1520 tree: &Tree,
1521 renderer: &mut Renderer,
1522 theme: &Theme,
1523 style: &renderer::Style,
1524 layout: Layout<'_>,
1525 cursor: mouse::Cursor,
1526 viewport: &Rectangle,
1527 ) {
1528 let memory = tree.state.downcast_ref::<Memory>();
1529 let colours = strip_style(&Tokens::of(theme));
1530 let bounds = layout.bounds();
1531 let mut children = layout.children();
1532 let (Some(content), Some(trailing), Some(overflow)) =
1533 (children.next(), children.next(), children.next())
1534 else {
1535 return;
1536 };
1537
1538 fill(renderer, bounds, colours.background);
1539
1540 let clip = memory.viewport(bounds);
1541 let inner = memory.inner_cursor(cursor, clip);
1542 let tabs: Vec<Rectangle> = content.children().map(|tab| tab.bounds()).collect();
1543 renderer.with_layer(clip, |renderer| {
1544 renderer.with_translation(Vector::new(-memory.offset, 0.0), |renderer| {
1545 self.content.as_widget().draw(
1546 &tree.children[0],
1547 renderer,
1548 theme,
1549 style,
1550 content,
1551 inner,
1552 &(clip + memory.shift()),
1553 );
1554 self.draw_marks(renderer, memory, &tabs, colours, bounds);
1555 });
1556 });
1557
1558 if memory.overflowing {
1560 let top = bounds.y + (bounds.height - DIVIDER_HEIGHT) / 2.0;
1561 fill(
1562 renderer,
1563 Rectangle::new(
1564 Point::new((clip.x + clip.width).round(), top),
1565 Size::new(1.0, DIVIDER_HEIGHT),
1566 ),
1567 colours.divider,
1568 );
1569 }
1570
1571 let line = Rectangle {
1573 y: bounds.y + bounds.height - 1.0,
1574 height: 1.0,
1575 ..bounds
1576 };
1577 let gap = self
1578 .active
1579 .and_then(|index| tabs.get(index))
1580 .map(|tab| *tab - memory.shift())
1581 .and_then(|tab| tab.intersection(&clip));
1582 match gap {
1583 Some(tab) => {
1584 fill(
1585 renderer,
1586 Rectangle {
1587 width: tab.x - line.x,
1588 ..line
1589 },
1590 colours.line,
1591 );
1592 let right = tab.x + tab.width;
1593 fill(
1594 renderer,
1595 Rectangle {
1596 x: right,
1597 width: line.x + line.width - right,
1598 ..line
1599 },
1600 colours.line,
1601 );
1602 }
1603 None => fill(renderer, line, colours.line),
1604 }
1605
1606 self.trailing.as_widget().draw(
1607 &tree.children[1],
1608 renderer,
1609 theme,
1610 style,
1611 trailing,
1612 cursor,
1613 viewport,
1614 );
1615 if memory.overflowing {
1616 self.overflow.as_widget().draw(
1617 &tree.children[2],
1618 renderer,
1619 theme,
1620 style,
1621 overflow,
1622 cursor,
1623 viewport,
1624 );
1625 }
1626 }
1627
1628 fn operate(
1629 &mut self,
1630 tree: &mut Tree,
1631 layout: Layout<'_>,
1632 renderer: &Renderer,
1633 operation: &mut dyn Operation,
1634 ) {
1635 let mut children = layout.children();
1636 let (Some(content), Some(trailing), Some(overflow)) =
1637 (children.next(), children.next(), children.next())
1638 else {
1639 return;
1640 };
1641 let [content_tree, trailing_tree, overflow_tree] = tree.children.as_mut_slice() else {
1642 return;
1643 };
1644 let memory = tree.state.downcast_mut::<Memory>();
1645 let clip = memory.viewport(layout.bounds());
1646 let shift = memory.shift();
1647 operation.scrollable(None, clip, content.bounds(), shift, memory);
1648 operation.traverse(&mut |operation| {
1649 self.content
1650 .as_widget_mut()
1651 .operate(content_tree, content, renderer, operation);
1652 });
1653 self.trailing
1654 .as_widget_mut()
1655 .operate(trailing_tree, trailing, renderer, operation);
1656 self.overflow
1657 .as_widget_mut()
1658 .operate(overflow_tree, overflow, renderer, operation);
1659 }
1660
1661 fn update(
1662 &mut self,
1663 tree: &mut Tree,
1664 event: &Input,
1665 layout: Layout<'_>,
1666 cursor: mouse::Cursor,
1667 renderer: &Renderer,
1668 clipboard: &mut dyn Clipboard,
1669 shell: &mut Shell<'_, Message>,
1670 viewport: &Rectangle,
1671 ) {
1672 let bounds = layout.bounds();
1673 let mut children = layout.children();
1674 let (Some(content), Some(trailing), Some(overflow)) =
1675 (children.next(), children.next(), children.next())
1676 else {
1677 return;
1678 };
1679 let spans = spans(content);
1680
1681 if self.drag(tree, event, cursor, content.bounds().x, &spans, shell) {
1682 return;
1683 }
1684
1685 let memory = tree.state.downcast_ref::<Memory>();
1686 let clip = memory.viewport(bounds);
1687 let inner = memory.inner_cursor(cursor, clip);
1688 let inner_viewport = clip + memory.shift();
1689 let overflowing = memory.overflowing;
1690 let [content_tree, trailing_tree, overflow_tree] = tree.children.as_mut_slice() else {
1691 return;
1692 };
1693 self.content.as_widget_mut().update(
1694 content_tree,
1695 event,
1696 content,
1697 inner,
1698 renderer,
1699 clipboard,
1700 shell,
1701 &inner_viewport,
1702 );
1703 self.trailing.as_widget_mut().update(
1704 trailing_tree,
1705 event,
1706 trailing,
1707 cursor,
1708 renderer,
1709 clipboard,
1710 shell,
1711 viewport,
1712 );
1713 if overflowing {
1714 self.overflow.as_widget_mut().update(
1715 overflow_tree,
1716 event,
1717 overflow,
1718 cursor,
1719 renderer,
1720 clipboard,
1721 shell,
1722 viewport,
1723 );
1724 }
1725 if shell.is_event_captured() {
1726 return;
1727 }
1728
1729 let memory = tree.state.downcast_mut::<Memory>();
1730 let content_x = content.bounds().x;
1731 self.pointer(
1732 memory, event, cursor, bounds, clip, content_x, &spans, shell,
1733 );
1734 }
1735
1736 fn mouse_interaction(
1737 &self,
1738 tree: &Tree,
1739 layout: Layout<'_>,
1740 cursor: mouse::Cursor,
1741 viewport: &Rectangle,
1742 renderer: &Renderer,
1743 ) -> mouse::Interaction {
1744 let memory = tree.state.downcast_ref::<Memory>();
1745 if memory.press.is_some_and(|press| press.dragging) {
1746 return mouse::Interaction::Grabbing;
1747 }
1748 let bounds = layout.bounds();
1749 let mut children = layout.children();
1750 let (Some(content), Some(trailing), Some(overflow)) =
1751 (children.next(), children.next(), children.next())
1752 else {
1753 return mouse::Interaction::None;
1754 };
1755 let clip = memory.viewport(bounds);
1756 let inner = memory.inner_cursor(cursor, clip);
1757 let interaction = self
1758 .content
1759 .as_widget()
1760 .mouse_interaction(
1761 &tree.children[0],
1762 content,
1763 inner,
1764 &(clip + memory.shift()),
1765 renderer,
1766 )
1767 .max(self.trailing.as_widget().mouse_interaction(
1768 &tree.children[1],
1769 trailing,
1770 cursor,
1771 viewport,
1772 renderer,
1773 ));
1774 if !memory.overflowing {
1775 return interaction;
1776 }
1777 interaction.max(self.overflow.as_widget().mouse_interaction(
1778 &tree.children[2],
1779 overflow,
1780 cursor,
1781 viewport,
1782 renderer,
1783 ))
1784 }
1785
1786 fn overlay<'b>(
1787 &'b mut self,
1788 tree: &'b mut Tree,
1789 layout: Layout<'b>,
1790 renderer: &Renderer,
1791 viewport: &Rectangle,
1792 translation: Vector,
1793 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1794 let memory = tree.state.downcast_ref::<Memory>();
1795 let clip = memory.viewport(layout.bounds());
1796 let shift = memory.shift();
1797 let overflowing = memory.overflowing;
1798 let mut children = layout.children();
1799 let (Some(content), Some(trailing), Some(overflow)) =
1800 (children.next(), children.next(), children.next())
1801 else {
1802 return None;
1803 };
1804 let [content_tree, trailing_tree, overflow_tree] = tree.children.as_mut_slice() else {
1805 return None;
1806 };
1807
1808 let mut overlays = Vec::new();
1809 overlays.extend(self.content.as_widget_mut().overlay(
1810 content_tree,
1811 content,
1812 renderer,
1813 &(clip + shift),
1814 translation - shift,
1815 ));
1816 overlays.extend(self.trailing.as_widget_mut().overlay(
1817 trailing_tree,
1818 trailing,
1819 renderer,
1820 viewport,
1821 translation,
1822 ));
1823 if overflowing {
1824 overlays.extend(self.overflow.as_widget_mut().overlay(
1825 overflow_tree,
1826 overflow,
1827 renderer,
1828 viewport,
1829 translation,
1830 ));
1831 }
1832 match overlays.len() {
1833 0 => None,
1834 1 => overlays.pop(),
1835 _ => Some(overlay::Group::with_children(overlays).overlay()),
1836 }
1837 }
1838}
1839
1840fn fill(renderer: &mut Renderer, bounds: Rectangle, colour: Color) {
1841 if bounds.width <= 0.0 || bounds.height <= 0.0 {
1842 return;
1843 }
1844 renderer.fill_quad(
1845 renderer::Quad {
1846 bounds,
1847 snap: true,
1848 ..renderer::Quad::default()
1849 },
1850 Background::Color(colour),
1851 );
1852}
1853
1854impl<Message> Strip<'_, Message> {
1855 fn draw_marks(
1858 &self,
1859 renderer: &mut Renderer,
1860 memory: &Memory,
1861 tabs: &[Rectangle],
1862 colours: StripStyle,
1863 bounds: Rectangle,
1864 ) {
1865 let top = bounds.y + (bounds.height - DIVIDER_HEIGHT) / 2.0;
1866 for (index, pair) in tabs.windows(2).enumerate() {
1867 let touches_active = self
1868 .active
1869 .is_some_and(|active| active == index || active == index + 1);
1870 if touches_active {
1871 continue;
1872 }
1873 let x = pair[1].x.round();
1874 fill(
1875 renderer,
1876 Rectangle::new(Point::new(x, top), Size::new(1.0, DIVIDER_HEIGHT)),
1877 colours.divider,
1878 );
1879 }
1880
1881 let Some(gap) = memory
1882 .press
1883 .filter(|press| press.dragging)
1884 .and_then(|press| {
1885 let gap = press.gap?;
1886 drop_index(press.index, gap).map(|_| gap)
1887 })
1888 else {
1889 return;
1890 };
1891 let x = match (tabs.get(gap), tabs.last()) {
1892 (Some(tab), _) => tab.x,
1893 (None, Some(last)) => last.x + last.width,
1894 (None, None) => return,
1895 };
1896 let x = (x - INDICATOR_WIDTH / 2.0)
1897 .max(bounds.x)
1898 .min(bounds.x + memory.content_width - INDICATOR_WIDTH);
1899 fill(
1900 renderer,
1901 Rectangle::new(
1902 Point::new(x, bounds.y),
1903 Size::new(INDICATOR_WIDTH, bounds.height),
1904 ),
1905 colours.indicator,
1906 );
1907 }
1908
1909 fn drag(
1911 &self,
1912 tree: &mut Tree,
1913 event: &Input,
1914 cursor: mouse::Cursor,
1915 content_x: f32,
1916 spans: &[(f32, f32)],
1917 shell: &mut Shell<'_, Message>,
1918 ) -> bool {
1919 let memory = tree.state.downcast_mut::<Memory>();
1920 let Some(mut press) = memory.press else {
1921 return false;
1922 };
1923 match event {
1924 Input::Mouse(mouse::Event::CursorMoved { .. }) => {
1925 let Some(position) = cursor.position() else {
1926 return false;
1927 };
1928 if !press.dragging {
1929 let far = press.origin.distance(position) > DRAG_THRESHOLD;
1930 if !far || self.on_gesture.is_none() {
1931 return false;
1932 }
1933 press.dragging = true;
1934 }
1935 let x = position.x + memory.offset - content_x;
1936 let gap = gap_at(spans, x);
1937 press.gap = Some(clamp_gap(press.index, gap, self.pinned, self.count));
1938 memory.press = Some(press);
1939 shell.request_redraw();
1940 shell.capture_event();
1941 true
1942 }
1943 Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
1944 memory.press = None;
1945 if !press.dragging {
1946 return false;
1947 }
1948 shell.request_redraw();
1949 shell.capture_event();
1950 let Some(to) = press.gap.and_then(|gap| drop_index(press.index, gap)) else {
1951 return true;
1952 };
1953 self.publish(
1954 shell,
1955 Gesture::Drop {
1956 from: press.index,
1957 to,
1958 },
1959 );
1960 true
1961 }
1962 _ => false,
1963 }
1964 }
1965
1966 #[allow(clippy::too_many_arguments)]
1969 fn pointer(
1970 &self,
1971 memory: &mut Memory,
1972 event: &Input,
1973 cursor: mouse::Cursor,
1974 bounds: Rectangle,
1975 clip: Rectangle,
1976 content_x: f32,
1977 spans: &[(f32, f32)],
1978 shell: &mut Shell<'_, Message>,
1979 ) {
1980 let tab_at = |memory: &Memory, cursor: mouse::Cursor| {
1981 let position = cursor.position_over(clip)?;
1982 let x = position.x + memory.offset - content_x;
1983 spans
1984 .iter()
1985 .position(|(left, width)| x >= *left && x < left + width)
1986 };
1987 match event {
1988 Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
1989 let Some(position) = cursor.position_over(bounds) else {
1990 return;
1991 };
1992 if let Some(index) = tab_at(memory, cursor) {
1993 memory.press = Some(Press {
1994 index,
1995 origin: position,
1996 dragging: false,
1997 gap: None,
1998 });
1999 self.publish(shell, Gesture::Press(index));
2000 shell.capture_event();
2001 return;
2002 }
2003 let click = mouse::Click::new(position, mouse::Button::Left, memory.last_click);
2004 memory.last_click = Some(click);
2005 if click.kind() == click::Kind::Double {
2006 self.publish(shell, Gesture::DoubleClick);
2007 shell.capture_event();
2008 }
2009 }
2010 Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
2011 if let Some(index) = tab_at(memory, cursor) {
2012 self.publish(shell, Gesture::Middle(index));
2013 shell.capture_event();
2014 }
2015 }
2016 Input::Mouse(mouse::Event::WheelScrolled { delta }) => {
2017 if !cursor.is_over(bounds) {
2018 return;
2019 }
2020 let step = match *delta {
2021 mouse::ScrollDelta::Lines { x, y } => {
2022 (if x != 0.0 { x } else { y }) * WHEEL_LINE
2023 }
2024 mouse::ScrollDelta::Pixels { x, y } => {
2025 if x.abs() > y.abs() {
2026 x
2027 } else {
2028 y
2029 }
2030 }
2031 };
2032 if memory.scroll_by(-step) {
2033 shell.request_redraw();
2034 shell.capture_event();
2035 }
2036 }
2037 Input::Touch(touch::Event::FingerPressed { id, position }) => {
2038 if !bounds.contains(*position) {
2039 return;
2040 }
2041 memory.touch = Some((*id, position.x));
2042 if let Some(index) = tab_at(memory, cursor) {
2043 self.publish(shell, Gesture::Press(index));
2044 shell.capture_event();
2045 }
2046 }
2047 Input::Touch(touch::Event::FingerMoved { id, position }) => {
2048 let Some((finger, last)) = memory.touch.filter(|(finger, _)| finger == id) else {
2049 return;
2050 };
2051 memory.touch = Some((finger, position.x));
2052 if memory.scroll_by(last - position.x) {
2053 shell.request_redraw();
2054 shell.capture_event();
2055 }
2056 }
2057 Input::Touch(
2058 touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. },
2059 ) if memory.touch.is_some_and(|(finger, _)| finger == *id) => {
2060 memory.touch = None;
2061 }
2062 _ => {}
2063 }
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::*;
2070 use crate::theme::{dark, light};
2071 use iced::keyboard::{Key, Modifiers};
2072
2073 fn state() -> State<u8> {
2074 State::new([
2075 document(1, "main.rs"),
2076 document(2, "lib.rs").modified(true),
2077 document(3, "README.md").pinned(true),
2078 document(4, "Cargo.toml"),
2079 ])
2080 }
2081
2082 fn ids(state: &State<u8>) -> Vec<u8> {
2083 state
2084 .documents()
2085 .iter()
2086 .map(|document| document.id)
2087 .collect()
2088 }
2089
2090 #[test]
2091 fn new_puts_pinned_documents_first_and_activates_the_first() {
2092 let state = state();
2093 assert_eq!(ids(&state), [3, 1, 2, 4]);
2094 assert_eq!(state.active(), Some(3));
2095 assert_eq!(state.recent(), [3, 1, 2, 4]);
2096 assert_eq!(state.pinned_count(), 1);
2097 assert!(state.is_active(3));
2098 assert!(!state.menu().is_open());
2099 assert!(!state.overflow().is_open());
2100 }
2101
2102 #[test]
2103 fn empty_state_has_nothing_to_do() {
2104 let mut state: State<u8> = State::new([]);
2105 assert_eq!(state.active(), None);
2106 assert_eq!(state.update(Event::Next), None);
2107 assert_eq!(state.update(Event::RecentNext), None);
2108 assert_eq!(state.update(Event::CloseAll), None);
2109 assert_eq!(Action::Close.event(&state), None);
2110 assert_eq!(Action::LastTab.event(&state), None);
2111 }
2112
2113 #[test]
2114 fn activate_reports_changes_only() {
2115 let mut state = state();
2116 assert_eq!(state.update(Event::Activate(2)), Some(Output::Activated(2)));
2117 assert_eq!(state.update(Event::Activate(2)), None);
2118 assert_eq!(state.update(Event::Activate(9)), None);
2119 assert_eq!(state.recent()[0], 2);
2120 assert_eq!(state.clone().with_active(4).active(), Some(4));
2121 }
2122
2123 #[test]
2124 fn next_and_previous_wrap_in_strip_order() {
2125 let mut state = state().with_active(4);
2126 assert_eq!(state.update(Event::Next), Some(Output::Activated(3)));
2127 assert_eq!(state.update(Event::Previous), Some(Output::Activated(4)));
2128 assert_eq!(state.update(Event::Previous), Some(Output::Activated(2)));
2129 }
2130
2131 #[test]
2132 fn recent_walks_until_the_cycle_ends() {
2133 let mut state = state();
2134 for id in [1, 2, 4] {
2135 let _ = state.update(Event::Activate(id));
2136 }
2137 assert_eq!(state.recent(), [4, 2, 1, 3]);
2138 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(2)));
2139 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(1)));
2140 assert_eq!(state.recent(), [4, 2, 1, 3], "the order holds mid-walk");
2141 assert_eq!(state.update(Event::EndCycle), None);
2142 assert_eq!(state.recent(), [1, 4, 2, 3]);
2143 assert_eq!(state.update(Event::RecentNext), Some(Output::Activated(4)));
2144 assert_eq!(state.update(Event::from(CycleEnd)), None);
2145 assert_eq!(state.recent(), [4, 1, 2, 3]);
2146 }
2147
2148 #[test]
2149 fn recent_previous_starts_from_the_oldest_and_other_events_end_the_walk() {
2150 let mut state = state();
2151 assert_eq!(
2152 state.update(Event::RecentPrevious),
2153 Some(Output::Activated(4))
2154 );
2155 assert_eq!(state.update(Event::Activate(2)), Some(Output::Activated(2)));
2156 assert_eq!(state.recent(), [2, 4, 3, 1]);
2157 let mut single = State::new([document(1, "a")]);
2158 assert_eq!(single.update(Event::RecentNext), None);
2159 }
2160
2161 #[test]
2162 fn close_events_request_without_closing() {
2163 let mut state = state();
2164 assert_eq!(
2165 state.update(Event::Close(3)),
2166 Some(Output::CloseRequested(vec![3]))
2167 );
2168 assert_eq!(state.update(Event::Close(9)), None);
2169 assert_eq!(
2170 state.update(Event::CloseOthers(1)),
2171 Some(Output::CloseRequested(vec![2, 4]))
2172 );
2173 assert_eq!(
2174 state.update(Event::CloseToTheRight(1)),
2175 Some(Output::CloseRequested(vec![2, 4]))
2176 );
2177 assert_eq!(state.update(Event::CloseToTheRight(4)), None);
2178 assert_eq!(
2179 state.update(Event::CloseSaved),
2180 Some(Output::CloseRequested(vec![1, 4]))
2181 );
2182 assert_eq!(
2183 state.update(Event::CloseAll),
2184 Some(Output::CloseRequested(vec![1, 2, 4]))
2185 );
2186 assert_eq!(state.documents().len(), 4);
2187 }
2188
2189 #[test]
2190 fn closing_the_active_document_activates_the_most_recent() {
2191 let mut state = state();
2192 let _ = state.update(Event::Activate(4));
2193 let _ = state.update(Event::Activate(1));
2194 assert_eq!(state.close(&[1]), Some(4));
2195 assert_eq!(ids(&state), [3, 2, 4]);
2196 assert_eq!(state.close(&[2]), None, "the active one stays");
2197 assert_eq!(state.active(), Some(4));
2198 assert_eq!(state.close(&[3, 4]), None);
2199 assert_eq!(state.active(), None);
2200 assert!(state.recent().is_empty());
2201 }
2202
2203 #[test]
2204 fn open_adds_to_the_end_of_its_group_or_activates() {
2205 let mut state = state();
2206 state.open(document(5, "new.rs"));
2207 assert_eq!(ids(&state), [3, 1, 2, 4, 5]);
2208 assert_eq!(state.active(), Some(5));
2209 state.open(document(6, "notes.md").pinned(true));
2210 assert_eq!(ids(&state), [3, 6, 1, 2, 4, 5]);
2211 state.open(document(1, "ignored"));
2212 assert_eq!(state.active(), Some(1));
2213 assert_eq!(state.document(1).map(|d| d.label.as_str()), Some("main.rs"));
2214 assert_eq!(state.recent()[..3], [1, 6, 5]);
2215 }
2216
2217 #[test]
2218 fn modified_and_label_change_in_place() {
2219 let mut state = state();
2220 state.set_modified(1, true);
2221 state.set_label(1, "main2.rs");
2222 let main = state.document(1).cloned();
2223 assert_eq!(
2224 main,
2225 Some(document(1, "main2.rs").modified(true)),
2226 "{main:?}"
2227 );
2228 state.set_modified(9, true);
2229 }
2230
2231 #[test]
2232 fn moves_stay_within_the_group() {
2233 let mut state = state();
2234 assert_eq!(
2235 state.update(Event::Move(1, 3)),
2236 Some(Output::Moved {
2237 id: 1,
2238 from: 1,
2239 to: 3
2240 })
2241 );
2242 assert_eq!(ids(&state), [3, 2, 4, 1]);
2243 assert_eq!(
2244 state.update(Event::Move(1, 0)),
2245 Some(Output::Moved {
2246 id: 1,
2247 from: 3,
2248 to: 1
2249 })
2250 );
2251 assert_eq!(state.update(Event::Move(1, 1)), None);
2252 assert_eq!(state.update(Event::Move(3, 5)), None, "pinned stays first");
2253 assert_eq!(state.update(Event::Move(4, 99)), None, "already last");
2254 }
2255
2256 #[test]
2257 fn pinning_moves_to_the_group_boundary() {
2258 let mut state = state();
2259 assert_eq!(
2260 state.update(Event::Pin(4, true)),
2261 Some(Output::Pinned(4, true))
2262 );
2263 assert_eq!(ids(&state), [3, 4, 1, 2]);
2264 assert_eq!(state.update(Event::Pin(4, true)), None);
2265 assert_eq!(
2266 state.update(Event::Pin(3, false)),
2267 Some(Output::Pinned(3, false))
2268 );
2269 assert_eq!(ids(&state), [4, 3, 1, 2]);
2270 assert_eq!(state.pinned_count(), 1);
2271 }
2272
2273 fn items(state: &State<u8>) -> Vec<(MenuItem, bool)> {
2274 state
2275 .menu()
2276 .menu()
2277 .entries()
2278 .iter()
2279 .filter_map(|entry| entry.as_item())
2280 .map(|item| (item.id, item.disabled))
2281 .collect()
2282 }
2283
2284 #[test]
2285 fn the_context_menu_is_built_for_the_tab_it_opens_on() {
2286 let mut state = state().with_menu_items([item(MenuItem::Custom("copy-path"), "Copy path")]);
2287 let _ = state.update(Event::Menu(context_menu::Event::Open(4, Point::ORIGIN)));
2288 assert!(state.menu().is_open_on(&4));
2289 assert_eq!(
2290 items(&state),
2291 [
2292 (MenuItem::Close, false),
2293 (MenuItem::CloseOthers, false),
2294 (MenuItem::CloseToTheRight, true),
2295 (MenuItem::CloseSaved, false),
2296 (MenuItem::CloseAll, false),
2297 (MenuItem::Pin, false),
2298 (MenuItem::Custom("copy-path"), false),
2299 ]
2300 );
2301 let _ = state.update(Event::Menu(context_menu::Event::OpenFromKeyboard(3)));
2302 assert!(items(&state).contains(&(MenuItem::Unpin, false)));
2303 assert_eq!(
2304 state.update(Event::Menu(context_menu::Event::Open(9, Point::ORIGIN))),
2305 None
2306 );
2307 }
2308
2309 #[test]
2310 fn menu_items_map_to_outputs() {
2311 let choose = |target: u8, item: MenuItem| {
2312 let mut state =
2313 state().with_menu_items([dropdown_menu::item(MenuItem::Custom("copy"), "Copy")]);
2314 let _ = state.update(Event::Menu(context_menu::Event::OpenFromKeyboard(target)));
2315 let output = state.update(Event::Menu(context_menu::Event::Menu(
2316 dropdown_menu::Event::Activate(item),
2317 )));
2318 assert!(!state.menu().is_open());
2319 (output, ids(&state))
2320 };
2321 assert_eq!(
2322 choose(1, MenuItem::Close).0,
2323 Some(Output::CloseRequested(vec![1]))
2324 );
2325 assert_eq!(
2326 choose(2, MenuItem::CloseOthers).0,
2327 Some(Output::CloseRequested(vec![1, 4]))
2328 );
2329 assert_eq!(
2330 choose(1, MenuItem::CloseToTheRight).0,
2331 Some(Output::CloseRequested(vec![2, 4]))
2332 );
2333 assert_eq!(
2334 choose(1, MenuItem::CloseSaved).0,
2335 Some(Output::CloseRequested(vec![1, 4]))
2336 );
2337 assert_eq!(
2338 choose(1, MenuItem::CloseAll).0,
2339 Some(Output::CloseRequested(vec![1, 2, 4]))
2340 );
2341 assert_eq!(
2342 choose(4, MenuItem::Pin),
2343 (Some(Output::Pinned(4, true)), vec![3, 4, 1, 2])
2344 );
2345 assert_eq!(choose(3, MenuItem::Unpin).0, Some(Output::Pinned(3, false)));
2346 assert_eq!(
2347 choose(2, MenuItem::Custom("copy")).0,
2348 Some(Output::Custom("copy", 2))
2349 );
2350 }
2351
2352 #[test]
2353 fn the_overflow_menu_lists_every_document_and_activates_one() {
2354 let mut state = state();
2355 let _ = state.update(Event::Overflow(dropdown_menu::Event::Toggle));
2356 assert!(state.overflow().is_open());
2357 assert_eq!(state.overflow().entries().len(), 4);
2358 assert!(state.overflow().is_checked(3));
2359 let lib = state.overflow().item(2).cloned();
2360 assert_eq!(lib.and_then(|item| item.shortcut), Some("Modified".into()));
2361 assert_eq!(
2362 state.update(Event::Overflow(dropdown_menu::Event::Activate(4))),
2363 Some(Output::Activated(4))
2364 );
2365 assert!(!state.overflow().is_open());
2366 assert_eq!(state.update(Event::New), Some(Output::NewTab));
2367 }
2368
2369 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2370 let chord: Chord = chord.parse().unwrap();
2371 keymap.resolve(chord.key(), chord.modifiers())
2372 }
2373
2374 #[test]
2375 fn default_keymap_covers_every_action() {
2376 let keymap = default_keymap();
2377 assert_eq!(press(&keymap, "Ctrl+Tab"), Some(Action::RecentNext));
2378 assert_eq!(
2379 press(&keymap, "Ctrl+Shift+Tab"),
2380 Some(Action::RecentPrevious)
2381 );
2382 assert_eq!(press(&keymap, "Ctrl+PageDown"), Some(Action::Next));
2383 assert_eq!(press(&keymap, "Ctrl+PageUp"), Some(Action::Previous));
2384 assert_eq!(press(&keymap, "Ctrl+Shift+PageUp"), Some(Action::MoveLeft));
2385 assert_eq!(
2386 press(&keymap, "Ctrl+Shift+PageDown"),
2387 Some(Action::MoveRight)
2388 );
2389 assert_eq!(press(&keymap, "Mod+W"), Some(Action::Close));
2390 assert_eq!(press(&keymap, "Mod+N"), Some(Action::New));
2391 assert_eq!(press(&keymap, "Mod+1"), Some(Action::Tab(1)));
2392 assert_eq!(press(&keymap, "Mod+8"), Some(Action::Tab(8)));
2393 assert_eq!(press(&keymap, "Mod+9"), Some(Action::LastTab));
2394 assert_eq!(press(&keymap, "Tab"), None);
2395 assert_eq!(press(&keymap, "PageDown"), None);
2396 for action in <Action as keys::Action>::ALL {
2397 let unbound = *action == Action::OpenMenu;
2398 assert_eq!(keymap.chords(action).is_empty(), unbound, "{action:?}");
2399 assert!(keys::Action::description(*action).ends_with('.'));
2400 }
2401 assert_eq!(Keymap::<Action>::defaults(), keymap);
2402 }
2403
2404 #[test]
2405 fn opening_the_tab_menu_is_left_to_context_menus_by_default() {
2406 let keymap = default_keymap();
2407 for chord in ["Shift+F10", "Menu"] {
2408 assert_eq!(press(&keymap, chord), None, "{chord}");
2409 let context = context_menu::default_keymap();
2410 assert_eq!(
2411 press_menu(&context, chord),
2412 Some(context_menu::Action::Open)
2413 );
2414 }
2415 let bound = keymap.bind("Shift+F10".parse().unwrap(), Action::OpenMenu);
2416 assert_eq!(press(&bound, "Shift+F10"), Some(Action::OpenMenu));
2417 }
2418
2419 fn press_menu(
2420 keymap: &Keymap<context_menu::Action>,
2421 chord: &str,
2422 ) -> Option<context_menu::Action> {
2423 let chord: Chord = chord.parse().unwrap();
2424 keymap.resolve(chord.key(), chord.modifiers())
2425 }
2426
2427 #[test]
2428 fn default_keymap_leaves_plain_and_alt_keys_to_other_components() {
2429 let keymap = default_keymap();
2430 for chord in ["ArrowLeft", "ArrowRight", "Home", "End", "Alt+1", "Escape"] {
2431 assert_eq!(press(&keymap, chord), None, "{chord}");
2432 }
2433 }
2434
2435 #[test]
2436 fn keymap_overrides_and_unbinding() {
2437 let keymap = default_keymap()
2438 .unbind(&"Ctrl+Tab".parse().unwrap())
2439 .bind("Alt+PageDown".parse().unwrap(), Action::Next);
2440 assert_eq!(press(&keymap, "Ctrl+Tab"), None);
2441 assert_eq!(press(&keymap, "Alt+PageDown"), Some(Action::Next));
2442 assert_eq!(press(&keymap, "Ctrl+PageDown"), Some(Action::Next));
2443 let event = keys::Event {
2444 key: Key::Character("w".into()),
2445 modifiers: Modifiers::COMMAND,
2446 };
2447 assert_eq!(keymap.resolve_event(&event), Some(Action::Close));
2448 }
2449
2450 #[test]
2451 fn actions_map_to_events() {
2452 let state = state().with_active(1);
2453 assert_eq!(Action::RecentNext.event(&state), Some(Event::RecentNext));
2454 assert_eq!(
2455 Action::RecentPrevious.event(&state),
2456 Some(Event::RecentPrevious)
2457 );
2458 assert_eq!(Action::Next.event(&state), Some(Event::Next));
2459 assert_eq!(Action::Previous.event(&state), Some(Event::Previous));
2460 assert_eq!(Action::MoveLeft.event(&state), Some(Event::Move(1, 0)));
2461 assert_eq!(Action::MoveRight.event(&state), Some(Event::Move(1, 2)));
2462 assert_eq!(Action::Close.event(&state), Some(Event::Close(1)));
2463 assert_eq!(Action::New.event(&state), Some(Event::New));
2464 assert_eq!(Action::Tab(1).event(&state), Some(Event::Activate(3)));
2465 assert_eq!(Action::Tab(4).event(&state), Some(Event::Activate(4)));
2466 assert_eq!(Action::Tab(5).event(&state), None);
2467 assert_eq!(Action::Tab(0).event(&state), None);
2468 assert_eq!(Action::LastTab.event(&state), Some(Event::Activate(4)));
2469 assert_eq!(
2470 Action::OpenMenu.event(&state),
2471 Some(Event::Menu(context_menu::Event::OpenFromKeyboard(1)))
2472 );
2473 let first = state.clone().with_active(3);
2474 assert_eq!(Action::MoveLeft.event(&first), None);
2475
2476 let mut state = state;
2477 let event = Action::MoveRight.event(&state).unwrap();
2478 assert!(state.update(event).is_some());
2479 assert_eq!(ids(&state), [3, 2, 1, 4]);
2480 }
2481
2482 #[test]
2483 fn gaps_follow_tab_centres() {
2484 let tabs = [(0.0, 100.0), (100.0, 60.0), (160.0, 80.0)];
2485 assert_eq!(gap_at(&tabs, -5.0), 0);
2486 assert_eq!(gap_at(&tabs, 49.0), 0);
2487 assert_eq!(gap_at(&tabs, 51.0), 1);
2488 assert_eq!(gap_at(&tabs, 131.0), 2);
2489 assert_eq!(gap_at(&tabs, 500.0), 3);
2490 }
2491
2492 #[test]
2493 fn gaps_are_kept_within_the_group() {
2494 assert_eq!(clamp_gap(0, 4, 2, 5), 2);
2495 assert_eq!(clamp_gap(3, 0, 2, 5), 2);
2496 assert_eq!(clamp_gap(3, 5, 2, 5), 5);
2497 assert_eq!(clamp_gap(1, 0, 0, 3), 0);
2498 }
2499
2500 #[test]
2501 fn dropping_next_to_itself_does_not_move() {
2502 assert_eq!(drop_index(2, 2), None);
2503 assert_eq!(drop_index(2, 3), None);
2504 assert_eq!(drop_index(2, 0), Some(0));
2505 assert_eq!(drop_index(0, 3), Some(2));
2506 }
2507
2508 #[test]
2509 fn memory_reveals_and_clamps() {
2510 let mut memory = Memory {
2511 content_width: 600.0,
2512 viewport_width: 200.0,
2513 ..Memory::default()
2514 };
2515 memory.reveal(450.0, 550.0);
2516 assert_eq!(memory.offset, 350.0);
2517 memory.reveal(100.0, 180.0);
2518 assert_eq!(memory.offset, 100.0);
2519 assert!(memory.scroll_by(1000.0));
2520 assert_eq!(memory.offset, 400.0);
2521 assert!(!memory.scroll_by(10.0));
2522 assert!(memory.scroll_by(-1000.0));
2523 assert_eq!(memory.offset, 0.0);
2524 }
2525
2526 fn offset_after(widths: &[f32]) -> f32 {
2529 let mut memory = Memory::default();
2530 for width in widths {
2531 memory.follow((Some(4), 6), (600.0, *width), Some((450.0, 100.0)));
2532 }
2533 memory.offset
2534 }
2535
2536 #[test]
2537 fn the_first_offset_does_not_depend_on_earlier_sizes() {
2538 assert_eq!(offset_after(&[300.0]), 250.0, "active tab ends at the edge");
2539 assert_eq!(offset_after(&[1000.0, 300.0]), 250.0);
2540 assert_eq!(offset_after(&[0.0, 300.0]), 250.0);
2541 assert_eq!(offset_after(&[200.0, 300.0]), 250.0);
2542 assert_eq!(offset_after(&[200.0, 300.0, 300.0]), 250.0);
2543 }
2544
2545 #[test]
2546 fn a_new_active_tab_scrolls_the_least_distance_and_keeps_wheel_scrolling() {
2547 let mut memory = Memory::default();
2548 memory.follow((Some(4), 6), (600.0, 300.0), Some((450.0, 100.0)));
2549 memory.follow((Some(3), 6), (600.0, 300.0), Some((350.0, 100.0)));
2550 assert_eq!(memory.offset, 250.0, "already in view");
2551 memory.follow((Some(0), 6), (600.0, 300.0), Some((0.0, 100.0)));
2552 assert_eq!(memory.offset, 0.0);
2553 let _ = memory.scroll_by(120.0);
2554 memory.follow((Some(0), 6), (600.0, 300.0), Some((0.0, 100.0)));
2555 assert_eq!(memory.offset, 120.0, "a relayout leaves the wheel offset");
2556 }
2557
2558 #[test]
2559 fn pointer_opens_place_the_menu_below_the_tab() {
2560 let mut state = state();
2561 let _ = state.update(Event::Menu(context_menu::Event::Open(
2562 1,
2563 Point::new(40.0, 12.0),
2564 )));
2565 assert!(state.menu().is_open_on(&1));
2566 assert_eq!(
2567 state.menu().position(),
2568 Some(Point::new(0.0, HEIGHT + KEYBOARD_GAP))
2569 );
2570 assert_eq!(state.menu().menu().highlighted(), None);
2571 }
2572
2573 #[test]
2574 fn active_tab_joins_the_page_and_others_sit_on_the_strip() {
2575 for theme in [light(), dark()] {
2576 let tokens = Tokens::of(&theme);
2577 let active = tab_style(&tokens, true, false);
2578 let idle = tab_style(&tokens, false, false);
2579 let hovered = tab_style(&tokens, false, true);
2580 assert_eq!(
2581 active.background,
2582 Some(Background::Color(tokens.background))
2583 );
2584 assert_eq!(idle.background, None);
2585 assert!(hovered.background.is_some());
2586 assert_ne!(hovered.background, active.background);
2587 assert_eq!(active.text_color, Some(tokens.foreground));
2588 assert_eq!(idle.text_color, Some(tokens.muted_foreground));
2589 assert_eq!(hovered.text_color, Some(tokens.foreground));
2590 assert_eq!(active.border.radius.bottom_left, 0.0);
2591 assert_eq!(active.border.radius.top_left, radius::MD);
2592 assert_eq!(strip_style(&tokens).background, tokens.muted);
2593 assert_ne!(strip_style(&tokens).background, tokens.background);
2594 }
2595 }
2596
2597 #[test]
2598 fn disabled_labels_fade() {
2599 let tokens = Tokens::of(&light());
2600 let on = label_colour(&tokens, true, false, true);
2601 let off = label_colour(&tokens, true, false, false);
2602 assert!(off.a < on.a);
2603 }
2604
2605 #[test]
2606 fn close_button_shows_a_surface_on_hover_only() {
2607 for theme in [light(), dark()] {
2608 let tokens = Tokens::of(&theme);
2609 assert!(close_style(&tokens, Status::Active).background.is_none());
2610 assert!(close_style(&tokens, Status::Disabled).background.is_none());
2611 assert!(close_style(&tokens, Status::Hovered).background.is_some());
2612 assert!(close_style(&tokens, Status::Pressed).background.is_some());
2613 }
2614 }
2615
2616 #[test]
2617 fn modified_dot_is_stronger_on_the_active_tab() {
2618 let tokens = Tokens::of(&dark());
2619 assert_eq!(
2620 dot_style(&tokens, true).background,
2621 Some(Background::Color(tokens.foreground))
2622 );
2623 assert_eq!(
2624 dot_style(&tokens, false).background,
2625 Some(Background::Color(tokens.muted_foreground))
2626 );
2627 }
2628
2629 #[test]
2630 fn slots_show_close_dot_or_space() {
2631 let look = |active, hovered| Look {
2632 active,
2633 hovered,
2634 enabled: true,
2635 };
2636 let saved = document(1, "a");
2637 let modified = document(1, "a").modified(true);
2638 let pinned = document(1, "a").pinned(true);
2639 let has_slot =
2640 |document: &Document<u8>, look| slot::<u8, ()>(document, look, None).is_some();
2641 assert!(has_slot(&saved, look(false, false)));
2642 assert!(has_slot(&modified, look(true, false)));
2643 assert!(!has_slot(&pinned, look(true, true)));
2644 assert!(has_slot(&pinned.clone().modified(true), look(false, false)));
2645 }
2646
2647 #[test]
2648 fn builder_defaults() {
2649 let state = state();
2650 let tabs: DocumentTabs<'_, u8, ()> = document_tabs(&state);
2651 assert!(!tabs.new_tab);
2652 assert_eq!(tabs.max_label_width, MAX_LABEL_WIDTH);
2653 assert_eq!(tabs.context_menu_keymap, context_menu::default_keymap());
2654 assert!(!tabs.is_enabled());
2655 let tabs: DocumentTabs<'_, u8, ()> = document_tabs(&state)
2656 .new_tab(true)
2657 .max_label_width(120.0)
2658 .context_menu_keymap(Keymap::new())
2659 .overflow_keymap(Keymap::new())
2660 .on_event(|_| ());
2661 assert!(tabs.new_tab);
2662 assert_eq!(tabs.max_label_width, 120.0);
2663 assert!(tabs.context_menu_keymap.is_empty());
2664 assert!(tabs.overflow_keymap.is_empty());
2665 assert!(tabs.is_enabled());
2666 }
2667}