Skip to main content

iced_cube/navigation/
document_tabs.rs

1//! A strip of open documents above an editor, one tab per document.
2//!
3//! [`State`] owns the documents in order, the active one and the order they
4//! were last used in. Its [`update`](State::update) activates, reorders and
5//! pins tabs, and turns close gestures into [`Output::CloseRequested`] so
6//! the app decides what closing means, such as asking to save a modified
7//! document first, before it calls [`State::close`].
8//!
9//! Each tab shows an optional icon, a label that ends in an ellipsis past
10//! its maximum width, and a slot that holds the close button on the active
11//! and hovered tab, or a dot while the document has unsaved changes. The
12//! slot is always reserved, so labels do not move when it changes. Pinned
13//! tabs sit first, compact and without a close button.
14//!
15//! With the pointer, a press activates a tab, dragging moves it (a line
16//! marks where it will land), a middle click asks to close it and a right
17//! click opens its context menu. The wheel scrolls tabs that do not fit,
18//! and a button at the end of the strip lists every tab. The strip keeps
19//! the active tab in view.
20//!
21//! Keyboard shortcuts resolve through a [`Keymap`] of [`Action`]s; see
22//! [`default_keymap`] and [`crate::keys`].
23
24use 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
56/// Height of the strip and its tabs.
57pub const HEIGHT: f32 = 36.0;
58
59/// Default widest a label grows before it ends in an ellipsis.
60pub const MAX_LABEL_WIDTH: f32 = 200.0;
61
62/// Widest label of a pinned tab without an icon.
63pub const PINNED_LABEL_WIDTH: f32 = 80.0;
64
65/// Widest the list of every tab grows before its labels end in an ellipsis.
66pub const OVERFLOW_WIDTH: f32 = 320.0;
67
68/// How far, in logical pixels, a pressed tab moves before it drags.
69pub const DRAG_THRESHOLD: f32 = 4.0;
70
71/// Side of the square slot for the close button or the modified dot.
72const 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;
79/// Pixels one wheel line scrolls the strip.
80const WHEEL_LINE: f32 = 40.0;
81
82/// One open document.
83#[derive(Debug, Clone, PartialEq)]
84pub struct Document<Id> {
85    pub id: Id,
86    pub label: String,
87    pub icon: Option<Glyph>,
88    /// Unsaved changes, shown as a dot in the tab's slot.
89    pub modified: bool,
90    pub pinned: bool,
91}
92
93/// Creates a saved, unpinned document.
94pub 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/// An item in a tab's context menu.
122#[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    /// An item the app added with [`State::with_menu_items`]. Choosing it
132    /// returns [`Output::Custom`] with the tab it was chosen on.
133    Custom(&'static str),
134}
135
136/// Changes to the documents.
137#[derive(Debug, Clone, Copy, PartialEq)]
138pub enum Event<Id> {
139    /// Makes a document the active one.
140    Activate(Id),
141    /// Activates the next document, wrapping at the end.
142    Next,
143    /// Activates the previous document, wrapping at the start.
144    Previous,
145    /// Activates the next document in most-recently-used order. Repeated
146    /// presses walk further down the list until [`Event::EndCycle`].
147    RecentNext,
148    /// Walks the most-recently-used list the other way.
149    RecentPrevious,
150    /// Ends a walk through the most-recently-used list, making the document
151    /// it stopped on the most recent. Any other event ends it too.
152    EndCycle,
153    /// Asks to close one document.
154    Close(Id),
155    /// Asks to close every unpinned document except this one.
156    CloseOthers(Id),
157    /// Asks to close the unpinned documents after this one.
158    CloseToTheRight(Id),
159    /// Asks to close every unpinned document without unsaved changes.
160    CloseSaved,
161    /// Asks to close every unpinned document.
162    CloseAll,
163    /// Moves a document to an index. Pinned documents stay before the
164    /// others, so the index is clamped to the document's group.
165    Move(Id, usize),
166    /// Pins or unpins a document. It moves to the end of the pinned tabs
167    /// or the start of the others.
168    Pin(Id, bool),
169    /// Asks for a new document, from the new tab button, a double click on
170    /// the empty strip or a shortcut.
171    New,
172    /// The context menu of a tab.
173    Menu(context_menu::Event<MenuItem, Id>),
174    /// The menu listing every document.
175    Overflow(dropdown_menu::Event<Id>),
176}
177
178/// Sent by [`cycle_end`] when every modifier key is released.
179#[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
188/// Reports when the last modifier key is released, which ends a walk
189/// through recent documents as editors do when Ctrl is let go.
190pub 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/// What an [`Event`] changed or asks the app to do.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum Output<Id> {
200    /// A different document became active.
201    Activated(Id),
202    /// The user asked to close these documents, in strip order. Nothing is
203    /// closed yet: the app saves or confirms as it needs to, then calls
204    /// [`State::close`].
205    CloseRequested(Vec<Id>),
206    /// A document moved from one index to another.
207    Moved { id: Id, from: usize, to: usize },
208    /// A document was pinned or unpinned.
209    Pinned(Id, bool),
210    /// The user asked for a new document.
211    NewTab,
212    /// An app menu item was chosen on a tab.
213    Custom(&'static str, Id),
214}
215
216/// The open documents, the active one and the order they were used in.
217#[derive(Debug, Clone, PartialEq)]
218pub struct State<Id> {
219    documents: Vec<Document<Id>>,
220    active: Option<Id>,
221    /// Every document id, most recently used first.
222    recent: Vec<Id>,
223    /// Where a walk through `recent` has got to.
224    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    /// Opens `documents`, pinned ones first, with the first one active.
232    /// Ids must be unique.
233    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    /// Starts on `id`, when it exists.
250    pub fn with_active(mut self, id: Id) -> Self {
251        let _ = self.activate(id);
252        self
253    }
254
255    /// Adds items to the end of every tab's context menu, such as "Copy
256    /// path". Give them [`MenuItem::Custom`] ids.
257    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    /// The documents in strip order.
263    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    /// Document ids, most recently used first.
280    pub fn recent(&self) -> &[Id] {
281        &self.recent
282    }
283
284    /// The tab context menu.
285    pub fn menu(&self) -> &context_menu::State<MenuItem, Id> {
286        &self.menu
287    }
288
289    /// The menu listing every document.
290    pub fn overflow(&self) -> &dropdown_menu::State<Id> {
291        &self.overflow
292    }
293
294    /// How many documents are pinned. They are always the first ones.
295    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    /// Activates `document`, adding it at the end of its group (pinned or
311    /// not) when it is not open yet.
312    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    /// Closes documents. When the active one closes, the most recently used
328    /// one left becomes active, and is returned.
329    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    /// Marks a document as having unsaved changes, or as saved.
343    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    /// Applies an event and returns what changed, if anything.
360    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    /// Moves `id` to the front of the recent list.
405    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            // The menu opens below the tab, not at the pointer, so it never
490            // covers the tab's label.
491            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    /// The context menu of the tab `target`, with the choices that would
519    /// do nothing disabled.
520    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/// What a document tabs keyboard shortcut does.
582#[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    /// Activates the document at this position, counting from 1.
593    Tab(u8),
594    /// Activates the last document.
595    LastTab,
596    /// Opens the active tab's context menu.
597    OpenMenu,
598}
599
600impl Action {
601    /// The [`Event`] this action sends to `state`, or `None` when it would
602    /// do nothing, such as moving the first tab left.
603    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
713/// The default document tabs shortcuts:
714///
715/// | Keys | Action |
716/// | --- | --- |
717/// | `Ctrl+Tab` | [`Action::RecentNext`] |
718/// | `Ctrl+Shift+Tab` | [`Action::RecentPrevious`] |
719/// | `Ctrl+PageDown` | [`Action::Next`] |
720/// | `Ctrl+PageUp` | [`Action::Previous`] |
721/// | `Ctrl+Shift+PageUp` | [`Action::MoveLeft`] |
722/// | `Ctrl+Shift+PageDown` | [`Action::MoveRight`] |
723/// | `Ctrl+W` | [`Action::Close`] |
724/// | `Ctrl+N` | [`Action::New`] |
725/// | `Ctrl+1` to `Ctrl+8` | [`Action::Tab`] |
726/// | `Ctrl+9` | [`Action::LastTab`] |
727///
728/// `Ctrl+Tab` walks the documents in most-recently-used order, and
729/// `Ctrl+PageDown` and `Ctrl+PageUp` in strip order. Ctrl+W, Ctrl+N and the
730/// digits use Cmd on macOS. Subscribe to [`cycle_end`] so a walk ends when
731/// Ctrl is released.
732///
733/// [`Action::OpenMenu`] has no default chord. The strip has no keyboard
734/// focus, so `Shift+F10` or `Menu` bound app-wide would also open any
735/// other context menu on screen. Bind it when the tab strip is the only
736/// one, or only while the editor has focus:
737///
738/// ```
739/// use iced::keyboard::key::Named;
740/// use iced_cube::keys::Chord;
741/// use iced_cube::navigation::document_tabs::{Action, default_keymap};
742///
743/// let keymap = default_keymap()
744///     .bind(Chord::named(Named::F10).shift(), Action::OpenMenu)
745///     .bind(Chord::named(Named::ContextMenu), Action::OpenMenu);
746/// ```
747///
748/// The segmented tabs also bind `Ctrl+Tab` and `Ctrl+Shift+Tab`. An app
749/// showing both should unbind them from one of them.
750pub 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
776/// A document tab strip builder. Convert it into an [`Element`] to render.
777pub 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
786/// Renders the tab strip of `state`. Without
787/// [`on_event`](DocumentTabs::on_event) every tab renders disabled.
788pub 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    /// Shows a button after the last tab, and lets a double click on the
811    /// empty strip ask for a new document. Both send [`Event::New`].
812    pub fn new_tab(mut self, new_tab: bool) -> Self {
813        self.new_tab = new_tab;
814        self
815    }
816
817    /// The widest a label grows before it ends in an ellipsis. Defaults to
818    /// [`MAX_LABEL_WIDTH`].
819    pub fn max_label_width(mut self, width: f32) -> Self {
820        self.max_label_width = width;
821        self
822    }
823
824    /// Replaces the default keymap of the open tab context menu.
825    pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
826        self.context_menu_keymap = keymap;
827        self
828    }
829
830    /// Replaces the default keymap of the open menu listing every tab.
831    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/// How one tab is drawn.
847#[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
1009/// The open list of every tab, one row per document with the active one
1010/// checked, scrolled to show the highlighted row or else the active one.
1011fn 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
1034/// One row of the list of every tab. The label ends in an ellipsis rather
1035/// than wrapping.
1036fn 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
1108/// The trailing slot of a tab: the close button on the active or hovered
1109/// tab, otherwise a dot while modified, otherwise empty space so the label
1110/// does not move. Pinned tabs only show the dot.
1111fn 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
1150/// The colour of a tab's label and icon.
1151pub 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
1160/// The surface of one tab. The active tab takes the page background, so it
1161/// joins the content below; the others sit flat on the strip.
1162pub 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
1186/// The close button inside a tab.
1187pub 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
1203/// The dot that marks unsaved changes.
1204pub 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/// Colours the strip draws itself.
1221#[derive(Debug, Clone, Copy, PartialEq)]
1222pub struct StripStyle {
1223    pub background: Color,
1224    /// The line along the bottom, broken under the active tab.
1225    pub line: Color,
1226    /// Short lines between inactive tabs.
1227    pub divider: Color,
1228    /// Where a dragged tab will land.
1229    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
1241/// The gap a pointer at `x` points to: the number of tabs whose centre lies
1242/// before it. `tabs` holds each tab's left edge and width, in order.
1243fn 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
1249/// The gap a tab dragged from `from` may drop into, kept within its group:
1250/// the first `pinned` tabs, or the rest of the `count`.
1251fn 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
1259/// The index a tab dragged from `from` ends up at when dropped into `gap`,
1260/// or `None` when it would not move.
1261fn 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/// Pointer gestures on the strip, by tab index.
1267#[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/// A press on a tab that may become a drag.
1276#[derive(Debug, Clone, Copy, PartialEq)]
1277struct Press {
1278    index: usize,
1279    origin: Point,
1280    dragging: bool,
1281    gap: Option<usize>,
1282}
1283
1284/// What the strip remembers between frames.
1285#[derive(Debug, Clone, Default)]
1286struct Memory {
1287    offset: f32,
1288    content_width: f32,
1289    viewport_width: f32,
1290    overflowing: bool,
1291    /// The active index and tab count the strip last scrolled into view.
1292    revealed: Option<(Option<usize>, usize)>,
1293    /// The content and viewport widths the strip last scrolled for.
1294    measured: Option<(f32, f32)>,
1295    press: Option<Press>,
1296    last_click: Option<mouse::Click>,
1297    /// A finger panning the strip: which one, and where it was last.
1298    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    /// Scrolls the least distance that shows a tab spanning `left` to
1332    /// `right` in full.
1333    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    /// Keeps the active tab, spanning `active`, in view after a layout.
1344    ///
1345    /// When the active tab or the tab count changes, the strip scrolls the
1346    /// least distance. When only the sizes change, such as on the first
1347    /// layout, a resize or once fonts load, the offset is worked out afresh
1348    /// from the start of the strip, so it never depends on earlier sizes.
1349    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    /// The cursor in the scrolled content's coordinates.
1381    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
1389/// The tab strip: tabs that scroll sideways, a new tab button after them
1390/// and, while they overflow, a menu button listing them at the end.
1391struct 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
1409/// Each tab's left edge and width, relative to the content.
1410fn 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        // Marks where tabs are cut off before the buttons.
1559        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        // The bottom line, broken under the active tab where it is visible.
1572        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    /// Dividers between inactive tabs and the drop indicator, in content
1856    /// coordinates.
1857    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    /// Follows a tab being dragged. Returns whether it used the event.
1910    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    /// Presses, double clicks, the wheel and touch panning, once no child
1967    /// has used the event.
1968    #[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    /// The offset after laying the strip out at each viewport width in
2527    /// turn, with the active tab at 450 to 550 of 600 pixels of tabs.
2528    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}