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