Skip to main content

iced_cube/layout/
resizable_panel.rs

1//! Two or more panels in a row or column, separated by handles that resize
2//! them.
3//!
4//! [`State`] holds the panels' constraints and their sizes, as fractions of
5//! the space the panels share, so an app can persist them with
6//! [`State::sizes`] and restore them with [`State::with_sizes`]. Its pure
7//! [`update`](State::update) applies drags, keyboard steps and collapsing,
8//! enforcing each panel's [min, max and collapse threshold](Panel).
9//!
10//! Dragging a handle moves the panels next to it; when one reaches its
11//! limit the change carries on to the panels beyond it. A
12//! [collapsible](Panel::collapsible) panel snaps shut when dragged below its
13//! threshold and snaps open again when dragged past it.
14//!
15//! # Keyboard
16//!
17//! iced 0.14 has no general focus model, but it does let widgets take part
18//! in its focus operations. Each handle is focusable: pressing it focuses
19//! it, as a text field is, and `iced::widget::operation::focus_next()` or
20//! `focus(id)` reach it too. While a handle has focus the group resolves
21//! its own [`Keymap`] of [`Action`]s and captures the keys it uses, so an
22//! app needs no subscription; see [`default_keymap`]. A press anywhere
23//! else, or Escape, takes the focus away.
24
25use iced::Event as Input;
26use iced::advanced::layout::{self, Layout};
27use iced::advanced::widget::operation::Focusable;
28use iced::advanced::widget::{Operation, Tree, Widget, tree};
29use iced::advanced::{Clipboard, Shell, overlay, renderer};
30use iced::keyboard::{self, key::Named};
31use iced::widget;
32use iced::{
33    Background, Border, Color, Element, Length, Point, Rectangle, Renderer, Shadow, Size, Theme,
34    Vector, mouse, touch,
35};
36
37use crate::keys::{self, Chord, Keymap};
38use crate::theme::{Tokens, radius};
39
40/// Space a handle takes up between two panels, in logical pixels.
41pub const HANDLE: f32 = 1.0;
42
43/// Width of the area around a handle that responds to the pointer.
44pub const HIT_AREA: f32 = 9.0;
45
46/// Width of the bar drawn over a highlighted handle.
47const HIGHLIGHT: f32 = 3.0;
48
49/// Size of the grip across and along the handle.
50const GRIP: Size = Size::new(12.0, 18.0);
51
52/// Sizes closer than this count as equal, in logical pixels.
53const EPSILON: f32 = 0.01;
54
55/// Whether panels sit side by side or one above the other.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
57pub enum Axis {
58    /// A row of panels with upright handles between them.
59    #[default]
60    Horizontal,
61    /// A column of panels with level handles between them.
62    Vertical,
63}
64
65impl Axis {
66    pub const ALL: [Axis; 2] = [Axis::Horizontal, Axis::Vertical];
67
68    /// The coordinate of `point` along this axis.
69    pub fn along(self, point: Point) -> f32 {
70        match self {
71            Axis::Horizontal => point.x,
72            Axis::Vertical => point.y,
73        }
74    }
75
76    /// The pointer shape over a handle.
77    pub fn cursor(self) -> mouse::Interaction {
78        match self {
79            Axis::Horizontal => mouse::Interaction::ResizingColumn,
80            Axis::Vertical => mouse::Interaction::ResizingRow,
81        }
82    }
83
84    fn main(self, size: Size) -> f32 {
85        match self {
86            Axis::Horizontal => size.width,
87            Axis::Vertical => size.height,
88        }
89    }
90
91    fn cross(self, size: Size) -> f32 {
92        match self {
93            Axis::Horizontal => size.height,
94            Axis::Vertical => size.width,
95        }
96    }
97
98    fn size(self, main: f32, cross: f32) -> Size {
99        match self {
100            Axis::Horizontal => Size::new(main, cross),
101            Axis::Vertical => Size::new(cross, main),
102        }
103    }
104
105    fn point(self, main: f32) -> Point {
106        match self {
107            Axis::Horizontal => Point::new(main, 0.0),
108            Axis::Vertical => Point::new(0.0, main),
109        }
110    }
111}
112
113/// A length along the group: a share of the space the panels have, or a
114/// fixed number of logical pixels.
115#[derive(Debug, Clone, Copy, PartialEq)]
116pub enum Extent {
117    /// A share of the space, from 0.0 to 1.0.
118    Fraction(f32),
119    Pixels(f32),
120}
121
122impl Extent {
123    /// The length in pixels, out of `total` pixels.
124    pub fn resolve(self, total: f32) -> f32 {
125        let pixels = match self {
126            Extent::Fraction(share) => share * total,
127            Extent::Pixels(pixels) => pixels,
128        };
129        pixels.max(0.0)
130    }
131}
132
133/// The constraints of one panel.
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct Panel {
136    /// The starting size. Fractions share out the space the pixel panels
137    /// leave.
138    pub default: Extent,
139    pub min: Extent,
140    pub max: Extent,
141    /// Whether the panel snaps shut below its threshold.
142    pub collapsible: bool,
143    /// The size of the panel while collapsed.
144    pub collapsed_size: Extent,
145    /// Below this size a collapsible panel snaps shut. Defaults to halfway
146    /// between the collapsed size and the minimum.
147    pub collapse_below: Option<Extent>,
148    /// Whether the panel starts collapsed.
149    pub collapsed: bool,
150}
151
152/// A panel starting at `default`, with no minimum or maximum.
153pub fn panel(default: Extent) -> Panel {
154    Panel {
155        default,
156        min: Extent::Pixels(0.0),
157        max: Extent::Fraction(1.0),
158        collapsible: false,
159        collapsed_size: Extent::Pixels(0.0),
160        collapse_below: None,
161        collapsed: false,
162    }
163}
164
165impl Panel {
166    pub fn min(mut self, min: Extent) -> Self {
167        self.min = min;
168        self
169    }
170
171    pub fn max(mut self, max: Extent) -> Self {
172        self.max = max;
173        self
174    }
175
176    pub fn collapsible(mut self, collapsible: bool) -> Self {
177        self.collapsible = collapsible;
178        self
179    }
180
181    /// The size while collapsed, such as the width of an icon rail.
182    pub fn collapsed_size(mut self, size: Extent) -> Self {
183        self.collapsed_size = size;
184        self
185    }
186
187    pub fn collapse_below(mut self, threshold: Extent) -> Self {
188        self.collapse_below = Some(threshold);
189        self
190    }
191
192    /// Starts the panel collapsed. Only applies to a collapsible panel.
193    pub fn collapsed(mut self, collapsed: bool) -> Self {
194        self.collapsed = collapsed;
195        self
196    }
197
198    /// The minimum, maximum and collapsed sizes in pixels. The maximum is
199    /// never below the minimum.
200    pub fn limits(&self, total: f32) -> Limits {
201        let min = self.min.resolve(total);
202        let collapsed = self.collapsed_size.resolve(total).min(min);
203        Limits {
204            min,
205            max: self.max.resolve(total).max(min),
206            collapsed,
207            threshold: self
208                .collapse_below
209                .map_or((collapsed + min) / 2.0, |threshold| {
210                    threshold.resolve(total).clamp(collapsed, min)
211                }),
212        }
213    }
214
215    /// The nearest size this panel can take to `size`: collapsed below the
216    /// threshold when collapsible, otherwise within its minimum and maximum.
217    pub fn settle(&self, size: f32, total: f32) -> f32 {
218        let limits = self.limits(total);
219        if self.collapsible && size < limits.threshold {
220            return limits.collapsed;
221        }
222        size.clamp(limits.min, limits.max)
223    }
224
225    /// Whether a panel of `size` pixels is collapsed.
226    pub fn is_collapsed_at(&self, size: f32, total: f32) -> bool {
227        let limits = self.limits(total);
228        self.collapsible && size <= limits.collapsed + EPSILON && limits.collapsed < limits.min
229    }
230}
231
232/// A panel's constraints resolved to pixels.
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub struct Limits {
235    pub min: f32,
236    pub max: f32,
237    pub collapsed: f32,
238    pub threshold: f32,
239}
240
241/// The starting sizes of `panels` in pixels: pixel defaults as given, and
242/// fraction defaults sharing out what is left in proportion, then fitted to
243/// the panels' constraints.
244pub fn defaults(panels: &[Panel], total: f32) -> Vec<f32> {
245    let fixed: f32 = panels
246        .iter()
247        .filter_map(|panel| match panel.default {
248            Extent::Pixels(pixels) => Some(pixels.max(0.0)),
249            Extent::Fraction(_) => None,
250        })
251        .sum();
252    let shares: f32 = panels
253        .iter()
254        .filter_map(|panel| match panel.default {
255            Extent::Fraction(share) => Some(share.max(0.0)),
256            Extent::Pixels(_) => None,
257        })
258        .sum();
259    let left = (total - fixed).max(0.0);
260    let sizes = panels
261        .iter()
262        .map(|panel| match panel.default {
263            Extent::Pixels(pixels) => pixels.max(0.0),
264            Extent::Fraction(share) if shares > 0.0 => left * share.max(0.0) / shares,
265            Extent::Fraction(_) => 0.0,
266        })
267        .collect();
268    let collapsed: Vec<bool> = panels
269        .iter()
270        .map(|panel| panel.collapsible && panel.collapsed)
271        .collect();
272    fit(panels, &collapsed, sizes, total)
273}
274
275/// Fits `sizes` into `total` pixels within each panel's limits.
276///
277/// Collapsed panels take their collapsed size. The rest grow or shrink in
278/// proportion to their size until the sizes add up. When the minimums do
279/// not fit, collapsible panels collapse, last first, and as a last resort
280/// every panel scales down, so the sizes always add up to `total`.
281pub fn fit(panels: &[Panel], collapsed: &[bool], sizes: Vec<f32>, total: f32) -> Vec<f32> {
282    let mut shut: Vec<bool> = (0..panels.len())
283        .map(|index| collapsed.get(index).copied().unwrap_or(false))
284        .collect();
285    let mut fitted = constrain(panels, &shut, &sizes, total);
286    distribute(panels, &shut, &mut fitted, total);
287
288    while fitted.iter().sum::<f32>() > total + EPSILON {
289        let Some(index) = (0..panels.len())
290            .rev()
291            .find(|&index| panels[index].collapsible && !shut[index])
292        else {
293            break;
294        };
295        shut[index] = true;
296        fitted = constrain(panels, &shut, &sizes, total);
297        distribute(panels, &shut, &mut fitted, total);
298    }
299
300    let sum: f32 = fitted.iter().sum();
301    if sum > EPSILON && (sum - total).abs() > EPSILON {
302        for size in &mut fitted {
303            *size *= total / sum;
304        }
305    }
306    fitted
307}
308
309fn constrain(panels: &[Panel], shut: &[bool], sizes: &[f32], total: f32) -> Vec<f32> {
310    panels
311        .iter()
312        .enumerate()
313        .map(|(index, panel)| {
314            let limits = panel.limits(total);
315            if shut[index] && panel.collapsible {
316                return limits.collapsed;
317            }
318            sizes
319                .get(index)
320                .copied()
321                .unwrap_or(0.0)
322                .clamp(limits.min, limits.max)
323        })
324        .collect()
325}
326
327/// Spreads the difference between `total` and the sizes over the open
328/// panels that have room, in proportion to their size.
329fn distribute(panels: &[Panel], shut: &[bool], sizes: &mut [f32], total: f32) {
330    for _ in 0..=panels.len() {
331        let gap = total - sizes.iter().sum::<f32>();
332        if gap.abs() <= EPSILON {
333            return;
334        }
335        let room = |index: usize| {
336            let limits = panels[index].limits(total);
337            let open = !(shut[index] && panels[index].collapsible);
338            open && if gap > 0.0 {
339                sizes[index] < limits.max - EPSILON
340            } else {
341                sizes[index] > limits.min + EPSILON
342            }
343        };
344        let flexible: Vec<usize> = (0..panels.len()).filter(|&index| room(index)).collect();
345        if flexible.is_empty() {
346            return;
347        }
348        let weight: f32 = flexible.iter().map(|&index| sizes[index]).sum();
349        for &index in &flexible {
350            let share = if weight > EPSILON {
351                sizes[index] / weight
352            } else {
353                1.0 / flexible.len() as f32
354            };
355            let limits = panels[index].limits(total);
356            sizes[index] = (sizes[index] + gap * share).clamp(limits.min, limits.max);
357        }
358    }
359}
360
361/// The sizes after moving handle `handle` by `delta` pixels from `origin`.
362///
363/// The panel on the side the handle moves towards shrinks, and once it
364/// reaches its minimum the panels beyond it shrink in turn. The panel on
365/// the other side grows, passing any growth past its maximum on outwards.
366/// Collapsible panels snap shut and open at their threshold. When the move
367/// cannot be balanced, `origin` comes back unchanged.
368pub fn resize(panels: &[Panel], origin: &[f32], handle: usize, delta: f32, total: f32) -> Vec<f32> {
369    let count = panels.len();
370    if handle + 1 >= count || origin.len() != count || delta.abs() < EPSILON {
371        return origin.to_vec();
372    }
373    let before: Vec<usize> = (0..=handle).rev().collect();
374    let after: Vec<usize> = (handle + 1..count).collect();
375    let (grow, shrink) = if delta > 0.0 {
376        (before, after)
377    } else {
378        (after, before)
379    };
380
381    let mut request = delta.abs();
382    for _ in 0..3 {
383        let (shrunk, given) = shrink_by(panels, origin, &shrink, request, total);
384        if given < EPSILON {
385            return origin.to_vec();
386        }
387        let (grown, taken) = grow_by(panels, shrunk, &grow, given, total);
388        if (taken - given).abs() < EPSILON {
389            return grown;
390        }
391        // Snapping made one side move more than the other: try again with
392        // what the growing side accepts.
393        request = taken;
394    }
395    origin.to_vec()
396}
397
398fn shrink_by(
399    panels: &[Panel],
400    origin: &[f32],
401    order: &[usize],
402    request: f32,
403    total: f32,
404) -> (Vec<f32>, f32) {
405    let mut sizes = origin.to_vec();
406    let mut given = 0.0;
407    for &index in order {
408        let remaining = request - given;
409        if remaining < EPSILON {
410            break;
411        }
412        let current = sizes[index];
413        let settled = panels[index].settle(current - remaining, total);
414        if settled < current {
415            given += current - settled;
416            sizes[index] = settled;
417        }
418    }
419    (sizes, given)
420}
421
422fn grow_by(
423    panels: &[Panel],
424    mut sizes: Vec<f32>,
425    order: &[usize],
426    amount: f32,
427    total: f32,
428) -> (Vec<f32>, f32) {
429    let mut taken = 0.0;
430    for &index in order {
431        let remaining = amount - taken;
432        if remaining < EPSILON {
433            break;
434        }
435        let current = sizes[index];
436        let settled = panels[index].settle(current + remaining, total);
437        if settled > current {
438            taken += settled - current;
439            sizes[index] = settled;
440        } else if panels[index].is_collapsed_at(current, total) {
441            // A collapsed panel that stays shut holds the handle in place.
442            break;
443        }
444    }
445    (sizes, taken)
446}
447
448/// Changes to a panel group.
449#[derive(Debug, Clone, Copy, PartialEq)]
450pub enum Event {
451    /// The group measured the space its panels share, in pixels. The
452    /// widget sends it whenever that changes.
453    Measure(f32),
454    /// The pointer pressed `handle` at `position` along the axis.
455    DragStart {
456        handle: usize,
457        position: f32,
458    },
459    /// The pointer moved to `position` along the axis while dragging.
460    Drag(f32),
461    DragEnd,
462    /// Moves `handle` by `delta` pixels, towards the end when positive.
463    Resize {
464        handle: usize,
465        delta: f32,
466    },
467    Collapse(usize),
468    Expand(usize),
469    /// Collapses an open panel or expands a collapsed one.
470    Toggle(usize),
471    /// Puts every panel back to its default size.
472    Reset,
473}
474
475/// What changed after an event.
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub enum Output {
478    /// The sizes changed. Read them with [`State::sizes`].
479    Resized,
480    /// This panel collapsed.
481    Collapsed(usize),
482    /// This panel expanded.
483    Expanded(usize),
484}
485
486/// A drag in progress: the handle, where the pointer started and the
487/// sizes at that moment.
488#[derive(Debug, Clone, PartialEq)]
489struct Drag {
490    handle: usize,
491    start: f32,
492    origin: Vec<f32>,
493}
494
495/// The panels, their sizes and any drag in progress.
496#[derive(Debug, Clone, PartialEq)]
497pub struct State {
498    panels: Vec<Panel>,
499    /// Fractions of the shared space, adding up to 1. Empty until the
500    /// group is first measured or sizes are restored.
501    sizes: Vec<f32>,
502    collapsed: Vec<bool>,
503    /// The size each collapsed panel had before it collapsed.
504    restore: Vec<Option<f32>>,
505    total: f32,
506    /// Sizes came from [`State::with_sizes`], so which panels are collapsed
507    /// is worked out on the first measurement.
508    restored: bool,
509    drag: Option<Drag>,
510    step: Extent,
511    large_step: Extent,
512}
513
514impl State {
515    /// A group of `panels` at their default sizes.
516    pub fn new(panels: impl IntoIterator<Item = Panel>) -> Self {
517        let panels: Vec<Panel> = panels.into_iter().collect();
518        let collapsed = panels
519            .iter()
520            .map(|panel| panel.collapsible && panel.collapsed)
521            .collect();
522        Self {
523            restore: vec![None; panels.len()],
524            panels,
525            sizes: Vec::new(),
526            collapsed,
527            total: 0.0,
528            restored: false,
529            drag: None,
530            step: Extent::Fraction(0.05),
531            large_step: Extent::Fraction(0.2),
532        }
533    }
534
535    /// Starts from saved sizes, as returned by [`State::sizes`]. Sizes of
536    /// the wrong length or adding up to nothing are ignored; the rest are
537    /// scaled to add up to 1.
538    pub fn with_sizes(mut self, sizes: impl IntoIterator<Item = f32>) -> Self {
539        self.set_sizes(sizes);
540        self
541    }
542
543    /// Replaces the sizes, as [`State::with_sizes`] does, fitting them to
544    /// the panels' limits once the group has been measured.
545    pub fn set_sizes(&mut self, sizes: impl IntoIterator<Item = f32>) {
546        let sizes: Vec<f32> = sizes.into_iter().map(|size| size.max(0.0)).collect();
547        let sum: f32 = sizes.iter().sum();
548        if sizes.len() != self.panels.len() || sum <= 0.0 {
549            return;
550        }
551        self.sizes = sizes.iter().map(|size| size / sum).collect();
552        self.collapsed = self
553            .panels
554            .iter()
555            .zip(&self.sizes)
556            .map(|(panel, size)| panel.collapsible && *size <= EPSILON / 100.0)
557            .collect();
558        self.restored = true;
559        if self.total > 0.0 {
560            self.settle_restored();
561        }
562    }
563
564    /// How far the arrow keys move a handle. Defaults to 5% of the space.
565    pub fn step(mut self, step: Extent) -> Self {
566        self.step = step;
567        self
568    }
569
570    /// How far Shift and an arrow key move a handle. Defaults to 20%.
571    pub fn large_step(mut self, step: Extent) -> Self {
572        self.large_step = step;
573        self
574    }
575
576    pub fn panels(&self) -> &[Panel] {
577        &self.panels
578    }
579
580    /// Each panel's share of the space, adding up to 1, for saving and
581    /// restoring with [`State::with_sizes`]. Before the group is first
582    /// measured this is the defaults as fractions, or empty when a default
583    /// is in pixels.
584    pub fn sizes(&self) -> Vec<f32> {
585        if !self.sizes.is_empty() {
586            return self.sizes.clone();
587        }
588        let fractions: Option<Vec<f32>> = self
589            .panels
590            .iter()
591            .map(|panel| match panel.default {
592                Extent::Fraction(share) => Some(share),
593                Extent::Pixels(_) => None,
594            })
595            .collect();
596        fractions.unwrap_or_default()
597    }
598
599    /// The pixels the panels share, as last measured. 0 before the first
600    /// measurement.
601    pub fn total(&self) -> f32 {
602        self.total
603    }
604
605    pub fn is_collapsed(&self, panel: usize) -> bool {
606        self.collapsed.get(panel).copied().unwrap_or(false)
607    }
608
609    /// The handle being dragged, if any.
610    pub fn dragging(&self) -> Option<usize> {
611        self.drag.as_ref().map(|drag| drag.handle)
612    }
613
614    /// Each panel's size in pixels, out of `total`.
615    pub fn resolve(&self, total: f32) -> Vec<f32> {
616        if self.sizes.is_empty() {
617            let mut sizes = defaults(&self.panels, total);
618            self.collapse_marked(&mut sizes, total);
619            return sizes;
620        }
621        let sizes = self.sizes.iter().map(|size| size * total).collect();
622        fit(&self.panels, &self.collapsed, sizes, total)
623    }
624
625    fn collapse_marked(&self, sizes: &mut [f32], total: f32) {
626        let marked = self.collapsed.iter().any(|collapsed| *collapsed);
627        if marked {
628            let fitted = fit(&self.panels, &self.collapsed, sizes.to_vec(), total);
629            sizes.copy_from_slice(&fitted);
630        }
631    }
632
633    /// The pixel sizes for the current measurement.
634    fn current(&self) -> Vec<f32> {
635        self.resolve(self.total)
636    }
637
638    /// Applies an event and says what changed.
639    pub fn update(&mut self, event: Event) -> Option<Output> {
640        if let Event::Measure(total) = event {
641            self.measure(total);
642            return None;
643        }
644        if self.total <= 0.0 {
645            return None;
646        }
647        match event {
648            Event::Measure(_) => None,
649            Event::DragStart { handle, position } => {
650                if handle + 1 >= self.panels.len() {
651                    return None;
652                }
653                self.drag = Some(Drag {
654                    handle,
655                    start: position,
656                    origin: self.current(),
657                });
658                None
659            }
660            Event::Drag(position) => {
661                let drag = self.drag.clone()?;
662                let sizes = resize(
663                    &self.panels,
664                    &drag.origin,
665                    drag.handle,
666                    position - drag.start,
667                    self.total,
668                );
669                self.apply(sizes, &drag.origin)
670            }
671            Event::DragEnd => {
672                self.drag = None;
673                None
674            }
675            Event::Resize { handle, delta } => {
676                let origin = self.current();
677                let sizes = resize(&self.panels, &origin, handle, delta, self.total);
678                self.apply(sizes, &origin)
679            }
680            Event::Collapse(panel) => self.collapse(panel),
681            Event::Expand(panel) => self.expand(panel),
682            Event::Toggle(panel) if self.is_collapsed(panel) => self.expand(panel),
683            Event::Toggle(panel) => self.collapse(panel),
684            Event::Reset => {
685                let origin = self.current();
686                self.collapsed = self
687                    .panels
688                    .iter()
689                    .map(|panel| panel.collapsible && panel.collapsed)
690                    .collect();
691                self.restore = vec![None; self.panels.len()];
692                self.sizes.clear();
693                let sizes = self.current();
694                self.store(&sizes);
695                (!same(&sizes, &origin)).then_some(Output::Resized)
696            }
697        }
698    }
699
700    fn measure(&mut self, total: f32) {
701        if total <= 0.0 || !total.is_finite() {
702            return;
703        }
704        if self.sizes.is_empty() {
705            let sizes = self.resolve(total);
706            self.total = total;
707            self.store(&sizes);
708            return;
709        }
710        self.total = total;
711        if self.restored {
712            self.settle_restored();
713        }
714    }
715
716    /// Fits restored sizes to the panels. A size below a collapsible
717    /// panel's threshold would snap shut, so that panel starts collapsed.
718    fn settle_restored(&mut self) {
719        self.restored = false;
720        let total = self.total;
721        self.collapsed = self
722            .panels
723            .iter()
724            .zip(&self.sizes)
725            .map(|(panel, size)| panel.collapsible && size * total < panel.limits(total).threshold)
726            .collect();
727        let sizes = self.current();
728        self.store(&sizes);
729    }
730
731    /// The handle to move to resize `panel`, and whether growing the panel
732    /// moves it towards the end.
733    fn handle_for(&self, panel: usize) -> Option<(usize, bool)> {
734        if panel + 1 < self.panels.len() {
735            return Some((panel, true));
736        }
737        (panel > 0 && panel < self.panels.len()).then(|| (panel - 1, false))
738    }
739
740    fn collapse(&mut self, panel: usize) -> Option<Output> {
741        let settings = self.panels.get(panel)?;
742        if !settings.collapsible || self.is_collapsed(panel) {
743            return None;
744        }
745        let (handle, forward) = self.handle_for(panel)?;
746        let origin = self.current();
747        let shrink = origin[panel] - settings.limits(self.total).collapsed;
748        let delta = if forward { -shrink } else { shrink };
749        let sizes = resize(&self.panels, &origin, handle, delta, self.total);
750        self.apply(sizes, &origin)
751    }
752
753    fn expand(&mut self, panel: usize) -> Option<Output> {
754        let settings = self.panels.get(panel)?;
755        if !self.is_collapsed(panel) {
756            return None;
757        }
758        let (handle, forward) = self.handle_for(panel)?;
759        let origin = self.current();
760        let limits = settings.limits(self.total);
761        let wanted = self
762            .restore
763            .get(panel)
764            .copied()
765            .flatten()
766            .map_or_else(
767                || settings.default.resolve(self.total),
768                |size| size * self.total,
769            )
770            .clamp(limits.min, limits.max);
771        let grow = wanted - origin[panel];
772        let delta = if forward { grow } else { -grow };
773        let sizes = resize(&self.panels, &origin, handle, delta, self.total);
774        self.apply(sizes, &origin)
775    }
776
777    /// Stores new pixel sizes and reports what changed against `origin`.
778    fn apply(&mut self, sizes: Vec<f32>, origin: &[f32]) -> Option<Output> {
779        if same(&sizes, &self.current()) {
780            return None;
781        }
782        let was = self.collapsed.clone();
783        self.store(&sizes);
784        let changed = (0..self.panels.len()).find(|&index| was[index] != self.collapsed[index]);
785        match changed {
786            Some(index) if self.collapsed[index] => {
787                if let (Some(slot), Some(size)) = (self.restore.get_mut(index), origin.get(index)) {
788                    *slot = Some(size / self.total);
789                }
790                Some(Output::Collapsed(index))
791            }
792            Some(index) => Some(Output::Expanded(index)),
793            None => Some(Output::Resized),
794        }
795    }
796
797    fn store(&mut self, sizes: &[f32]) {
798        let sum: f32 = sizes.iter().sum();
799        if sum <= 0.0 {
800            return;
801        }
802        self.sizes = sizes.iter().map(|size| size / sum).collect();
803        self.collapsed = self
804            .panels
805            .iter()
806            .zip(sizes)
807            .map(|(panel, size)| panel.is_collapsed_at(*size, self.total.max(sum)))
808            .collect();
809    }
810}
811
812fn same(a: &[f32], b: &[f32]) -> bool {
813    a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a - b).abs() < EPSILON)
814}
815
816/// What a handle keyboard shortcut does.
817#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
818pub enum Action {
819    /// Moves the handle towards the start by one step.
820    Decrease,
821    /// Moves the handle towards the end by one step.
822    Increase,
823    DecreaseMore,
824    IncreaseMore,
825    /// Shrinks the panel before the handle to its minimum.
826    Min,
827    /// Grows the panel before the handle to its maximum.
828    Max,
829    /// Collapses or expands the collapsible panel next to the handle.
830    Toggle,
831    /// Takes the focus away from the handle.
832    Release,
833}
834
835impl Action {
836    /// The [`Event`] this action sends for `handle`, or `None` when it has
837    /// nothing to do, as for [`Action::Release`], which the group handles
838    /// itself, or [`Action::Toggle`] beside panels that cannot collapse.
839    pub fn event(self, state: &State, handle: usize) -> Option<Event> {
840        if handle + 1 >= state.panels.len() {
841            return None;
842        }
843        let total = state.total;
844        let size = || state.current().get(handle).copied().unwrap_or(0.0);
845        let limits = || state.panels[handle].limits(total);
846        let delta = match self {
847            Action::Decrease => -state.step.resolve(total),
848            Action::Increase => state.step.resolve(total),
849            Action::DecreaseMore => -state.large_step.resolve(total),
850            Action::IncreaseMore => state.large_step.resolve(total),
851            Action::Min => limits().min - size(),
852            Action::Max => limits().max - size(),
853            Action::Toggle => {
854                return [handle, handle + 1]
855                    .into_iter()
856                    .find(|&panel| state.panels[panel].collapsible)
857                    .map(Event::Toggle);
858            }
859            Action::Release => return None,
860        };
861        Some(Event::Resize { handle, delta })
862    }
863}
864
865impl keys::Action for Action {
866    const ALL: &'static [Self] = &[
867        Action::Decrease,
868        Action::Increase,
869        Action::DecreaseMore,
870        Action::IncreaseMore,
871        Action::Min,
872        Action::Max,
873        Action::Toggle,
874        Action::Release,
875    ];
876
877    fn defaults() -> Keymap<Self> {
878        default_keymap()
879    }
880
881    fn name(self) -> &'static str {
882        match self {
883            Action::Decrease => "Decrease",
884            Action::Increase => "Increase",
885            Action::DecreaseMore => "DecreaseMore",
886            Action::IncreaseMore => "IncreaseMore",
887            Action::Min => "Min",
888            Action::Max => "Max",
889            Action::Toggle => "Toggle",
890            Action::Release => "Release",
891        }
892    }
893
894    fn description(self) -> &'static str {
895        match self {
896            Action::Decrease => "Moves the focused handle one step towards the start.",
897            Action::Increase => "Moves the focused handle one step towards the end.",
898            Action::DecreaseMore => "Moves the focused handle a large step towards the start.",
899            Action::IncreaseMore => "Moves the focused handle a large step towards the end.",
900            Action::Min => "Shrinks the panel before the handle to its minimum.",
901            Action::Max => "Grows the panel before the handle to its maximum.",
902            Action::Toggle => "Collapses or expands the collapsible panel next to the handle.",
903            Action::Release => "Takes the focus away from the handle.",
904        }
905    }
906}
907
908/// The default handle shortcuts, active while a handle has focus:
909///
910/// | Keys | Action |
911/// | --- | --- |
912/// | `ArrowLeft`, `ArrowUp` | [`Action::Decrease`] |
913/// | `ArrowRight`, `ArrowDown` | [`Action::Increase`] |
914/// | `Shift+ArrowLeft`, `Shift+ArrowUp` | [`Action::DecreaseMore`] |
915/// | `Shift+ArrowRight`, `Shift+ArrowDown` | [`Action::IncreaseMore`] |
916/// | `Home` | [`Action::Min`] |
917/// | `End` | [`Action::Max`] |
918/// | `Enter` | [`Action::Toggle`] |
919/// | `Escape` | [`Action::Release`] |
920///
921/// The group resolves these itself and captures them, so they never reach
922/// `keys::subscription` while a handle has focus.
923pub fn default_keymap() -> Keymap<Action> {
924    Keymap::new()
925        .bind(Chord::named(Named::ArrowLeft), Action::Decrease)
926        .bind(Chord::named(Named::ArrowUp), Action::Decrease)
927        .bind(Chord::named(Named::ArrowRight), Action::Increase)
928        .bind(Chord::named(Named::ArrowDown), Action::Increase)
929        .bind(Chord::named(Named::ArrowLeft).shift(), Action::DecreaseMore)
930        .bind(Chord::named(Named::ArrowUp).shift(), Action::DecreaseMore)
931        .bind(
932            Chord::named(Named::ArrowRight).shift(),
933            Action::IncreaseMore,
934        )
935        .bind(Chord::named(Named::ArrowDown).shift(), Action::IncreaseMore)
936        .bind(Chord::named(Named::Home), Action::Min)
937        .bind(Chord::named(Named::End), Action::Max)
938        .bind(Chord::named(Named::Enter), Action::Toggle)
939        .bind(Chord::named(Named::Escape), Action::Release)
940}
941
942/// How a handle is drawn.
943#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
944pub enum HandleStatus {
945    #[default]
946    Idle,
947    /// Under the pointer.
948    Hovered,
949    Dragged,
950    /// Has keyboard focus.
951    Focused,
952    /// The group has no message, so the handle does nothing.
953    Disabled,
954}
955
956impl HandleStatus {
957    pub const ALL: [HandleStatus; 5] = [
958        HandleStatus::Idle,
959        HandleStatus::Hovered,
960        HandleStatus::Dragged,
961        HandleStatus::Focused,
962        HandleStatus::Disabled,
963    ];
964}
965
966/// The colours of a handle.
967#[derive(Debug, Clone, Copy, PartialEq)]
968pub struct HandleStyle {
969    /// The one pixel line between the panels.
970    pub line: Color,
971    /// The wider bar drawn over the line while highlighted.
972    pub highlight: Option<Color>,
973    pub grip: Color,
974    pub grip_border: Color,
975    pub grip_dots: Color,
976}
977
978/// Resolves a handle's colours for a status.
979pub fn handle_style(tokens: &Tokens, status: HandleStatus) -> HandleStyle {
980    let highlight = match status {
981        HandleStatus::Idle | HandleStatus::Disabled => None,
982        HandleStatus::Hovered => Some(tokens.border),
983        HandleStatus::Dragged | HandleStatus::Focused => Some(tokens.ring),
984    };
985    HandleStyle {
986        line: tokens.border,
987        highlight,
988        grip: tokens.background,
989        grip_border: match status {
990            HandleStatus::Focused | HandleStatus::Dragged => tokens.ring,
991            _ => tokens.border,
992        },
993        grip_dots: tokens.muted_foreground,
994    }
995}
996
997/// A panel group builder. Convert it into an [`Element`] to render.
998pub struct ResizablePanel<'a, Message> {
999    state: &'a State,
1000    panes: Vec<Element<'a, Message>>,
1001    axis: Axis,
1002    stack_below: Option<f32>,
1003    grip: bool,
1004    width: Length,
1005    height: Length,
1006    id: Option<widget::Id>,
1007    keymap: Keymap<Action>,
1008    on_event: Option<Box<dyn Fn(Event) -> Message + 'a>>,
1009}
1010
1011impl<Message> std::fmt::Debug for ResizablePanel<'_, Message> {
1012    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013        f.debug_struct("ResizablePanel")
1014            .field("state", self.state)
1015            .field("panes", &self.panes.len())
1016            .field("axis", &self.axis)
1017            .field("stack_below", &self.stack_below)
1018            .field("grip", &self.grip)
1019            .field("keymap", &self.keymap)
1020            .finish_non_exhaustive()
1021    }
1022}
1023
1024/// Lays out `panes`, one per panel of `state`, with handles between them.
1025/// Without [`on_event`](ResizablePanel::on_event) the handles do nothing.
1026pub fn resizable_panel<'a, Message>(
1027    state: &'a State,
1028    panes: impl IntoIterator<Item = Element<'a, Message>>,
1029) -> ResizablePanel<'a, Message> {
1030    ResizablePanel {
1031        state,
1032        panes: panes.into_iter().collect(),
1033        axis: Axis::default(),
1034        stack_below: None,
1035        grip: false,
1036        width: Length::Fill,
1037        height: Length::Fill,
1038        id: None,
1039        keymap: default_keymap(),
1040        on_event: None,
1041    }
1042}
1043
1044impl<'a, Message> ResizablePanel<'a, Message> {
1045    pub fn axis(mut self, axis: Axis) -> Self {
1046        self.axis = axis;
1047        self
1048    }
1049
1050    /// Stacks a horizontal group into a column while it is narrower than
1051    /// `width`, as on a phone.
1052    pub fn stack_below(mut self, width: f32) -> Self {
1053        self.stack_below = Some(width);
1054        self
1055    }
1056
1057    /// Draws a grip on each handle so it is easier to find.
1058    pub fn grip(mut self, grip: bool) -> Self {
1059        self.grip = grip;
1060        self
1061    }
1062
1063    /// Defaults to [`Length::Fill`].
1064    pub fn width(mut self, width: impl Into<Length>) -> Self {
1065        self.width = width.into();
1066        self
1067    }
1068
1069    /// Defaults to [`Length::Fill`].
1070    pub fn height(mut self, height: impl Into<Length>) -> Self {
1071        self.height = height.into();
1072        self
1073    }
1074
1075    /// The id of the first handle, for focusing it with
1076    /// `iced::widget::operation::focus`.
1077    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
1078        self.id = Some(id.into());
1079        self
1080    }
1081
1082    /// Replaces the [`default_keymap`] a focused handle resolves keys with.
1083    pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1084        self.keymap = keymap;
1085        self
1086    }
1087
1088    pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
1089        self.on_event = Some(Box::new(on_event));
1090        self
1091    }
1092
1093    /// The axis panels are laid out along in `width` pixels.
1094    pub fn axis_at(&self, width: f32) -> Axis {
1095        match self.stack_below {
1096            Some(breakpoint) if self.axis == Axis::Horizontal && width < breakpoint => {
1097                Axis::Vertical
1098            }
1099            _ => self.axis,
1100        }
1101    }
1102}
1103
1104impl<'a, Message: 'a> From<ResizablePanel<'a, Message>> for Element<'a, Message> {
1105    fn from(group: ResizablePanel<'a, Message>) -> Self {
1106        Element::new(group)
1107    }
1108}
1109
1110/// Whether one handle has keyboard focus, and whether to show it. A press
1111/// focuses a handle without showing it; a key or a focus operation shows
1112/// it.
1113#[derive(Debug, Clone, Copy, Default)]
1114struct HandleFocus {
1115    focused: bool,
1116    visible: bool,
1117}
1118
1119impl Focusable for HandleFocus {
1120    fn is_focused(&self) -> bool {
1121        self.focused
1122    }
1123
1124    fn focus(&mut self) {
1125        self.focused = true;
1126        self.visible = true;
1127    }
1128
1129    fn unfocus(&mut self) {
1130        self.focused = false;
1131        self.visible = false;
1132    }
1133}
1134
1135/// What the widget remembers between events.
1136#[derive(Debug, Default)]
1137struct Memory {
1138    axis: Axis,
1139    total: f32,
1140    /// Where each panel starts along the axis, and its length, relative
1141    /// to the group.
1142    slots: Vec<(f32, f32)>,
1143    hovered: Option<usize>,
1144    handles: Vec<HandleFocus>,
1145}
1146
1147impl Memory {
1148    fn focused(&self) -> Option<usize> {
1149        self.handles.iter().position(|handle| handle.focused)
1150    }
1151
1152    /// The focused handle, if its focus should be drawn.
1153    fn visible(&self) -> Option<usize> {
1154        self.handles
1155            .iter()
1156            .position(|handle| handle.focused && handle.visible)
1157    }
1158
1159    /// Focuses one handle, or none, without showing the focus.
1160    fn focus(&mut self, index: Option<usize>) {
1161        for (i, handle) in self.handles.iter_mut().enumerate() {
1162            handle.focused = Some(i) == index;
1163            handle.visible = false;
1164        }
1165    }
1166
1167    fn show_focus(&mut self) {
1168        for handle in &mut self.handles {
1169            handle.visible = handle.focused;
1170        }
1171    }
1172
1173    /// The space panel `index` has, in window coordinates.
1174    fn slot(&self, bounds: Rectangle, index: usize) -> Option<Rectangle> {
1175        let (start, length) = *self.slots.get(index)?;
1176        Some(match self.axis {
1177            Axis::Horizontal => Rectangle {
1178                x: bounds.x + start,
1179                width: length,
1180                ..bounds
1181            },
1182            Axis::Vertical => Rectangle {
1183                y: bounds.y + start,
1184                height: length,
1185                ..bounds
1186            },
1187        })
1188    }
1189
1190    /// The line of each handle, after every panel but the last.
1191    fn lines(&self, bounds: Rectangle) -> Vec<Rectangle> {
1192        let count = self.slots.len().saturating_sub(1);
1193        self.slots
1194            .iter()
1195            .take(count)
1196            .map(|(start, length)| match self.axis {
1197                Axis::Horizontal => Rectangle {
1198                    x: bounds.x + start + length,
1199                    width: HANDLE,
1200                    ..bounds
1201                },
1202                Axis::Vertical => Rectangle {
1203                    y: bounds.y + start + length,
1204                    height: HANDLE,
1205                    ..bounds
1206                },
1207            })
1208            .collect()
1209    }
1210
1211    fn handle_at(&self, bounds: Rectangle, point: Point) -> Option<usize> {
1212        self.lines(bounds)
1213            .into_iter()
1214            .position(|line| hit_area(line, self.axis).contains(point))
1215    }
1216}
1217
1218/// The area around a handle's line that responds to the pointer.
1219fn hit_area(line: Rectangle, axis: Axis) -> Rectangle {
1220    let grow = (HIT_AREA - HANDLE) / 2.0;
1221    match axis {
1222        Axis::Horizontal => Rectangle {
1223            x: line.x - grow,
1224            width: HIT_AREA,
1225            ..line
1226        },
1227        Axis::Vertical => Rectangle {
1228            y: line.y - grow,
1229            height: HIT_AREA,
1230            ..line
1231        },
1232    }
1233}
1234
1235fn pointer(event: &Input, cursor: mouse::Cursor) -> Option<Point> {
1236    match event {
1237        Input::Touch(
1238            touch::Event::FingerPressed { position, .. }
1239            | touch::Event::FingerMoved { position, .. },
1240        ) => Some(*position),
1241        _ => cursor.position(),
1242    }
1243}
1244
1245impl<Message> Widget<Message, Theme, Renderer> for ResizablePanel<'_, Message> {
1246    fn tag(&self) -> tree::Tag {
1247        tree::Tag::of::<Memory>()
1248    }
1249
1250    fn state(&self) -> tree::State {
1251        tree::State::new(Memory::default())
1252    }
1253
1254    fn size(&self) -> Size<Length> {
1255        Size::new(self.width, self.height)
1256    }
1257
1258    fn children(&self) -> Vec<Tree> {
1259        self.panes.iter().map(Tree::new).collect()
1260    }
1261
1262    fn diff(&self, tree: &mut Tree) {
1263        tree.diff_children(&self.panes);
1264    }
1265
1266    fn layout(
1267        &mut self,
1268        tree: &mut Tree,
1269        renderer: &Renderer,
1270        limits: &layout::Limits,
1271    ) -> layout::Node {
1272        let limits = limits.width(self.width).height(self.height);
1273        let max = limits.max();
1274        let axis = self.axis_at(max.width);
1275        let count = self.panes.len();
1276        let handles = count.saturating_sub(1) as f32 * HANDLE;
1277        let main = axis.main(max);
1278        let total = if main.is_finite() {
1279            (main - handles).max(0.0)
1280        } else {
1281            0.0
1282        };
1283        let cross = axis.cross(max);
1284        let sizes = self.state.resolve(total);
1285
1286        let mut start = 0.0_f32;
1287        let mut used = 0.0_f32;
1288        let mut widest = 0.0_f32;
1289        let mut nodes = Vec::with_capacity(count);
1290        let mut slots = Vec::with_capacity(count);
1291        for (index, (pane, tree)) in self.panes.iter_mut().zip(&mut tree.children).enumerate() {
1292            used += sizes.get(index).copied().unwrap_or(0.0);
1293            let end = used.round();
1294            let length = (end - start).max(0.0);
1295            let offset = start + index as f32 * HANDLE;
1296            let bounds = axis.size(length, cross);
1297            let node = pane
1298                .as_widget_mut()
1299                .layout(tree, renderer, &layout::Limits::new(Size::ZERO, bounds))
1300                .move_to(axis.point(offset));
1301            widest = widest.max(axis.cross(node.size()));
1302            nodes.push(node);
1303            slots.push((offset, length));
1304            start = end;
1305        }
1306
1307        let cross = if cross.is_finite() { cross } else { widest };
1308        let memory = tree.state.downcast_mut::<Memory>();
1309        memory.axis = axis;
1310        memory.total = total;
1311        memory.slots = slots;
1312        memory
1313            .handles
1314            .resize(count.saturating_sub(1), HandleFocus::default());
1315
1316        let size = limits.resolve(self.width, self.height, axis.size(total + handles, cross));
1317        layout::Node::with_children(size, nodes)
1318    }
1319
1320    fn operate(
1321        &mut self,
1322        tree: &mut Tree,
1323        layout: Layout<'_>,
1324        renderer: &Renderer,
1325        operation: &mut dyn Operation,
1326    ) {
1327        operation.container(None, layout.bounds());
1328        let Tree {
1329            state, children, ..
1330        } = tree;
1331        let memory = state.downcast_mut::<Memory>();
1332        let lines = memory.lines(layout.bounds());
1333        let id = self.id.as_ref();
1334        operation.traverse(&mut |operation| {
1335            for (index, (handle, line)) in memory.handles.iter_mut().zip(&lines).enumerate() {
1336                let id = id.filter(|_| index == 0);
1337                operation.focusable(id, *line, handle);
1338            }
1339            for ((pane, tree), layout) in self
1340                .panes
1341                .iter_mut()
1342                .zip(children.iter_mut())
1343                .zip(layout.children())
1344            {
1345                pane.as_widget_mut()
1346                    .operate(tree, layout, renderer, operation);
1347            }
1348        });
1349    }
1350
1351    fn update(
1352        &mut self,
1353        tree: &mut Tree,
1354        event: &Input,
1355        layout: Layout<'_>,
1356        cursor: mouse::Cursor,
1357        renderer: &Renderer,
1358        clipboard: &mut dyn Clipboard,
1359        shell: &mut Shell<'_, Message>,
1360        viewport: &Rectangle,
1361    ) {
1362        if let Some(on_event) = &self.on_event {
1363            let memory = tree.state.downcast_mut::<Memory>();
1364            if handle_event(
1365                self.state,
1366                &self.keymap,
1367                on_event,
1368                memory,
1369                event,
1370                layout,
1371                cursor,
1372                shell,
1373            ) {
1374                return;
1375            }
1376        }
1377
1378        for ((pane, tree), layout) in self
1379            .panes
1380            .iter_mut()
1381            .zip(&mut tree.children)
1382            .zip(layout.children())
1383        {
1384            pane.as_widget_mut().update(
1385                tree, event, layout, cursor, renderer, clipboard, shell, viewport,
1386            );
1387        }
1388    }
1389
1390    fn mouse_interaction(
1391        &self,
1392        tree: &Tree,
1393        layout: Layout<'_>,
1394        cursor: mouse::Cursor,
1395        viewport: &Rectangle,
1396        renderer: &Renderer,
1397    ) -> mouse::Interaction {
1398        let memory = tree.state.downcast_ref::<Memory>();
1399        if self.on_event.is_some() {
1400            let over = cursor
1401                .position()
1402                .and_then(|point| memory.handle_at(layout.bounds(), point));
1403            if self.state.dragging().is_some() || over.is_some() {
1404                return memory.axis.cursor();
1405            }
1406        }
1407        self.panes
1408            .iter()
1409            .zip(&tree.children)
1410            .zip(layout.children())
1411            .map(|((pane, tree), layout)| {
1412                pane.as_widget()
1413                    .mouse_interaction(tree, layout, cursor, viewport, renderer)
1414            })
1415            .max()
1416            .unwrap_or_default()
1417    }
1418
1419    fn draw(
1420        &self,
1421        tree: &Tree,
1422        renderer: &mut Renderer,
1423        theme: &Theme,
1424        style: &renderer::Style,
1425        layout: Layout<'_>,
1426        cursor: mouse::Cursor,
1427        viewport: &Rectangle,
1428    ) {
1429        use iced::advanced::Renderer as _;
1430
1431        let memory = tree.state.downcast_ref::<Memory>();
1432        let bounds = layout.bounds();
1433        for (index, ((pane, tree), layout)) in self
1434            .panes
1435            .iter()
1436            .zip(&tree.children)
1437            .zip(layout.children())
1438            .enumerate()
1439        {
1440            let Some(slot) = memory.slot(bounds, index) else {
1441                continue;
1442            };
1443            if slot.width < 1.0 || slot.height < 1.0 {
1444                continue;
1445            }
1446            let Some(clip) = slot.intersection(viewport) else {
1447                continue;
1448            };
1449            renderer.with_layer(clip, |renderer| {
1450                pane.as_widget()
1451                    .draw(tree, renderer, theme, style, layout, cursor, &clip);
1452            });
1453        }
1454
1455        let tokens = Tokens::of(theme);
1456        let focused = memory.visible();
1457        let over = cursor
1458            .position()
1459            .and_then(|point| memory.handle_at(bounds, point));
1460        for (index, line) in memory.lines(bounds).into_iter().enumerate() {
1461            let status = if self.on_event.is_none() {
1462                HandleStatus::Disabled
1463            } else if self.state.dragging() == Some(index) {
1464                HandleStatus::Dragged
1465            } else if focused == Some(index) {
1466                HandleStatus::Focused
1467            } else if over == Some(index) {
1468                HandleStatus::Hovered
1469            } else {
1470                HandleStatus::Idle
1471            };
1472            draw_handle(
1473                renderer,
1474                line,
1475                memory.axis,
1476                handle_style(&tokens, status),
1477                self.grip,
1478            );
1479        }
1480    }
1481
1482    fn overlay<'b>(
1483        &'b mut self,
1484        tree: &'b mut Tree,
1485        layout: Layout<'b>,
1486        renderer: &Renderer,
1487        viewport: &Rectangle,
1488        translation: Vector,
1489    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
1490        overlay::from_children(
1491            &mut self.panes,
1492            tree,
1493            layout,
1494            renderer,
1495            viewport,
1496            translation,
1497        )
1498    }
1499}
1500
1501/// Handles pointer and key events on the handles. Returns whether the
1502/// event was used, so the panes never see it.
1503#[allow(clippy::too_many_arguments)]
1504fn handle_event<Message>(
1505    state: &State,
1506    keymap: &Keymap<Action>,
1507    on_event: &dyn Fn(Event) -> Message,
1508    memory: &mut Memory,
1509    event: &Input,
1510    layout: Layout<'_>,
1511    cursor: mouse::Cursor,
1512    shell: &mut Shell<'_, Message>,
1513) -> bool {
1514    if memory.total > 0.0 && (state.total() - memory.total).abs() > 0.5 {
1515        shell.publish(on_event(Event::Measure(memory.total)));
1516    }
1517    let axis = memory.axis;
1518
1519    match event {
1520        Input::Mouse(mouse::Event::CursorMoved { .. })
1521        | Input::Touch(touch::Event::FingerMoved { .. }) => {
1522            let Some(point) = pointer(event, cursor) else {
1523                return false;
1524            };
1525            if state.dragging().is_some() {
1526                shell.publish(on_event(Event::Drag(axis.along(point))));
1527                shell.capture_event();
1528                return true;
1529            }
1530            let hovered = memory.handle_at(layout.bounds(), point);
1531            if hovered != memory.hovered {
1532                memory.hovered = hovered;
1533                shell.request_redraw();
1534            }
1535            false
1536        }
1537        Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1538        | Input::Touch(touch::Event::FingerPressed { .. }) => {
1539            let hit = pointer(event, cursor).and_then(|point| {
1540                memory
1541                    .handle_at(layout.bounds(), point)
1542                    .map(|handle| (handle, point))
1543            });
1544            let Some((handle, point)) = hit else {
1545                if memory.focused().is_some() {
1546                    memory.focus(None);
1547                    shell.request_redraw();
1548                }
1549                return false;
1550            };
1551            memory.focus(Some(handle));
1552            shell.publish(on_event(Event::DragStart {
1553                handle,
1554                position: axis.along(point),
1555            }));
1556            shell.capture_event();
1557            true
1558        }
1559        Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1560        | Input::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
1561            if state.dragging().is_none() {
1562                return false;
1563            }
1564            shell.publish(on_event(Event::DragEnd));
1565            shell.capture_event();
1566            true
1567        }
1568        Input::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
1569            let Some(handle) = memory.focused() else {
1570                return false;
1571            };
1572            let Some(action) = keymap.resolve(key, *modifiers) else {
1573                return false;
1574            };
1575            if action == Action::Release {
1576                memory.focus(None);
1577            } else {
1578                memory.show_focus();
1579                if let Some(event) = action.event(state, handle) {
1580                    shell.publish(on_event(event));
1581                }
1582            }
1583            shell.request_redraw();
1584            shell.capture_event();
1585            true
1586        }
1587        _ => false,
1588    }
1589}
1590
1591fn draw_handle(
1592    renderer: &mut Renderer,
1593    line: Rectangle,
1594    axis: Axis,
1595    style: HandleStyle,
1596    grip: bool,
1597) {
1598    use iced::advanced::Renderer as _;
1599
1600    let quad = |bounds: Rectangle, radius: f32, border: Option<Color>| renderer::Quad {
1601        bounds,
1602        border: Border {
1603            color: border.unwrap_or(Color::TRANSPARENT),
1604            width: if border.is_some() { 1.0 } else { 0.0 },
1605            radius: radius.into(),
1606        },
1607        shadow: Shadow::default(),
1608        snap: true,
1609    };
1610
1611    renderer.fill_quad(quad(line, 0.0, None), Background::Color(style.line));
1612
1613    if let Some(colour) = style.highlight {
1614        let grow = (HIGHLIGHT - HANDLE) / 2.0;
1615        let bar = match axis {
1616            Axis::Horizontal => Rectangle {
1617                x: line.x - grow,
1618                width: HIGHLIGHT,
1619                ..line
1620            },
1621            Axis::Vertical => Rectangle {
1622                y: line.y - grow,
1623                height: HIGHLIGHT,
1624                ..line
1625            },
1626        };
1627        renderer.fill_quad(quad(bar, 0.0, None), Background::Color(colour));
1628    }
1629
1630    if !grip {
1631        return;
1632    }
1633    let size = match axis {
1634        Axis::Horizontal => GRIP,
1635        Axis::Vertical => Size::new(GRIP.height, GRIP.width),
1636    };
1637    let centre = line.center();
1638    let bounds = Rectangle::new(
1639        Point::new(
1640            (centre.x - size.width / 2.0).round(),
1641            (centre.y - size.height / 2.0).round(),
1642        ),
1643        size,
1644    );
1645    renderer.fill_quad(
1646        quad(bounds, radius::SM, Some(style.grip_border)),
1647        Background::Color(style.grip),
1648    );
1649
1650    // Two rows of three dots, turned to follow the handle.
1651    let dot = 2.0;
1652    for column in [-1.0_f32, 1.0] {
1653        for row in [-1.0_f32, 0.0, 1.0] {
1654            let (dx, dy) = match axis {
1655                Axis::Horizontal => (column * 2.0, row * 4.0),
1656                Axis::Vertical => (row * 4.0, column * 2.0),
1657            };
1658            let position = Point::new(
1659                (centre.x + dx - dot / 2.0).round(),
1660                (centre.y + dy - dot / 2.0).round(),
1661            );
1662            renderer.fill_quad(
1663                quad(Rectangle::new(position, Size::new(dot, dot)), 1.0, None),
1664                Background::Color(style.grip_dots),
1665            );
1666        }
1667    }
1668}
1669
1670#[cfg(test)]
1671mod tests {
1672    use super::*;
1673    use crate::theme::{dark, light};
1674
1675    const TOTAL: f32 = 1000.0;
1676
1677    fn close(a: f32, b: f32) -> bool {
1678        (a - b).abs() < 0.05
1679    }
1680
1681    fn assert_sizes(actual: &[f32], expected: &[f32]) {
1682        assert_eq!(actual.len(), expected.len(), "{actual:?}");
1683        for (a, b) in actual.iter().zip(expected) {
1684            assert!(close(*a, *b), "{actual:?} != {expected:?}");
1685        }
1686    }
1687
1688    fn measured(panels: impl IntoIterator<Item = Panel>) -> State {
1689        let mut state = State::new(panels);
1690        let _ = state.update(Event::Measure(TOTAL));
1691        state
1692    }
1693
1694    fn pixels(state: &State) -> Vec<f32> {
1695        state.resolve(state.total())
1696    }
1697
1698    fn sidebar_and_content() -> [Panel; 2] {
1699        [
1700            panel(Extent::Fraction(0.3))
1701                .min(Extent::Pixels(200.0))
1702                .max(Extent::Fraction(0.5))
1703                .collapsible(true),
1704            panel(Extent::Fraction(0.7)).min(Extent::Pixels(300.0)),
1705        ]
1706    }
1707
1708    #[test]
1709    fn panel_defaults() {
1710        let panel = panel(Extent::Fraction(0.5));
1711        assert_eq!(panel.min, Extent::Pixels(0.0));
1712        assert_eq!(panel.max, Extent::Fraction(1.0));
1713        assert!(!panel.collapsible);
1714        assert_eq!(panel.collapsed_size, Extent::Pixels(0.0));
1715        assert_eq!(panel.collapse_below, None);
1716    }
1717
1718    #[test]
1719    fn extents_resolve_to_pixels() {
1720        assert_eq!(Extent::Fraction(0.25).resolve(800.0), 200.0);
1721        assert_eq!(Extent::Pixels(120.0).resolve(800.0), 120.0);
1722        assert_eq!(Extent::Pixels(-5.0).resolve(800.0), 0.0);
1723    }
1724
1725    #[test]
1726    fn limits_keep_max_above_min_and_threshold_between() {
1727        let limits = panel(Extent::Fraction(0.5))
1728            .min(Extent::Pixels(300.0))
1729            .max(Extent::Pixels(100.0))
1730            .collapsible(true)
1731            .collapsed_size(Extent::Pixels(40.0))
1732            .limits(TOTAL);
1733        assert_eq!(limits.min, 300.0);
1734        assert_eq!(limits.max, 300.0);
1735        assert_eq!(limits.collapsed, 40.0);
1736        assert_eq!(limits.threshold, 170.0);
1737
1738        let custom = panel(Extent::Fraction(0.5))
1739            .min(Extent::Pixels(200.0))
1740            .collapsible(true)
1741            .collapse_below(Extent::Pixels(500.0))
1742            .limits(TOTAL);
1743        assert_eq!(custom.threshold, 200.0, "never above the minimum");
1744    }
1745
1746    #[test]
1747    fn settle_clamps_and_snaps_at_the_threshold() {
1748        let plain = panel(Extent::Fraction(0.5))
1749            .min(Extent::Pixels(100.0))
1750            .max(Extent::Pixels(400.0));
1751        assert_eq!(plain.settle(50.0, TOTAL), 100.0);
1752        assert_eq!(plain.settle(250.0, TOTAL), 250.0);
1753        assert_eq!(plain.settle(900.0, TOTAL), 400.0);
1754
1755        let collapsible = plain.collapsible(true);
1756        assert_eq!(
1757            collapsible.settle(60.0, TOTAL),
1758            100.0,
1759            "above the threshold"
1760        );
1761        assert_eq!(collapsible.settle(49.0, TOTAL), 0.0, "below the threshold");
1762        assert!(collapsible.is_collapsed_at(0.0, TOTAL));
1763        assert!(!plain.is_collapsed_at(0.0, TOTAL));
1764    }
1765
1766    #[test]
1767    fn defaults_share_what_pixel_panels_leave() {
1768        let panels = [
1769            panel(Extent::Pixels(250.0)),
1770            panel(Extent::Fraction(1.0)),
1771            panel(Extent::Fraction(1.0)),
1772        ];
1773        assert_sizes(&defaults(&panels, TOTAL), &[250.0, 375.0, 375.0]);
1774    }
1775
1776    #[test]
1777    fn defaults_respect_limits_and_initial_collapse() {
1778        let panels = [
1779            panel(Extent::Fraction(0.1)).min(Extent::Pixels(200.0)),
1780            panel(Extent::Fraction(0.9)),
1781        ];
1782        assert_sizes(&defaults(&panels, TOTAL), &[200.0, 800.0]);
1783
1784        let collapsed = [
1785            panel(Extent::Fraction(0.3))
1786                .collapsible(true)
1787                .collapsed_size(Extent::Pixels(48.0))
1788                .min(Extent::Pixels(200.0))
1789                .collapsed(true),
1790            panel(Extent::Fraction(0.7)),
1791        ];
1792        assert_sizes(&defaults(&collapsed, TOTAL), &[48.0, 952.0]);
1793    }
1794
1795    #[test]
1796    fn fit_always_adds_up() {
1797        let panels = sidebar_and_content();
1798        for total in [1200.0, 600.0, 450.0, 320.0, 100.0] {
1799            let sizes = fit(
1800                &panels,
1801                &[false, false],
1802                vec![0.3 * total, 0.7 * total],
1803                total,
1804            );
1805            let sum: f32 = sizes.iter().sum();
1806            assert!(close(sum, total), "{total}: {sizes:?}");
1807        }
1808    }
1809
1810    #[test]
1811    fn fit_collapses_side_panels_when_minimums_do_not_fit() {
1812        let panels = sidebar_and_content();
1813        // 200 + 300 minimums do not fit in 360, so the sidebar collapses.
1814        let sizes = fit(&panels, &[false, false], vec![108.0, 252.0], 360.0);
1815        assert_sizes(&sizes, &[0.0, 360.0]);
1816    }
1817
1818    #[test]
1819    fn fit_scales_as_a_last_resort() {
1820        let panels = [
1821            panel(Extent::Fraction(0.5)).min(Extent::Pixels(300.0)),
1822            panel(Extent::Fraction(0.5)).min(Extent::Pixels(300.0)),
1823        ];
1824        let sizes = fit(&panels, &[false, false], vec![200.0, 200.0], 400.0);
1825        assert_sizes(&sizes, &[200.0, 200.0]);
1826    }
1827
1828    #[test]
1829    fn state_starts_from_defaults_once_measured() {
1830        let state = State::new(sidebar_and_content());
1831        assert_eq!(state.total(), 0.0);
1832        assert_sizes(&state.sizes(), &[0.3, 0.7]);
1833        assert_sizes(&state.resolve(TOTAL), &[300.0, 700.0]);
1834
1835        let state = measured(sidebar_and_content());
1836        assert_eq!(state.total(), TOTAL);
1837        assert_sizes(&state.sizes(), &[0.3, 0.7]);
1838    }
1839
1840    #[test]
1841    fn unmeasured_state_ignores_changes() {
1842        let mut state = State::new(sidebar_and_content());
1843        assert_eq!(
1844            state.update(Event::Resize {
1845                handle: 0,
1846                delta: 50.0
1847            }),
1848            None
1849        );
1850        assert_eq!(state.update(Event::Collapse(0)), None);
1851        let _ = state.update(Event::Measure(-1.0));
1852        let _ = state.update(Event::Measure(f32::INFINITY));
1853        assert_eq!(state.total(), 0.0);
1854    }
1855
1856    #[test]
1857    fn saved_sizes_round_trip_and_bad_ones_are_ignored() {
1858        let state = State::new(sidebar_and_content()).with_sizes([2.0, 6.0]);
1859        assert_sizes(&state.sizes(), &[0.25, 0.75]);
1860
1861        let wrong_length = State::new(sidebar_and_content()).with_sizes([1.0]);
1862        assert_sizes(&wrong_length.sizes(), &[0.3, 0.7]);
1863        let empty = State::new(sidebar_and_content()).with_sizes([0.0, 0.0]);
1864        assert_sizes(&empty.sizes(), &[0.3, 0.7]);
1865
1866        let collapsed = State::new(sidebar_and_content()).with_sizes([0.0, 1.0]);
1867        assert!(collapsed.is_collapsed(0));
1868    }
1869
1870    #[test]
1871    fn dragging_moves_the_handle_from_where_it_started() {
1872        let mut state = measured(sidebar_and_content());
1873        let _ = state.update(Event::DragStart {
1874            handle: 0,
1875            position: 300.0,
1876        });
1877        assert_eq!(state.dragging(), Some(0));
1878        assert_eq!(state.update(Event::Drag(340.0)), Some(Output::Resized));
1879        assert_sizes(&pixels(&state), &[340.0, 660.0]);
1880        assert_eq!(state.update(Event::Drag(320.0)), Some(Output::Resized));
1881        assert_sizes(&pixels(&state), &[320.0, 680.0]);
1882        let _ = state.update(Event::DragEnd);
1883        assert_eq!(state.dragging(), None);
1884        assert_eq!(state.update(Event::Drag(500.0)), None);
1885    }
1886
1887    #[test]
1888    fn drags_clamp_to_min_and_max() {
1889        let mut state = measured(sidebar_and_content());
1890        let _ = state.update(Event::DragStart {
1891            handle: 0,
1892            position: 300.0,
1893        });
1894        let _ = state.update(Event::Drag(900.0));
1895        assert_sizes(&pixels(&state), &[500.0, 500.0]);
1896        let _ = state.update(Event::Drag(160.0));
1897        assert_sizes(&pixels(&state), &[200.0, 800.0]);
1898    }
1899
1900    #[test]
1901    fn collapsible_panels_snap_shut_below_the_threshold_and_open_past_it() {
1902        let mut state = measured(sidebar_and_content());
1903        let _ = state.update(Event::DragStart {
1904            handle: 0,
1905            position: 300.0,
1906        });
1907        // Threshold is 100: halfway between collapsed (0) and min (200).
1908        let _ = state.update(Event::Drag(110.0));
1909        assert_sizes(&pixels(&state), &[200.0, 800.0]);
1910        assert_eq!(state.update(Event::Drag(90.0)), Some(Output::Collapsed(0)));
1911        assert!(state.is_collapsed(0));
1912        assert_sizes(&pixels(&state), &[0.0, 1000.0]);
1913        assert_eq!(state.update(Event::Drag(120.0)), Some(Output::Expanded(0)));
1914        assert_sizes(&pixels(&state), &[200.0, 800.0]);
1915        let _ = state.update(Event::DragEnd);
1916
1917        let _ = state.update(Event::Collapse(0));
1918        let _ = state.update(Event::DragStart {
1919            handle: 0,
1920            position: 0.0,
1921        });
1922        assert_eq!(
1923            state.update(Event::Drag(60.0)),
1924            None,
1925            "still below the threshold"
1926        );
1927        assert!(state.is_collapsed(0));
1928        assert_eq!(state.update(Event::Drag(101.0)), Some(Output::Expanded(0)));
1929        assert_sizes(&pixels(&state), &[200.0, 800.0]);
1930    }
1931
1932    #[test]
1933    fn shrinking_carries_on_past_a_panel_at_its_minimum() {
1934        let mut state = measured([
1935            panel(Extent::Fraction(0.4)),
1936            panel(Extent::Fraction(0.3)).min(Extent::Pixels(200.0)),
1937            panel(Extent::Fraction(0.3)).min(Extent::Pixels(100.0)),
1938        ]);
1939        let _ = state.update(Event::Resize {
1940            handle: 0,
1941            delta: 250.0,
1942        });
1943        assert_sizes(&pixels(&state), &[650.0, 200.0, 150.0]);
1944        let _ = state.update(Event::Resize {
1945            handle: 0,
1946            delta: 500.0,
1947        });
1948        assert_sizes(&pixels(&state), &[700.0, 200.0, 100.0]);
1949    }
1950
1951    #[test]
1952    fn growth_past_a_maximum_passes_outwards() {
1953        let origin = [200.0, 300.0, 500.0];
1954        let panels = [
1955            panel(Extent::Fraction(0.2)),
1956            panel(Extent::Fraction(0.3)).max(Extent::Pixels(350.0)),
1957            panel(Extent::Fraction(0.5)),
1958        ];
1959        let sizes = resize(&panels, &origin, 1, 100.0, TOTAL);
1960        assert_sizes(&sizes, &[250.0, 350.0, 400.0]);
1961    }
1962
1963    #[test]
1964    fn resize_ignores_bad_handles_and_zero_moves() {
1965        let panels = sidebar_and_content();
1966        let origin = [300.0, 700.0];
1967        assert_eq!(resize(&panels, &origin, 1, 50.0, TOTAL), origin);
1968        assert_eq!(resize(&panels, &origin, 0, 0.0, TOTAL), origin);
1969        assert_eq!(resize(&panels, &[300.0], 0, 50.0, TOTAL), [300.0]);
1970    }
1971
1972    #[test]
1973    fn toggle_collapses_and_restores_the_previous_size() {
1974        let mut state = measured(sidebar_and_content());
1975        let _ = state.update(Event::Resize {
1976            handle: 0,
1977            delta: 100.0,
1978        });
1979        assert_eq!(state.update(Event::Toggle(0)), Some(Output::Collapsed(0)));
1980        assert_sizes(&pixels(&state), &[0.0, 1000.0]);
1981        assert_eq!(state.update(Event::Collapse(0)), None);
1982        assert_eq!(state.update(Event::Toggle(0)), Some(Output::Expanded(0)));
1983        assert_sizes(&pixels(&state), &[400.0, 600.0]);
1984        assert_eq!(state.update(Event::Expand(0)), None);
1985    }
1986
1987    #[test]
1988    fn the_last_panel_collapses_through_the_handle_before_it() {
1989        let mut state = measured([
1990            panel(Extent::Fraction(0.6)),
1991            panel(Extent::Fraction(0.4))
1992                .collapsible(true)
1993                .collapsed_size(Extent::Pixels(40.0))
1994                .min(Extent::Pixels(200.0)),
1995        ]);
1996        assert_eq!(state.update(Event::Collapse(1)), Some(Output::Collapsed(1)));
1997        assert_sizes(&pixels(&state), &[960.0, 40.0]);
1998        assert_eq!(state.update(Event::Expand(1)), Some(Output::Expanded(1)));
1999        assert_sizes(&pixels(&state), &[600.0, 400.0]);
2000    }
2001
2002    #[test]
2003    fn panels_that_cannot_collapse_ignore_it() {
2004        let mut state = measured(sidebar_and_content());
2005        assert_eq!(state.update(Event::Collapse(1)), None);
2006        assert_eq!(state.update(Event::Toggle(7)), None);
2007    }
2008
2009    #[test]
2010    fn reset_puts_the_defaults_back() {
2011        let mut state = measured(sidebar_and_content());
2012        let _ = state.update(Event::Collapse(0));
2013        assert_eq!(state.update(Event::Reset), Some(Output::Resized));
2014        assert!(!state.is_collapsed(0));
2015        assert_sizes(&pixels(&state), &[300.0, 700.0]);
2016        assert_eq!(state.update(Event::Reset), None);
2017    }
2018
2019    #[test]
2020    fn keyboard_steps_follow_the_step_sizes() {
2021        let state = measured(sidebar_and_content());
2022        let resize = |action: Action| action.event(&state, 0);
2023        assert_eq!(
2024            resize(Action::Increase),
2025            Some(Event::Resize {
2026                handle: 0,
2027                delta: 50.0
2028            })
2029        );
2030        assert_eq!(
2031            resize(Action::Decrease),
2032            Some(Event::Resize {
2033                handle: 0,
2034                delta: -50.0
2035            })
2036        );
2037        assert_eq!(
2038            resize(Action::IncreaseMore),
2039            Some(Event::Resize {
2040                handle: 0,
2041                delta: 200.0
2042            })
2043        );
2044        assert_eq!(
2045            resize(Action::DecreaseMore),
2046            Some(Event::Resize {
2047                handle: 0,
2048                delta: -200.0
2049            })
2050        );
2051
2052        let pixels = measured(sidebar_and_content())
2053            .step(Extent::Pixels(10.0))
2054            .large_step(Extent::Pixels(40.0));
2055        assert_eq!(
2056            Action::Increase.event(&pixels, 0),
2057            Some(Event::Resize {
2058                handle: 0,
2059                delta: 10.0
2060            })
2061        );
2062        assert_eq!(
2063            Action::DecreaseMore.event(&pixels, 0),
2064            Some(Event::Resize {
2065                handle: 0,
2066                delta: -40.0
2067            })
2068        );
2069    }
2070
2071    #[test]
2072    fn home_and_end_go_to_min_and_max() {
2073        let mut state = measured(sidebar_and_content());
2074        let home = Action::Min.event(&state, 0);
2075        assert_eq!(
2076            home,
2077            Some(Event::Resize {
2078                handle: 0,
2079                delta: -100.0
2080            })
2081        );
2082        if let Some(event) = home {
2083            let _ = state.update(event);
2084        }
2085        assert_sizes(&pixels(&state), &[200.0, 800.0]);
2086        if let Some(event) = Action::Max.event(&state, 0) {
2087            let _ = state.update(event);
2088        }
2089        assert_sizes(&pixels(&state), &[500.0, 500.0]);
2090    }
2091
2092    #[test]
2093    fn toggle_and_release_actions() {
2094        let state = measured(sidebar_and_content());
2095        assert_eq!(Action::Toggle.event(&state, 0), Some(Event::Toggle(0)));
2096        assert_eq!(Action::Release.event(&state, 0), None);
2097        assert_eq!(Action::Increase.event(&state, 1), None, "no such handle");
2098
2099        let neither = measured([panel(Extent::Fraction(0.5)), panel(Extent::Fraction(0.5))]);
2100        assert_eq!(Action::Toggle.event(&neither, 0), None);
2101        let after = measured([
2102            panel(Extent::Fraction(0.5)),
2103            panel(Extent::Fraction(0.5)).collapsible(true),
2104        ]);
2105        assert_eq!(Action::Toggle.event(&after, 0), Some(Event::Toggle(1)));
2106    }
2107
2108    fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2109        let chord: Chord = chord.parse().unwrap();
2110        keymap.resolve(chord.key(), chord.modifiers())
2111    }
2112
2113    #[test]
2114    fn default_keymap_covers_every_action() {
2115        let keymap = default_keymap();
2116        assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Decrease));
2117        assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Decrease));
2118        assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Increase));
2119        assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Increase));
2120        assert_eq!(
2121            press(&keymap, "Shift+ArrowLeft"),
2122            Some(Action::DecreaseMore)
2123        );
2124        assert_eq!(
2125            press(&keymap, "Shift+ArrowDown"),
2126            Some(Action::IncreaseMore)
2127        );
2128        assert_eq!(press(&keymap, "Home"), Some(Action::Min));
2129        assert_eq!(press(&keymap, "End"), Some(Action::Max));
2130        assert_eq!(press(&keymap, "Enter"), Some(Action::Toggle));
2131        assert_eq!(press(&keymap, "Escape"), Some(Action::Release));
2132        for action in <Action as keys::Action>::ALL {
2133            assert!(!keymap.chords(action).is_empty(), "{action:?}");
2134        }
2135        assert_eq!(Keymap::<Action>::defaults(), keymap);
2136    }
2137
2138    #[test]
2139    fn keymap_overrides_and_unbinding() {
2140        let keymap = default_keymap()
2141            .unbind(&"Enter".parse().unwrap())
2142            .bind("Space".parse().unwrap(), Action::Toggle);
2143        assert_eq!(press(&keymap, "Enter"), None);
2144        assert_eq!(press(&keymap, "Space"), Some(Action::Toggle));
2145        assert!(keymap.clear().is_empty());
2146    }
2147
2148    #[test]
2149    fn stacking_only_turns_narrow_horizontal_groups() {
2150        let state = State::new(sidebar_and_content());
2151        let group: ResizablePanel<'_, ()> = resizable_panel(&state, []).stack_below(480.0);
2152        assert_eq!(group.axis_at(360.0), Axis::Vertical);
2153        assert_eq!(group.axis_at(720.0), Axis::Horizontal);
2154        let plain: ResizablePanel<'_, ()> = resizable_panel(&state, []);
2155        assert_eq!(plain.axis_at(360.0), Axis::Horizontal);
2156        let vertical: ResizablePanel<'_, ()> = resizable_panel(&state, [])
2157            .axis(Axis::Vertical)
2158            .stack_below(480.0);
2159        assert_eq!(vertical.axis_at(360.0), Axis::Vertical);
2160    }
2161
2162    #[test]
2163    fn builder_defaults() {
2164        let state = State::new(sidebar_and_content());
2165        let group: ResizablePanel<'_, ()> = resizable_panel(&state, []);
2166        assert_eq!(group.axis, Axis::Horizontal);
2167        assert!(!group.grip);
2168        assert_eq!(group.width, Length::Fill);
2169        assert_eq!(group.height, Length::Fill);
2170        assert!(group.on_event.is_none());
2171        assert_eq!(group.keymap, default_keymap());
2172    }
2173
2174    #[test]
2175    fn axes_pick_their_coordinate_and_cursor() {
2176        let point = Point::new(3.0, 7.0);
2177        assert_eq!(Axis::Horizontal.along(point), 3.0);
2178        assert_eq!(Axis::Vertical.along(point), 7.0);
2179        assert_eq!(
2180            Axis::Horizontal.cursor(),
2181            mouse::Interaction::ResizingColumn
2182        );
2183        assert_eq!(Axis::Vertical.cursor(), mouse::Interaction::ResizingRow);
2184    }
2185
2186    #[test]
2187    fn a_press_focuses_quietly_and_keys_or_operations_show_it() {
2188        let mut memory = Memory {
2189            handles: vec![HandleFocus::default(); 2],
2190            ..Memory::default()
2191        };
2192        memory.focus(Some(1));
2193        assert_eq!(memory.focused(), Some(1));
2194        assert_eq!(memory.visible(), None);
2195        memory.show_focus();
2196        assert_eq!(memory.visible(), Some(1));
2197        memory.focus(None);
2198        assert_eq!(memory.focused(), None);
2199
2200        memory.handles[0].focus();
2201        assert_eq!(memory.visible(), Some(0), "focus operations show it");
2202        memory.handles[0].unfocus();
2203        assert_eq!(memory.focused(), None);
2204    }
2205
2206    #[test]
2207    fn saved_sizes_below_the_threshold_restore_collapsed() {
2208        let panels = [
2209            panel(Extent::Pixels(240.0))
2210                .min(Extent::Pixels(180.0))
2211                .collapsible(true)
2212                .collapsed_size(Extent::Pixels(48.0)),
2213            panel(Extent::Fraction(1.0)),
2214        ];
2215        let mut state = State::new(panels).with_sizes([0.048, 0.952]);
2216        let _ = state.update(Event::Measure(TOTAL));
2217        assert!(state.is_collapsed(0));
2218        assert_sizes(&pixels(&state), &[48.0, 952.0]);
2219
2220        // A wider window keeps the rail at its pixel size.
2221        let _ = state.update(Event::Measure(2000.0));
2222        assert_sizes(&state.resolve(2000.0), &[48.0, 1952.0]);
2223        assert_eq!(state.update(Event::Expand(0)), Some(Output::Expanded(0)));
2224        assert_sizes(&state.resolve(2000.0), &[240.0, 1760.0]);
2225    }
2226
2227    #[test]
2228    fn handles_highlight_on_hover_drag_and_focus() {
2229        for theme in [light(), dark()] {
2230            let tokens = Tokens::of(&theme);
2231            let idle = handle_style(&tokens, HandleStatus::Idle);
2232            assert_eq!(idle.line, tokens.border);
2233            assert_eq!(idle.highlight, None);
2234            assert_eq!(
2235                handle_style(&tokens, HandleStatus::Disabled).highlight,
2236                None
2237            );
2238            assert_eq!(
2239                handle_style(&tokens, HandleStatus::Hovered).highlight,
2240                Some(tokens.border)
2241            );
2242            for status in [HandleStatus::Dragged, HandleStatus::Focused] {
2243                let style = handle_style(&tokens, status);
2244                assert_eq!(style.highlight, Some(tokens.ring));
2245                assert_eq!(style.grip_border, tokens.ring);
2246            }
2247        }
2248    }
2249}