Skip to main content

densa_widgets/dock/
mod.rs

1//! Docking: panels in tab groups, side by side or stacked, rearranged by dragging tabs.
2
3mod access;
4mod geometry;
5mod input;
6mod layout;
7mod tree;
8
9use densa_core::{Color, Point, Rect, Size};
10use densa_draw::Canvas;
11use densa_ui::{
12    AccessNode, AccessRequest, Event, EventCx, Handled, LayoutCx, Length, Overflow, PaintCx,
13    PointerButton, PointerKind, Style, Ui, UiError, Widget, WidgetId,
14};
15
16pub use self::layout::{
17    DockAxis, DockLayout, DockLayoutError, DockLayoutErrorKind, MAX_LAYOUT_DEPTH,
18};
19
20use self::geometry::{bar, beside, content_style, divider_rect, group_rect, hairline};
21use crate::paint::{centered, outline};
22use crate::text::Text;
23
24/// Space before a title cut off by its shrunk tab, in logical pixels.
25const TITLE_INSET: f32 = 6.0;
26
27/// Where [`DockArea::add_panel`] puts a panel.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum DockAt {
31    /// As another tab of the first group (a new group if the dock is empty).
32    First,
33    /// As another tab of `panel`'s group.
34    With(WidgetId),
35    /// In a new group on `side` of `panel`'s group, taking half its space.
36    Beside(WidgetId, DockSide),
37}
38
39/// A side of a dock group.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum DockSide {
43    /// Left.
44    Left,
45    /// Right.
46    Right,
47    /// Above.
48    Top,
49    /// Below.
50    Bottom,
51}
52
53/// What a dock shows for a panel: its tab's title and, for saving and restoring workspaces,
54/// a key that stays the same across runs (widget ids do not, and titles need not be unique).
55///
56/// `&str` and `String` convert into a `DockPanel` with that title and no key.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct DockPanel {
60    /// The tab's title.
61    pub title: String,
62    /// The application's name for the panel, such as `"orders"`.
63    pub key: Option<String>,
64}
65
66impl DockPanel {
67    /// A panel titled `title`, without a key.
68    pub fn new(title: impl Into<String>) -> Self {
69        Self {
70            title: title.into(),
71            key: None,
72        }
73    }
74
75    /// The panel with `key`. Give each panel its own key: panels sharing one take each
76    /// other's places in turn when a layout is restored.
77    #[must_use]
78    pub fn with_key(mut self, key: impl Into<String>) -> Self {
79        self.key = Some(key.into());
80        self
81    }
82}
83
84impl From<&str> for DockPanel {
85    fn from(title: &str) -> Self {
86        Self::new(title)
87    }
88}
89
90impl From<String> for DockPanel {
91    fn from(title: String) -> Self {
92        Self::new(title)
93    }
94}
95
96/// Which way a split's parts sit.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum Axis {
99    /// Side by side.
100    Row,
101    /// One above the other.
102    Column,
103}
104
105/// Part of the dock, as fractions of its rectangle.
106#[derive(Debug, Clone, Copy, PartialEq)]
107struct Frac {
108    x0: f32,
109    y0: f32,
110    x1: f32,
111    y1: f32,
112}
113
114impl Frac {
115    const WHOLE: Self = Self {
116        x0: 0.0,
117        y0: 0.0,
118        x1: 1.0,
119        y1: 1.0,
120    };
121}
122
123#[derive(Debug, Clone)]
124enum Kind {
125    /// Two parts sharing a rectangle; `ratio` of it is the first's.
126    Split {
127        axis: Axis,
128        ratio: f32,
129        first: usize,
130        second: usize,
131    },
132    /// Tabs (indices into `DockArea::tabs`), the position of the shown one among them, and
133    /// how far the tab bar is scrolled, in logical pixels (when its tabs do not fit).
134    Group {
135        tabs: Vec<usize>,
136        active: usize,
137        scroll: f32,
138        /// The group's number for assistive technology: kept while it lives.
139        serial: u32,
140    },
141}
142
143#[derive(Debug, Clone)]
144struct Node {
145    kind: Kind,
146    parent: Option<usize>,
147}
148
149/// A panel's tab: its widget, title, key and group.
150#[derive(Debug)]
151struct Tab {
152    widget: WidgetId,
153    title: Text,
154    key: Option<String>,
155    group: usize,
156    /// The tab's number for assistive technology: kept while it lives, whatever its index.
157    serial: u32,
158}
159
160/// The divider between a split's two parts.
161#[derive(Debug, Clone, Copy)]
162struct Divider {
163    split: usize,
164    axis: Axis,
165    ratio: f32,
166    /// The split's whole area.
167    area: Frac,
168}
169
170/// Where a dragged tab would land.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum Target {
173    /// Into the group: before the tab at the position (dropped on its tab bar), or at the end
174    /// (`None`: dropped in its middle, where its own tab is already in it).
175    Into(usize, Option<usize>),
176    /// Beside the group, in a new one.
177    Beside(usize, DockSide),
178}
179
180#[derive(Debug, Clone, Copy)]
181enum Drag {
182    /// A tab pressed at `from` that has not moved far: a click so far.
183    Pressed { tab: usize, from: Point },
184    /// A tab being dragged, and where it would land.
185    Tab { tab: usize, target: Option<Target> },
186    /// A split's divider being dragged: grabbed `grab` from its middle; whether it moved.
187    Divider {
188        split: usize,
189        grab: f32,
190        moved: bool,
191    },
192}
193
194/// What a point of the dock is on.
195#[derive(Debug, Clone, Copy)]
196enum Found {
197    Tab(usize, Rect),
198    Divider(usize),
199    Group(usize, Rect),
200    /// A scroll button of a group's tab bar: forward (`true`) or back.
201    ScrollTabs(usize, bool),
202    Nothing,
203}
204
205/// Panels in tab groups, side by side or stacked, that the user rearranges: click a tab to
206/// show its panel, drag it along its bar to reorder, onto another group to join that group,
207/// or near a group's edge to split the group, and drag the gaps between groups to resize
208/// them. Tabs that do not fit their bar shrink, and the shown tab always stays in view.
209///
210/// The panels are the dock's children, added with [`DockArea::add_panel`] and removed with
211/// [`DockArea::remove_panel`] (a panel removed from the [`Ui`] directly is forgotten at the
212/// dock's next event). The dock owns their styles, which place and hide them, so style what
213/// goes inside a panel rather than the panel itself, and leave the dock's own padding at
214/// zero. A hidden panel keeps its state, scroll positions included. Give the dock
215/// [`DockArea::style`].
216///
217/// The arrangement of panels added with keys ([`DockPanel::with_key`]) is saved with
218/// [`DockArea::layout`] and put back, in this run or the next, with
219/// [`DockArea::restore_layout`]; a [`DockLayout`] reads and writes itself as a line of text.
220///
221/// ```
222/// use densa_core::Size;
223/// use densa_paragraph::TextSystem;
224/// use densa_ui::Ui;
225/// use densa_widgets::{DockArea, DockAt, DockPanel, DockSide, Label, Panel};
226///
227/// let mut ui = Ui::new(TextSystem::new()?);
228/// ui.resize(Size::new(1200.0, 800.0));
229/// let dock = ui.add(ui.root(), DockArea::new(), DockArea::style())?;
230/// let quotes = DockArea::add_panel(&mut ui, dock, Panel::new(), "Quotes", DockAt::First)?;
231/// let right = DockAt::Beside(quotes, DockSide::Right);
232/// let chart = DockPanel::new("Chart").with_key("chart");
233/// let chart = DockArea::add_panel(&mut ui, dock, Panel::new(), chart, right)?;
234/// DockArea::add_panel(&mut ui, dock, Panel::new(), "Trades", DockAt::With(quotes))?;
235/// ui.add(chart, Label::new("Panel content goes inside the panel"), Default::default())?;
236/// ui.layout();
237/// let area = ui.widget::<DockArea>(dock).unwrap();
238/// assert_eq!(area.group_count(), 2);
239/// assert!(!area.is_shown(quotes), "Trades, added last, is the tab shown");
240/// assert_eq!(area.panel_with_key("chart"), Some(chart));
241/// # Ok::<(), Box<dyn std::error::Error>>(())
242/// ```
243///
244/// It reports [`ActionKind::Selected`](densa_ui::ActionKind::Selected) with the panel's
245/// position in [`DockArea::panels`] (valid until a panel is removed) when a click shows a
246/// panel, and [`ActionKind::Changed`](densa_ui::ActionKind::Changed) when a drag moved a tab
247/// or a divider.
248#[derive(Debug, Default)]
249pub struct DockArea {
250    /// Splits and groups; `None` slots are free.
251    nodes: Vec<Option<Node>>,
252    root: Option<usize>,
253    tabs: Vec<Tab>,
254    /// The groups and where they are, recomputed when the arrangement changes.
255    frames: Vec<(usize, Frac)>,
256    dividers: Vec<Divider>,
257    /// Reused by `arrange`, which runs on every move of a divider drag.
258    stack: Vec<(usize, Frac)>,
259    drag: Option<Drag>,
260    /// The tab under the pointer.
261    hovered: Option<usize>,
262    /// The scale the panels were last styled at.
263    styled_scale: f32,
264    /// The group whose tabs the keyboard moves between (the last one clicked or chosen).
265    focus_group: Option<usize>,
266    /// The last serial given to a tab or a group.
267    last_serial: u32,
268}
269
270impl DockArea {
271    /// An empty dock.
272    pub fn new() -> Self {
273        Self::default()
274    }
275
276    /// A dock filling its parent's free space, allowed to be smaller than its panels' content
277    /// and clipping them.
278    pub fn style() -> Style {
279        Style::column()
280            .grow(1.0)
281            .min_size(Length::Px(0.0), Length::Px(0.0))
282            .overflow(Overflow::Clip)
283    }
284
285    /// Adds `widget` to `dock` as a panel (a title, or a [`DockPanel`]), placed `at`, and
286    /// shows it.
287    ///
288    /// # Errors
289    /// [`UiError::NoSuchWidget`] if `dock` is gone or is not a [`DockArea`], or if `at`
290    /// names a widget that is not one of its panels; [`UiError::TooManyWidgets`] as for
291    /// [`Ui::add`].
292    pub fn add_panel(
293        ui: &mut Ui,
294        dock: WidgetId,
295        widget: impl Widget,
296        panel: impl Into<DockPanel>,
297        at: DockAt,
298    ) -> Result<WidgetId, UiError> {
299        let area = ui
300            .widget::<DockArea>(dock)
301            .ok_or(UiError::NoSuchWidget(dock))?;
302        if let DockAt::With(other) | DockAt::Beside(other, _) = at {
303            if area.index_of(other).is_none() {
304                return Err(UiError::NoSuchWidget(other));
305            }
306        }
307        let id = ui.add(dock, widget, Style::default().hidden(true))?;
308        let panel = panel.into();
309        ui.update::<DockArea, _>(dock, |area, cx| area.attach(cx, id, panel, at));
310        Ok(id)
311    }
312
313    /// Removes `panel` from `dock` and from the `Ui`; its group shows its next tab, and an
314    /// emptied group's space goes to its neighbor.
315    ///
316    /// # Errors
317    /// [`UiError::NoSuchWidget`] if `dock` is gone or is not a [`DockArea`], or `panel` is
318    /// not one of its panels.
319    pub fn remove_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
320        let removed = ui
321            .update::<DockArea, _>(dock, |area, cx| {
322                let index = area.index_of(panel)?;
323                area.drop_tab(cx, index);
324                Some(())
325            })
326            .ok_or(UiError::NoSuchWidget(dock))?;
327        removed.ok_or(UiError::NoSuchWidget(panel))?;
328        ui.remove(panel);
329        Ok(())
330    }
331
332    /// Shows `panel` in its group.
333    ///
334    /// # Errors
335    /// [`UiError::NoSuchWidget`] if `dock` is gone or is not a [`DockArea`], or `panel` is
336    /// not one of its panels.
337    pub fn show_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
338        let shown = ui
339            .update::<DockArea, _>(dock, |area, cx| area.show(cx, panel))
340            .ok_or(UiError::NoSuchWidget(dock))?;
341        shown.then_some(()).ok_or(UiError::NoSuchWidget(panel))
342    }
343
344    /// The panels' widgets, in the order they were added (removals close the gaps).
345    pub fn panels(&self) -> impl ExactSizeIterator<Item = WidgetId> + '_ {
346        self.tabs.iter().map(|t| t.widget)
347    }
348
349    /// Whether `panel` is the one its group shows.
350    pub fn is_shown(&self, panel: WidgetId) -> bool {
351        self.index_of(panel).is_some_and(|i| self.is_active(i))
352    }
353
354    /// Shows `panel` in its group (from an [`Ui::update`] on the dock); `false` if it is not
355    /// one of the dock's panels.
356    pub fn show(&mut self, cx: &mut EventCx<'_>, panel: WidgetId) -> bool {
357        let Some(index) = self.index_of(panel) else {
358            return false;
359        };
360        self.activate(index);
361        self.reveal(self.tabs[index].group, cx.rect(), cx.scale());
362        self.restyle(cx);
363        true
364    }
365
366    /// Changes `panel`'s tab title (from an [`Ui::update`] on the dock); `false` if it is not
367    /// one of the dock's panels.
368    pub fn set_title(
369        &mut self,
370        cx: &mut EventCx<'_>,
371        panel: WidgetId,
372        title: impl Into<String>,
373    ) -> bool {
374        let Some(index) = self.index_of(panel) else {
375            return false;
376        };
377        self.tabs[index].title.set(title);
378        // Shaped in `layout`, which keeps the shown tab in view.
379        cx.request_layout();
380        cx.request_paint();
381        true
382    }
383
384    /// `panel`'s tab title.
385    pub fn title(&self, panel: WidgetId) -> Option<&str> {
386        self.index_of(panel).map(|i| self.tabs[i].title.as_str())
387    }
388
389    /// The key `panel` was added with.
390    pub fn key_of(&self, panel: WidgetId) -> Option<&str> {
391        self.index_of(panel)
392            .and_then(|i| self.tabs[i].key.as_deref())
393    }
394
395    /// The first panel added with `key`.
396    pub fn panel_with_key(&self, key: &str) -> Option<WidgetId> {
397        self.tabs
398            .iter()
399            .find(|t| t.key.as_deref() == Some(key))
400            .map(|t| t.widget)
401    }
402
403    /// How many groups the panels are in.
404    pub fn group_count(&self) -> usize {
405        self.frames.len()
406    }
407
408    /// The panels sharing `panel`'s group, in tab order; none if it is not a panel.
409    pub fn tabs_with(&self, panel: WidgetId) -> impl Iterator<Item = WidgetId> + '_ {
410        let tabs = self
411            .index_of(panel)
412            .map_or(&[][..], |i| self.group_tabs(self.tabs[i].group).0);
413        tabs.iter().map(|&i| self.tabs[i].widget)
414    }
415
416    /// Where `panel`'s tab is drawn when the dock is at `dock` (its rectangle) and `scale`;
417    /// `None` if it is not a panel or its tab is scrolled out of its bar.
418    pub fn tab_rect(&self, panel: WidgetId, dock: Rect, scale: f32) -> Option<Rect> {
419        self.tab_rect_of(self.index_of(panel)?, dock, scale)
420    }
421
422    fn index_of(&self, panel: WidgetId) -> Option<usize> {
423        self.tabs.iter().position(|t| t.widget == panel)
424    }
425
426    fn attach(&mut self, cx: &mut EventCx<'_>, widget: WidgetId, panel: DockPanel, at: DockAt) {
427        let index = self.tabs.len();
428        let serial = self.next_serial();
429        self.tabs.push(Tab {
430            widget,
431            title: Text::new(panel.title),
432            key: panel.key,
433            group: usize::MAX,
434            serial,
435        });
436        let first = self.frames.first().map(|&(group, _)| group);
437        let other = match at {
438            DockAt::With(other) | DockAt::Beside(other, _) => self.index_of(other),
439            DockAt::First => None,
440        };
441        match (at, other) {
442            (DockAt::Beside(_, side), Some(other)) => {
443                let group = self.tabs[other].group;
444                self.split(index, group, side);
445            }
446            (DockAt::With(_), Some(other)) => {
447                let group = self.tabs[other].group;
448                self.join(index, Some(group), None);
449            }
450            _ => self.join(index, first, None),
451        }
452        self.arrange();
453        self.restyle(cx);
454        // The new title is shaped in `layout`, which then brings it into view.
455        cx.request_layout();
456    }
457
458    /// Shows each group's active panel where its group is, and hides the others; panels whose
459    /// style is already right are left alone (setting a style lays the panel out again).
460    fn restyle(&mut self, cx: &mut EventCx<'_>) {
461        let scale = cx.scale();
462        self.styled_scale = scale;
463        for (index, tab) in self.tabs.iter().enumerate() {
464            let frame = self.frames.iter().find(|(g, _)| *g == tab.group);
465            let style = match frame {
466                Some(&(_, frac)) if self.is_active(index) => content_style(frac, scale),
467                _ => Style::default().hidden(true),
468            };
469            if cx.style_of(tab.widget) != Some(&style) {
470                cx.set_style(tab.widget, style);
471            }
472        }
473        cx.request_paint();
474    }
475
476    /// Paints a group's frame and tab bar; the shown tab takes its panel's color, so it reads
477    /// as joined to the panel below.
478    fn paint_group(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, group: usize, area: Rect) {
479        let (theme, scale) = (cx.theme(), cx.scale());
480        let line = hairline(scale);
481        let bar = bar(area, scale);
482        canvas.fill_rect(area, theme.surface);
483        let strip = Rect::from_edges(area.left(), area.top(), area.right(), bar.bottom());
484        canvas.fill_rect(strip, theme.background);
485        outline(canvas, area, line, theme.border);
486        let under = Rect::from_edges(area.left(), bar.bottom() - line, area.right(), bar.bottom());
487        canvas.fill_rect(under, theme.border);
488        let strip = self.strip(group, bar, scale);
489        if let Some((back, forward)) = strip.buttons {
490            let can = |yes: bool| if yes { theme.text } else { theme.text_dim };
491            chevron(canvas, back, line, false, can(strip.offset > 0.0));
492            chevron(
493                canvas,
494                forward,
495                line,
496                true,
497                can(strip.offset < strip.max_offset),
498            );
499        }
500        let saved = canvas.state();
501        canvas.clip(strip.tabs);
502        let (_, active) = self.group_tabs(group);
503        for (position, (index, tab)) in self.tab_rects(group, bar, scale).enumerate() {
504            // A tab whose panel was removed from the Ui directly, before the dock forgets it.
505            if tab.is_empty() || cx.rect_of(self.tabs[index].widget).is_none() {
506                continue;
507            }
508            if position == active {
509                canvas.fill_rect(tab, theme.surface);
510                if cx.is_focused() && self.focused_group() == Some(group) {
511                    outline(canvas, tab, 2.0 * line, theme.focus);
512                }
513                let accent =
514                    Rect::from_edges(tab.left(), tab.top(), tab.right(), tab.top() + 2.0 * line);
515                canvas.fill_rect(accent, theme.accent);
516            } else if self.hovered == Some(index) {
517                let above = Rect::from_edges(tab.left(), tab.top(), tab.right(), under.top());
518                canvas.fill_rect(above, theme.hover);
519            }
520            let title = &self.tabs[index].title;
521            let size = title.size();
522            let saved = canvas.state();
523            canvas.clip(tab);
524            let mut at = centered(tab, size.width, size.height);
525            // A title wider than its shrunk tab shows its start, not its middle.
526            let inset = (TITLE_INSET * scale).round();
527            if size.width > tab.width() - 2.0 * inset {
528                at.x = tab.left() + inset;
529            }
530            title.paint(canvas, at);
531            canvas.restore(saved);
532        }
533        canvas.restore(saved);
534    }
535
536    /// Paints where a dragged tab would land: the new group's place beside a group, or a
537    /// marker between tabs (and the group, when it is another one).
538    fn paint_target(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, tab: usize, target: Target) {
539        if !self.changes(tab, target) {
540            return;
541        }
542        let (rect, scale) = (cx.rect(), cx.scale());
543        let (Target::Into(group, _) | Target::Beside(group, _)) = target;
544        let Some(&(_, frac)) = self.frames.iter().find(|(g, _)| *g == group) else {
545            return;
546        };
547        let area = group_rect(frac, rect, scale);
548        let accent = cx.theme().accent;
549        let line = hairline(scale);
550        match target {
551            Target::Beside(_, side) => {
552                let shown = beside(frac, side, rect, scale);
553                canvas.fill_rect(shown, accent.with_alpha(0.22));
554                outline(canvas, shown, 2.0 * line, accent);
555            }
556            Target::Into(_, position) => {
557                if group != self.tabs[tab].group {
558                    canvas.fill_rect(area, accent.with_alpha(0.12));
559                }
560                let bar = bar(area, scale);
561                let mut x = bar.right();
562                if let Some(position) = position {
563                    let slot = self.tab_rects(group, bar, scale).nth(position);
564                    if let Some((_, slot)) = slot {
565                        x = slot.left();
566                    } else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
567                        x = last.right();
568                    }
569                } else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
570                    x = last.right();
571                }
572                let x = x.clamp(bar.left(), bar.right() - 2.0 * line);
573                let marker = Rect::from_edges(x, bar.top(), x + 2.0 * line, bar.bottom());
574                canvas.fill_rect(marker, accent);
575            }
576        }
577    }
578}
579
580impl Widget for DockArea {
581    fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
582        self.describe(cx, node);
583    }
584
585    fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
586        self.act(cx, request)
587    }
588
589    /// Reachable by Tab, as a tab list; clicks on panels do not focus it (a tab press does).
590    fn accepts_focus(&self) -> bool {
591        !self.tabs.is_empty()
592    }
593
594    fn focus_on_press(&self) -> bool {
595        false
596    }
597
598    fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
599        let color = cx.theme().text;
600        for tab in &mut self.tabs {
601            tab.title.fit(cx, f32::INFINITY, color);
602        }
603        // New titles, or a new size: keep every group's shown tab in view. Widths do not
604        // depend on where the dock is, only on its size.
605        let rect = Rect::new(0.0, 0.0, size.width, size.height);
606        for i in 0..self.frames.len() {
607            let group = self.frames[i].0;
608            self.reveal(group, rect, cx.scale());
609        }
610    }
611
612    fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
613        let (rect, scale) = (cx.rect(), cx.scale());
614        canvas.fill_rect(rect, cx.theme().background);
615        for &(group, frac) in &self.frames {
616            self.paint_group(cx, canvas, group, group_rect(frac, rect, scale));
617        }
618        if let Some(Drag::Divider { split, .. }) = self.drag {
619            if let Some(divider) = self.dividers.iter().find(|d| d.split == split) {
620                let strip = divider_rect(divider, rect, scale);
621                canvas.fill_rect(strip, cx.theme().accent.with_alpha(0.6));
622            }
623        }
624    }
625
626    fn paint_over(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
627        if let Some(Drag::Tab {
628            tab,
629            target: Some(target),
630        }) = self.drag
631        {
632            self.paint_target(cx, canvas, tab, target);
633        }
634    }
635
636    fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
637        self.prune(cx);
638        // Panel margins are in whole physical pixels: restyle after a scale change.
639        if cx.scale() != self.styled_scale {
640            self.restyle(cx);
641        }
642        let pointer = match event {
643            Event::Pointer(pointer) => pointer,
644            Event::Key(key) => return self.key(cx, key),
645            Event::FocusGained | Event::FocusLost => {
646                cx.request_paint();
647                return Handled::No;
648            }
649            _ => return Handled::No,
650        };
651        match pointer.kind {
652            PointerKind::Down(PointerButton::Primary) => self.press(cx, pointer.position),
653            PointerKind::Move => self.drag_to(cx, pointer.position),
654            // Any release ends the press (the Ui stops sending the dock the pointer); only
655            // the primary button's drops a dragged tab.
656            PointerKind::Up(button) => {
657                let handled = self.finish(cx, button == PointerButton::Primary);
658                // Released over a divider: it can be grabbed again right there.
659                if let Found::Divider(i) = self.find(pointer.position, cx.rect(), cx.scale()) {
660                    cx.set_cursor(Some(input::resize_cursor(self.dividers[i].axis)));
661                }
662                handled
663            }
664            PointerKind::Cancel => self.finish(cx, false),
665            PointerKind::Scroll(delta) => self.wheel(cx, pointer.position, delta),
666            PointerKind::Leave => {
667                if self.drag.is_none() {
668                    cx.set_cursor(None);
669                }
670                self.hover(cx, None);
671                Handled::No
672            }
673            _ => Handled::No,
674        }
675    }
676}
677
678/// A chevron pointing back (`<`) or forward (`>`) in the middle of `button`, drawn as a stack
679/// of `line`-high rectangles.
680fn chevron(canvas: &mut Canvas<'_>, button: Rect, line: f32, forward: bool, color: Color) {
681    let rows = 7_u8;
682    let reach = f32::from(rows / 2) * line;
683    let cx = ((button.left() + button.right()) / 2.0).round();
684    let top = ((button.top() + button.bottom()) / 2.0 - reach - line / 2.0).round();
685    for row in 0..rows {
686        let step = f32::from(row.abs_diff(rows / 2)) * line;
687        // `>`: the middle row furthest right; `<`: furthest left.
688        let x = if forward {
689            cx - reach / 2.0 + (reach - step)
690        } else {
691            cx - reach / 2.0 + step
692        };
693        let y = top + f32::from(row) * line;
694        canvas.fill_rect(Rect::from_edges(x, y, x + 2.0 * line, y + line), color);
695    }
696}