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