Skip to main content

gpui_kit_material/components/
pane_logic.rs

1//! The rules behind the list-detail and supporting pane layouts, as pure
2//! functions: which panes show, where they sit and how they move.
3//!
4//! Source: only m3e lists canonical layouts (the Figma kit and md3.io do not).
5//! This is a port of `packages/m3e-motion/src/three-pane-scaffold.ts`, itself a
6//! port of androidx `ThreePaneScaffoldValue.kt`, `ThreePaneScaffold.kt`,
7//! `PaneMotion.kt`, `PaneExpansionState.kt` and
8//! `ThreePaneScaffoldNavigator.kt` at
9//! `19991f2c0858aadfcb442ce02b2c8e4743c96898`. Levitated panes and hinges are
10//! not ported, as in m3e. Lengths are dp; geometry is physical left to right,
11//! so a right-to-left caller passes the pane order mirrored.
12//!
13//! Copyright 2023 The Android Open Source Project. Licensed under the Apache
14//! License, Version 2.0: see `LICENSE-androidx` in this directory.
15use super::adaptive::{WindowHeightClass, WindowSizeClass, WindowWidthClass};
16
17/// A pane's role. List-detail: list = secondary, detail = primary. Supporting
18/// pane: main = primary, supporting = secondary. Extra is tertiary in both.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum PaneRole {
21    /// The highest-priority pane: it takes the width left over.
22    Primary,
23    /// The second pane to get room.
24    Secondary,
25    /// The extra pane, last to get room.
26    Tertiary,
27}
28
29impl PaneRole {
30    /// Every role, by priority.
31    pub const ALL: [Self; 3] = [Self::Primary, Self::Secondary, Self::Tertiary];
32
33    /// This role's position in [`Self::ALL`].
34    pub const fn index(self) -> usize {
35        self as usize
36    }
37
38    const fn priority(self) -> u8 {
39        match self {
40            Self::Primary => 10,
41            Self::Secondary => 5,
42            Self::Tertiary => 1,
43        }
44    }
45}
46
47/// How a pane adapts when there is no partition for it.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum AdaptStrategy {
50    /// It hides.
51    Hide,
52    /// It stacks under the given pane, where the height allows.
53    Reflow(PaneRole),
54}
55
56/// The adapt strategy of each role, in [`PaneRole::ALL`] order.
57pub type AdaptStrategies = [AdaptStrategy; 3];
58
59/// List-detail panes all hide.
60pub const LIST_DETAIL_STRATEGIES: AdaptStrategies = [AdaptStrategy::Hide; 3];
61/// The supporting pane reflows under the main pane; the others hide.
62pub const SUPPORTING_PANE_STRATEGIES: AdaptStrategies = [
63    AdaptStrategy::Hide,
64    AdaptStrategy::Reflow(PaneRole::Primary),
65    AdaptStrategy::Hide,
66];
67/// List-detail's start-to-end order: list, detail, extra.
68pub const LIST_DETAIL_ORDER: [PaneRole; 3] =
69    [PaneRole::Secondary, PaneRole::Primary, PaneRole::Tertiary];
70/// Supporting pane's start-to-end order: main, supporting, extra.
71pub const SUPPORTING_PANE_ORDER: [PaneRole; 3] =
72    [PaneRole::Primary, PaneRole::Secondary, PaneRole::Tertiary];
73
74/// What a pane is at the moment.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum PaneAdaptedValue {
77    /// In its own partition.
78    Expanded,
79    /// Stacked under another pane.
80    Reflowed,
81    /// Not shown.
82    Hidden,
83}
84
85/// The state of all three panes.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct ThreePaneValue(pub [PaneAdaptedValue; 3]);
88
89impl ThreePaneValue {
90    /// A role's value.
91    pub const fn get(&self, role: PaneRole) -> PaneAdaptedValue {
92        self.0[role.index()]
93    }
94}
95
96/// Which panes get a partition: the latest destination first, then earlier
97/// ones, then the rest by priority. With one partition and room for two stacked
98/// panes, the first pane that reflows goes under its expanded anchor.
99pub fn three_pane_value(
100    max_horizontal: usize,
101    max_vertical: usize,
102    strategies: &AdaptStrategies,
103    history: impl DoubleEndedIterator<Item = PaneRole>,
104) -> ThreePaneValue {
105    let mut expanded = 0;
106    let mut values: [Option<PaneAdaptedValue>; 3] = [None; 3];
107    let mut check_reflow = max_horizontal == 1
108        && max_vertical > 1
109        && strategies.iter().any(|s| *s != AdaptStrategy::Hide);
110    for pane in history.rev().chain(PaneRole::ALL) {
111        let available = expanded < max_horizontal;
112        if !available && !check_reflow {
113            break;
114        }
115        if values[pane.index()].is_some() {
116            continue;
117        }
118        let mut reflowed = None;
119        let mut anchor = pane;
120        let mut anchor_value = None;
121        if check_reflow && let AdaptStrategy::Reflow(under) = strategies[pane.index()] {
122            reflowed = Some(pane);
123            anchor = under;
124            anchor_value = values[anchor.index()];
125        }
126        match anchor_value {
127            None => {
128                if !available {
129                    continue;
130                }
131                values[anchor.index()] = Some(PaneAdaptedValue::Expanded);
132                expanded += 1;
133            }
134            Some(PaneAdaptedValue::Expanded) => {}
135            Some(_) => continue,
136        }
137        if let Some(pane) = reflowed {
138            values[pane.index()] = Some(PaneAdaptedValue::Reflowed);
139            check_reflow = false;
140        }
141    }
142    ThreePaneValue(values.map(|value| value.unwrap_or(PaneAdaptedValue::Hidden)))
143}
144
145/// How many panes may sit side by side.
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum PaneRule {
148    /// m3.material.io's Breakpoints, through m3e's default: 1 / 1 / 2 / 2 / 3
149    /// panes by width class.
150    #[default]
151    Recommended,
152    /// Compose's `calculatePaneScaffoldDirective`: 1 / 1 / 2 / 3 / 3.
153    Compose,
154}
155
156/// The pane layout a size class calls for.
157#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct PaneDirective {
159    /// Panes side by side, at most.
160    pub max_horizontal: usize,
161    /// The space between side-by-side panes.
162    pub horizontal_spacer: f32,
163    /// Panes stacked, at most.
164    pub max_vertical: usize,
165    /// The space between stacked panes.
166    pub vertical_spacer: f32,
167    /// A pane's default preferred width.
168    pub preferred_width: f32,
169    /// A pane's default preferred height.
170    pub preferred_height: f32,
171}
172
173/// The directive for a size class (Compose `PaneScaffoldDirective.kt`).
174pub fn pane_directive(size: WindowSizeClass, rule: PaneRule) -> PaneDirective {
175    use WindowWidthClass::*;
176    let (partitions, horizontal_spacer, preferred_width) = match size.width {
177        Compact | Medium => (1, 0., 360.),
178        Expanded => (2, 24., 360.),
179        Large | ExtraLarge => (3, 24., 412.),
180    };
181    let stacked = partitions == 1 && size.height == WindowHeightClass::Expanded;
182    let recommended = match size.width {
183        Compact | Medium => 1,
184        Expanded | Large => 2,
185        ExtraLarge => 3,
186    };
187    PaneDirective {
188        max_horizontal: match rule {
189            PaneRule::Recommended => partitions.min(recommended),
190            PaneRule::Compose => partitions,
191        },
192        horizontal_spacer,
193        max_vertical: if stacked { 2 } else { 1 },
194        vertical_spacer: if stacked { 24. } else { 0. },
195        preferred_width,
196        preferred_height: 420.,
197    }
198}
199
200/// What "back" pops to.
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
202pub enum BackNavigationBehavior {
203    /// The previous destination.
204    PopLatest,
205    /// The latest destination that changes which panes show.
206    #[default]
207    PopUntilScaffoldValueChange,
208    /// The latest destination with another pane.
209    PopUntilCurrentDestinationChange,
210    /// The latest destination with other content or other panes.
211    PopUntilContentChange,
212}
213
214/// One entry of a navigator's history.
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct PaneDestination<K> {
217    /// The pane navigated to.
218    pub pane: PaneRole,
219    /// What it shows, if the caller keys its content.
220    pub content_key: Option<K>,
221}
222
223/// A destination history and the pane value it gives. Own it in an `Entity`
224/// and hand it to a [`super::PaneScaffold`], which keeps its directive current.
225///
226/// ```
227/// use gpui_kit_material::prelude::*;
228///
229/// // One pane at a time: the list, then the detail, then back.
230/// let mut navigator = PaneNavigator::<u32>::list_detail();
231/// assert!(navigator.is_expanded(PaneRole::Secondary));
232/// navigator.navigate_to(PaneRole::Primary, Some(7));
233/// assert!(navigator.is_expanded(PaneRole::Primary));
234/// assert_eq!(navigator.content_key(), Some(&7));
235/// navigator.navigate_back(BackNavigationBehavior::default());
236/// assert!(navigator.is_expanded(PaneRole::Secondary));
237/// ```
238#[derive(Clone, Debug)]
239pub struct PaneNavigator<K> {
240    history: Vec<PaneDestination<K>>,
241    directive: PaneDirective,
242    strategies: AdaptStrategies,
243}
244
245impl<K: Clone + PartialEq> PaneNavigator<K> {
246    fn new(first: PaneRole, strategies: AdaptStrategies) -> Self {
247        Self {
248            history: vec![PaneDestination {
249                pane: first,
250                content_key: None,
251            }],
252            directive: pane_directive(
253                WindowSizeClass {
254                    width: WindowWidthClass::Compact,
255                    height: WindowHeightClass::Medium,
256                },
257                PaneRule::Recommended,
258            ),
259            strategies,
260        }
261    }
262
263    /// A list-detail navigator, starting on the list.
264    pub fn list_detail() -> Self {
265        Self::new(PaneRole::Secondary, LIST_DETAIL_STRATEGIES)
266    }
267
268    /// A supporting pane navigator, starting on the main pane.
269    pub fn supporting_pane() -> Self {
270        Self::new(PaneRole::Primary, SUPPORTING_PANE_STRATEGIES)
271    }
272
273    /// The destination history, oldest first.
274    pub fn history(&self) -> &[PaneDestination<K>] {
275        &self.history
276    }
277
278    /// The latest destination.
279    pub fn current(&self) -> Option<&PaneDestination<K>> {
280        self.history.last()
281    }
282
283    /// The latest content key in the history, for what a detail pane shows.
284    pub fn content_key(&self) -> Option<&K> {
285        self.history
286            .iter()
287            .rev()
288            .find_map(|destination| destination.content_key.as_ref())
289    }
290
291    /// The directive in force.
292    pub fn directive(&self) -> PaneDirective {
293        self.directive
294    }
295
296    /// Apply a directive; true when it changed.
297    pub fn set_directive(&mut self, directive: PaneDirective) -> bool {
298        let changed = self.directive != directive;
299        self.directive = directive;
300        changed
301    }
302
303    /// The adapt strategies in force.
304    pub fn strategies(&self) -> &AdaptStrategies {
305        &self.strategies
306    }
307
308    /// The panes' current value.
309    pub fn value(&self) -> ThreePaneValue {
310        self.value_at(self.history.len())
311    }
312
313    fn value_at(&self, length: usize) -> ThreePaneValue {
314        three_pane_value(
315            self.directive.max_horizontal,
316            self.directive.max_vertical,
317            &self.strategies,
318            self.history[..length].iter().map(|item| item.pane),
319        )
320    }
321
322    /// Whether a pane has its own partition, as a list checks before marking
323    /// its selected row.
324    pub fn is_expanded(&self, pane: PaneRole) -> bool {
325        self.value().get(pane) == PaneAdaptedValue::Expanded
326    }
327
328    /// Navigate to a pane.
329    pub fn navigate_to(&mut self, pane: PaneRole, content_key: Option<K>) {
330        self.history.push(PaneDestination { pane, content_key });
331    }
332
333    fn previous_index(&self, behavior: BackNavigationBehavior) -> Option<usize> {
334        if self.history.len() <= 1 {
335            return None;
336        }
337        let current = self.history.last()?;
338        let value = self.value();
339        (0..self.history.len() - 1).rev().find(|&index| {
340            let item = &self.history[index];
341            let value_differs = self.value_at(index + 1) != value;
342            match behavior {
343                BackNavigationBehavior::PopLatest => true,
344                BackNavigationBehavior::PopUntilScaffoldValueChange => value_differs,
345                BackNavigationBehavior::PopUntilCurrentDestinationChange => {
346                    item.pane != current.pane
347                }
348                BackNavigationBehavior::PopUntilContentChange => {
349                    item.content_key != current.content_key || value_differs
350                }
351            }
352        })
353    }
354
355    /// Whether there is a destination to go back to.
356    pub fn can_navigate_back(&self, behavior: BackNavigationBehavior) -> bool {
357        self.previous_index(behavior).is_some()
358    }
359
360    /// Go back; false when there was nothing to go back to, which also clears
361    /// the history, as Compose does.
362    pub fn navigate_back(&mut self, behavior: BackNavigationBehavior) -> bool {
363        match self.previous_index(behavior) {
364            Some(index) => {
365                self.history.truncate(index + 1);
366                true
367            }
368            None => {
369                self.history.clear();
370                false
371            }
372        }
373    }
374}
375
376/// A pane's bounds in its scaffold.
377#[derive(Clone, Copy, Debug, Default, PartialEq)]
378pub struct PaneRect {
379    /// The distance from the scaffold's left edge.
380    pub x: f32,
381    /// The distance from the scaffold's top edge.
382    pub y: f32,
383    /// The width.
384    pub width: f32,
385    /// The height.
386    pub height: f32,
387}
388
389impl PaneRect {
390    /// The right edge.
391    pub fn right(&self) -> f32 {
392        self.x + self.width
393    }
394
395    /// The rectangle `t` of the way from `self` to `to`.
396    pub fn lerp(&self, to: &Self, t: f32) -> Self {
397        let mix = |a: f32, b: f32| a + (b - a) * t;
398        Self {
399            x: mix(self.x, to.x),
400            y: mix(self.y, to.y),
401            width: mix(self.width, to.width).max(0.),
402            height: mix(self.height, to.height).max(0.),
403        }
404    }
405}
406
407/// Everything a pane layout depends on.
408#[derive(Clone, Copy, Debug)]
409pub struct PaneLayoutInput<'a> {
410    /// The scaffold's width.
411    pub width: f32,
412    /// The scaffold's height.
413    pub height: f32,
414    /// The directive in force.
415    pub directive: PaneDirective,
416    /// The panes' value.
417    pub value: ThreePaneValue,
418    /// The panes from left to right.
419    pub order: &'a [PaneRole; 3],
420    /// The adapt strategies in force.
421    pub strategies: &'a AdaptStrategies,
422    /// Whether each role has a pane, in [`PaneRole::ALL`] order.
423    pub present: [bool; 3],
424    /// Each role's preferred width, where it overrides the directive's.
425    pub preferred_widths: [Option<f32>; 3],
426    /// The handle's center while it is dragged.
427    pub dragging_offset: Option<f32>,
428    /// The first pane's width at a resting split.
429    pub first_pane_width: Option<f32>,
430}
431
432/// The panes' bounds.
433#[derive(Clone, Copy, Debug, Default, PartialEq)]
434pub struct PaneLayout {
435    /// Each role's bounds where it shows, in [`PaneRole::ALL`] order. A pane
436    /// collapsed by a split has zero width.
437    pub panes: [Option<PaneRect>; 3],
438    /// The drag handle's center when exactly two panes are expanded.
439    pub handle_offset: Option<f32>,
440}
441
442#[derive(Clone, Copy)]
443struct Measuring {
444    role: PaneRole,
445    width: f32,
446    height: f32,
447    bounds: Option<PaneRect>,
448}
449
450/// Up to three panes, so a layout allocates nothing.
451#[derive(Clone, Copy)]
452struct Panes {
453    items: [Measuring; 3],
454    len: usize,
455}
456
457impl std::ops::Deref for Panes {
458    type Target = [Measuring];
459    fn deref(&self) -> &[Measuring] {
460        &self.items[..self.len]
461    }
462}
463
464impl std::ops::DerefMut for Panes {
465    fn deref_mut(&mut self) -> &mut [Measuring] {
466        &mut self.items[..self.len]
467    }
468}
469
470/// Place the panes (Compose `ThreePaneContentMeasurePolicy`, without hinges).
471pub fn three_pane_layout(input: &PaneLayoutInput) -> PaneLayout {
472    let (w, h) = (input.width, input.height);
473    let directive = input.directive;
474    let select = |wanted: PaneAdaptedValue| {
475        let empty = Measuring {
476            role: PaneRole::Primary,
477            width: 0.,
478            height: 0.,
479            bounds: None,
480        };
481        let mut panes = Panes {
482            items: [empty; 3],
483            len: 0,
484        };
485        for role in input.order {
486            if input.present[role.index()] && input.value.get(*role) == wanted {
487                panes.items[panes.len] = Measuring {
488                    role: *role,
489                    width: input.preferred_widths[role.index()]
490                        .unwrap_or(directive.preferred_width),
491                    height: directive.preferred_height,
492                    bounds: None,
493                };
494                panes.len += 1;
495            }
496        }
497        panes
498    };
499    let mut expanded = select(PaneAdaptedValue::Expanded);
500    let mut reflowed = select(PaneAdaptedValue::Reflowed);
501    let spacer = directive.horizontal_spacer;
502    let v_spacer = directive.vertical_spacer;
503    let outer = PaneRect {
504        x: 0.,
505        y: 0.,
506        width: w,
507        height: h,
508    };
509    let between = |left: f32, right: f32| PaneRect {
510        x: left,
511        y: 0.,
512        width: right - left,
513        height: h,
514    };
515    let in_partition = |partition: PaneRect, pane: &mut Measuring, flowed: &mut Panes| {
516        let under = flowed
517            .first()
518            .and_then(|flowed| match input.strategies[flowed.role.index()] {
519                AdaptStrategy::Reflow(anchor) => Some(anchor),
520                AdaptStrategy::Hide => None,
521            });
522        if under == Some(pane.role) {
523            let flowed = &mut flowed[0];
524            let available = partition.height - v_spacer;
525            pane.height = (available - flowed.height).max((available / 2.).trunc());
526            flowed.height = available - pane.height;
527            flowed.bounds = Some(PaneRect {
528                x: partition.x,
529                y: partition.y + pane.height + v_spacer,
530                width: partition.width,
531                height: partition.height - pane.height - v_spacer,
532            });
533        } else {
534            pane.height = partition.height;
535        }
536        pane.bounds = Some(PaneRect {
537            height: pane.height,
538            ..partition
539        });
540    };
541
542    let two = expanded.len() == 2;
543    if two && (input.dragging_offset.is_some() || input.first_pane_width.is_some()) {
544        let half = (spacer / 2.).trunc();
545        let (head, tail) = expanded.split_at_mut(1);
546        let (first, second) = (&mut head[0], &mut tail[0]);
547        if let Some(offset) = input.dragging_offset {
548            if offset <= half {
549                let partition = PaneRect {
550                    x: offset * 2.,
551                    width: w - offset * 2.,
552                    ..outer
553                };
554                in_partition(partition, second, &mut reflowed);
555            } else if offset >= w - half {
556                let partition = PaneRect {
557                    width: offset * 2. - w,
558                    ..outer
559                };
560                in_partition(partition, first, &mut reflowed);
561            } else {
562                in_partition(between(0., offset - half), first, &mut reflowed);
563                in_partition(between(offset + half, w), second, &mut reflowed);
564            }
565        } else if let Some(resting) = input.first_pane_width {
566            let width = resting.clamp(0., w.max(0.));
567            if width == 0. {
568                in_partition(outer, second, &mut reflowed);
569            } else if width >= w - spacer {
570                in_partition(outer, first, &mut reflowed);
571            } else {
572                in_partition(between(0., width), first, &mut reflowed);
573                in_partition(between(width + spacer, w), second, &mut reflowed);
574            }
575        }
576    } else if !expanded.is_empty() {
577        let allocatable = w - (expanded.len() as f32 - 1.) * spacer;
578        let total: f32 = expanded.iter().map(|pane| pane.width).sum();
579        if allocatable > total {
580            let mut top = 0;
581            for (index, pane) in expanded.iter().enumerate() {
582                if pane.role.priority() > expanded[top].role.priority() {
583                    top = index;
584                }
585            }
586            expanded[top].width += allocatable - total;
587        } else if allocatable < total && total > 0. {
588            let scale = allocatable.max(0.) / total;
589            for pane in expanded.iter_mut() {
590                pane.width = (pane.width * scale).trunc();
591            }
592        }
593        let mut x = 0.;
594        for pane in expanded.iter_mut() {
595            let partition = PaneRect {
596                x,
597                y: 0.,
598                width: pane.width,
599                height: h,
600            };
601            in_partition(partition, pane, &mut reflowed);
602            x += pane.width + spacer;
603        }
604    }
605
606    let handle_offset = two.then(|| {
607        if let Some(offset) = input.dragging_offset {
608            offset
609        } else {
610            match (expanded[0].bounds, expanded[1].bounds) {
611                (Some(first), Some(second)) => ((first.right() + second.x) / 2.).trunc(),
612                (Some(first), None) => first.right(),
613                _ => 0.,
614            }
615        }
616    });
617    let mut panes = [None; 3];
618    for pane in expanded.iter().chain(reflowed.iter()) {
619        panes[pane.role.index()] = Some(pane.bounds.unwrap_or_default());
620    }
621    PaneLayout {
622        panes,
623        handle_offset,
624    }
625}
626
627/// How a pane moves when the value changes (Compose `PaneMotion`).
628#[derive(Clone, Copy, Debug, PartialEq, Eq)]
629pub enum PaneMotion {
630    /// It stays hidden.
631    None,
632    /// It stays shown, moving to its new bounds.
633    AnimateBounds,
634    /// It slides in from the left.
635    EnterFromLeft,
636    /// It slides in from the right.
637    EnterFromRight,
638    /// It slides in from the left, a little after the others.
639    EnterFromLeftDelayed,
640    /// It slides in from the right, a little after the others.
641    EnterFromRightDelayed,
642    /// It slides out to the left.
643    ExitToLeft,
644    /// It slides out to the right.
645    ExitToRight,
646    /// It grows from nothing between its neighbours.
647    EnterWithExpand,
648    /// It shrinks to nothing between its neighbours.
649    ExitWithShrink,
650}
651
652#[derive(Clone, Copy, PartialEq, Eq)]
653enum MotionType {
654    Hidden,
655    Exiting,
656    Entering,
657    Shown,
658}
659
660impl PaneMotion {
661    fn kind(self) -> MotionType {
662        match self {
663            Self::None => MotionType::Hidden,
664            Self::AnimateBounds => MotionType::Shown,
665            Self::EnterFromLeft
666            | Self::EnterFromRight
667            | Self::EnterFromLeftDelayed
668            | Self::EnterFromRightDelayed
669            | Self::EnterWithExpand => MotionType::Entering,
670            Self::ExitToLeft | Self::ExitToRight | Self::ExitWithShrink => MotionType::Exiting,
671        }
672    }
673
674    /// Whether this motion starts after the others (Compose's delayed spring).
675    pub const fn delayed(self) -> bool {
676        matches!(
677            self,
678            Self::EnterFromLeftDelayed | Self::EnterFromRightDelayed
679        )
680    }
681}
682
683/// The motion of each role, in [`PaneRole::ALL`] order, for a change of value.
684pub fn pane_motions(
685    previous: ThreePaneValue,
686    current: ThreePaneValue,
687    order: &[PaneRole; 3],
688) -> [PaneMotion; 3] {
689    const N: isize = 3;
690    let shows = |value: PaneAdaptedValue| value != PaneAdaptedValue::Hidden;
691    let types = order.map(
692        |role| match (shows(previous.get(role)), shows(current.get(role))) {
693            (true, true) => MotionType::Shown,
694            (true, false) => MotionType::Exiting,
695            (false, true) => MotionType::Entering,
696            (false, false) => MotionType::Hidden,
697        },
698    );
699    let mut motions = [PaneMotion::None; 3];
700    let (mut first_shown, mut first_entering) = (N, N);
701    let (mut last_shown, mut last_entering) = (-1, -1);
702    for (i, kind) in types.iter().enumerate() {
703        let i = i as isize;
704        match kind {
705            MotionType::Shown => {
706                first_shown = first_shown.min(i);
707                last_shown = last_shown.max(i);
708                motions[i as usize] = PaneMotion::AnimateBounds;
709            }
710            MotionType::Entering => {
711                first_entering = first_entering.min(i);
712                last_entering = last_entering.max(i);
713            }
714            _ => {}
715        }
716    }
717    let (mut exit_right, mut exit_left) = (false, false);
718    let (mut first_exit_right, mut last_exit_left) = (N, -1);
719    for (index, kind) in types.iter().enumerate() {
720        if *kind != MotionType::Exiting {
721            continue;
722        }
723        let i = index as isize;
724        let (shown_left, entering_left) = (first_shown < i, first_entering < i);
725        let (shown_right, entering_right) = (last_shown > i, last_entering > i);
726        let to_right =
727            (!shown_right && !entering_right) || ((shown_left || entering_left) && !shown_right);
728        let to_left = !to_right && ((!shown_left && !entering_left) || !shown_left);
729        if to_right {
730            exit_right = true;
731            first_exit_right = first_exit_right.min(i);
732            motions[index] = PaneMotion::ExitToRight;
733        } else if to_left {
734            exit_left = true;
735            last_exit_left = last_exit_left.max(i);
736            motions[index] = PaneMotion::ExitToLeft;
737        } else {
738            motions[index] = PaneMotion::ExitWithShrink;
739        }
740    }
741    for (index, kind) in types.iter().enumerate() {
742        if *kind != MotionType::Entering {
743            continue;
744        }
745        let i = index as isize;
746        let free_right = last_shown <= i && last_exit_left <= i;
747        let free_left = first_shown >= i && first_exit_right >= i;
748        motions[index] = if free_right && !exit_right {
749            PaneMotion::EnterFromRight
750        } else if free_left && !exit_left {
751            PaneMotion::EnterFromLeft
752        } else if free_right {
753            PaneMotion::EnterFromRightDelayed
754        } else if free_left {
755            PaneMotion::EnterFromLeftDelayed
756        } else {
757            PaneMotion::EnterWithExpand
758        };
759    }
760    let mut out = [PaneMotion::None; 3];
761    for (index, role) in order.iter().enumerate() {
762        out[role.index()] = motions[index];
763    }
764    out
765}
766
767/// Where a pane moves from and to.
768#[derive(Clone, Copy, Debug, PartialEq)]
769pub struct PaneTransition {
770    /// The motion this realizes.
771    pub motion: PaneMotion,
772    /// The bounds it starts at.
773    pub from: PaneRect,
774    /// The bounds it ends at.
775    pub to: PaneRect,
776}
777
778/// The transition of each role, in [`PaneRole::ALL`] order. `origin` holds the
779/// bounds before the change and `target` those after it.
780pub fn pane_transitions(
781    motions: &[PaneMotion; 3],
782    order: &[PaneRole; 3],
783    origin: &[Option<PaneRect>; 3],
784    target: &[Option<PaneRect>; 3],
785    scaffold_width: f32,
786) -> [Option<PaneTransition>; 3] {
787    let t = |r: PaneRole| target[r.index()].or(origin[r.index()]).unwrap_or_default();
788    let o = |r: PaneRole| origin[r.index()].or(target[r.index()]).unwrap_or_default();
789    let kind = |r: PaneRole| motions[r.index()].kind();
790    let shown = |r: PaneRole, other: MotionType| kind(r) == MotionType::Shown || kind(r) == other;
791
792    let slide_in_left = || {
793        let mut previous = None;
794        for r in order.iter().rev().copied() {
795            if matches!(
796                motions[r.index()],
797                PaneMotion::EnterFromLeft | PaneMotion::EnterFromLeftDelayed
798            ) {
799                return -previous.map_or(t(r).right(), |p: PaneRole| t(p).x);
800            }
801            if shown(r, MotionType::Entering) {
802                previous = Some(r);
803            }
804        }
805        0.
806    };
807    let slide_in_right = || {
808        let mut previous = None;
809        for r in order.iter().copied() {
810            if matches!(
811                motions[r.index()],
812                PaneMotion::EnterFromRight | PaneMotion::EnterFromRightDelayed
813            ) {
814                return scaffold_width - previous.map_or(t(r).x, |p: PaneRole| t(p).right());
815            }
816            if shown(r, MotionType::Entering) {
817                previous = Some(r);
818            }
819        }
820        0.
821    };
822    let slide_out_left = || {
823        let mut previous = None;
824        for r in order.iter().rev().copied() {
825            if motions[r.index()] == PaneMotion::ExitToLeft {
826                return -previous.map_or(o(r).right(), |p: PaneRole| o(p).x);
827            }
828            if shown(r, MotionType::Exiting) {
829                previous = Some(r);
830            }
831        }
832        0.
833    };
834    let slide_out_right = || {
835        let mut previous = None;
836        for r in order.iter().copied() {
837            if motions[r.index()] == PaneMotion::ExitToRight {
838                return scaffold_width - previous.map_or(o(r).x, |p: PaneRole| o(p).right());
839            }
840            if shown(r, MotionType::Exiting) {
841                previous = Some(r);
842            }
843        }
844        0.
845    };
846    let left_edge = |role: PaneRole, other: MotionType, rect: &dyn Fn(PaneRole) -> PaneRect| {
847        let mut edge = 0.;
848        for r in order.iter().copied() {
849            if r == role {
850                return edge;
851            }
852            if shown(r, other) {
853                edge = rect(r).right();
854            }
855        }
856        edge
857    };
858    let shift = |rect: PaneRect, dx: f32| PaneRect {
859        x: rect.x + dx,
860        ..rect
861    };
862
863    let mut out = [None; 3];
864    for r in order.iter().copied() {
865        let motion = motions[r.index()];
866        let (to, from) = (t(r), o(r));
867        out[r.index()] = match motion {
868            PaneMotion::None => None,
869            PaneMotion::AnimateBounds => Some(PaneTransition { motion, from, to }),
870            PaneMotion::EnterFromLeft | PaneMotion::EnterFromLeftDelayed => Some(PaneTransition {
871                motion,
872                from: shift(to, slide_in_left()),
873                to,
874            }),
875            PaneMotion::EnterFromRight | PaneMotion::EnterFromRightDelayed => {
876                Some(PaneTransition {
877                    motion,
878                    from: shift(to, slide_in_right()),
879                    to,
880                })
881            }
882            PaneMotion::EnterWithExpand => Some(PaneTransition {
883                motion,
884                from: PaneRect {
885                    x: left_edge(r, MotionType::Exiting, &o),
886                    width: 0.,
887                    ..to
888                },
889                to,
890            }),
891            PaneMotion::ExitToLeft => Some(PaneTransition {
892                motion,
893                from,
894                to: shift(from, slide_out_left()),
895            }),
896            PaneMotion::ExitToRight => Some(PaneTransition {
897                motion,
898                from,
899                to: shift(from, slide_out_right()),
900            }),
901            PaneMotion::ExitWithShrink => Some(PaneTransition {
902                motion,
903                from,
904                to: PaneRect {
905                    x: left_edge(r, MotionType::Entering, &t),
906                    width: 0.,
907                    ..from
908                },
909            }),
910        };
911    }
912    out
913}
914
915/// A position the drag handle settles at.
916#[derive(Clone, Copy, Debug, PartialEq)]
917pub enum PaneAnchor {
918    /// A fraction of the scaffold's width.
919    Proportion(f32),
920    /// Dp from the left edge.
921    FromStart(f32),
922    /// Dp from the right edge.
923    FromEnd(f32),
924}
925
926impl PaneAnchor {
927    /// The handle's center for this anchor in a scaffold `width` wide.
928    pub fn position(self, width: f32) -> f32 {
929        match self {
930            Self::Proportion(fraction) => (width * fraction).round().clamp(0., width.max(0.)),
931            Self::FromStart(offset) => offset.round(),
932            Self::FromEnd(offset) => width - offset.round(),
933        }
934    }
935}
936
937/// The Compose sample's anchors, which m3e uses by default: 0%, 240dp from the
938/// start, 50%, 240dp from the end, 100%.
939pub const DEFAULT_PANE_ANCHORS: [PaneAnchor; 5] = [
940    PaneAnchor::Proportion(0.),
941    PaneAnchor::FromStart(240.),
942    PaneAnchor::Proportion(0.5),
943    PaneAnchor::FromEnd(240.),
944    PaneAnchor::Proportion(1.),
945];
946
947/// The anchor position closest to `position`.
948pub fn closest_pane_anchor(anchors: &[PaneAnchor], width: f32, position: f32) -> Option<f32> {
949    anchors
950        .iter()
951        .map(|anchor| anchor.position(width))
952        .fold(None, |best: Option<f32>, at| match best {
953            Some(best) if (best - position).abs() <= (at - position).abs() => Some(best),
954            _ => Some(at),
955        })
956}
957
958/// The first anchor position right of `position`, wrapping to the leftmost.
959pub fn next_pane_anchor(anchors: &[PaneAnchor], width: f32, position: f32) -> Option<f32> {
960    let mut sorted: Vec<f32> = anchors
961        .iter()
962        .map(|anchor| anchor.position(width))
963        .collect();
964    sorted.sort_by(f32::total_cmp);
965    sorted
966        .iter()
967        .find(|at| position < **at)
968        .or(sorted.first())
969        .copied()
970}
971
972/// The first anchor position left of `position`, wrapping to the rightmost.
973pub fn previous_pane_anchor(anchors: &[PaneAnchor], width: f32, position: f32) -> Option<f32> {
974    let mut sorted: Vec<f32> = anchors
975        .iter()
976        .map(|anchor| anchor.position(width))
977        .collect();
978    sorted.sort_by(f32::total_cmp);
979    sorted
980        .iter()
981        .rev()
982        .find(|at| position > **at)
983        .or(sorted.last())
984        .copied()
985}
986
987#[cfg(test)]
988mod tests {
989    use super::PaneAdaptedValue::{Expanded, Hidden, Reflowed};
990    use super::PaneRole::{Primary, Secondary, Tertiary};
991    use super::*;
992    use core::prelude::v1::test;
993
994    fn class(width: f32, height: f32) -> WindowSizeClass {
995        WindowSizeClass::of(width, height)
996    }
997
998    fn value(
999        horizontal: usize,
1000        vertical: usize,
1001        strategies: &AdaptStrategies,
1002        history: &[PaneRole],
1003    ) -> [PaneAdaptedValue; 3] {
1004        three_pane_value(horizontal, vertical, strategies, history.iter().copied()).0
1005    }
1006
1007    #[test]
1008    fn pane_counts_follow_the_rule_at_each_width() {
1009        for (width, recommended, compose) in [
1010            (360., 1, 1),
1011            (700., 1, 1),
1012            (1000., 2, 2),
1013            (1300., 2, 3),
1014            (1700., 3, 3),
1015        ] {
1016            let size = class(width, 800.);
1017            assert_eq!(
1018                pane_directive(size, PaneRule::Recommended).max_horizontal,
1019                recommended,
1020                "{width}"
1021            );
1022            assert_eq!(
1023                pane_directive(size, PaneRule::Compose).max_horizontal,
1024                compose,
1025                "{width}"
1026            );
1027        }
1028    }
1029
1030    #[test]
1031    fn directive_values_are_composes() {
1032        let compact = pane_directive(class(360., 800.), PaneRule::Recommended);
1033        assert_eq!(compact.horizontal_spacer, 0.);
1034        assert_eq!(compact.preferred_width, 360.);
1035        assert_eq!(compact.max_vertical, 1);
1036        let tall = pane_directive(class(360., 900.), PaneRule::Recommended);
1037        assert_eq!((tall.max_vertical, tall.vertical_spacer), (2, 24.));
1038        let expanded = pane_directive(class(1000., 900.), PaneRule::Recommended);
1039        assert_eq!(expanded.horizontal_spacer, 24.);
1040        assert_eq!(expanded.max_vertical, 1, "stacking is for a single pane");
1041        let large = pane_directive(class(1300., 800.), PaneRule::Recommended);
1042        assert_eq!(large.preferred_width, 412.);
1043        assert_eq!(large.preferred_height, 420.);
1044    }
1045
1046    #[test]
1047    fn the_latest_destination_takes_the_only_partition() {
1048        let list = [Secondary];
1049        assert_eq!(
1050            value(1, 1, &LIST_DETAIL_STRATEGIES, &list),
1051            [Hidden, Expanded, Hidden]
1052        );
1053        assert_eq!(
1054            value(1, 1, &LIST_DETAIL_STRATEGIES, &[Secondary, Primary]),
1055            [Expanded, Hidden, Hidden]
1056        );
1057    }
1058
1059    #[test]
1060    fn a_second_partition_goes_to_the_next_in_the_history_then_by_priority() {
1061        assert_eq!(
1062            value(2, 1, &LIST_DETAIL_STRATEGIES, &[Secondary]),
1063            [Expanded, Expanded, Hidden]
1064        );
1065        assert_eq!(
1066            value(
1067                2,
1068                1,
1069                &LIST_DETAIL_STRATEGIES,
1070                &[Secondary, Primary, Tertiary]
1071            ),
1072            [Expanded, Hidden, Expanded]
1073        );
1074        assert_eq!(
1075            value(3, 1, &LIST_DETAIL_STRATEGIES, &[Secondary]),
1076            [Expanded, Expanded, Expanded]
1077        );
1078    }
1079
1080    #[test]
1081    fn the_supporting_pane_reflows_under_the_main_pane_when_tall() {
1082        assert_eq!(
1083            value(1, 2, &SUPPORTING_PANE_STRATEGIES, &[Primary]),
1084            [Expanded, Reflowed, Hidden]
1085        );
1086        assert_eq!(
1087            value(1, 1, &SUPPORTING_PANE_STRATEGIES, &[Primary]),
1088            [Expanded, Hidden, Hidden]
1089        );
1090    }
1091
1092    #[test]
1093    fn back_pops_until_the_panes_change() {
1094        let mut navigator = PaneNavigator::<u32>::list_detail();
1095        navigator.set_directive(pane_directive(class(360., 800.), PaneRule::Recommended));
1096        assert!(!navigator.can_navigate_back(BackNavigationBehavior::default()));
1097        navigator.navigate_to(Primary, Some(1));
1098        navigator.navigate_to(Primary, Some(2));
1099        assert_eq!(navigator.content_key(), Some(&2));
1100        assert!(navigator.can_navigate_back(BackNavigationBehavior::PopLatest));
1101        // Two details look the same: the default skips to the list.
1102        assert!(navigator.navigate_back(BackNavigationBehavior::default()));
1103        assert_eq!(navigator.current().unwrap().pane, Secondary);
1104        assert_eq!(navigator.history().len(), 1);
1105        // Nothing left: it clears, and the value falls back to priority order.
1106        assert!(!navigator.navigate_back(BackNavigationBehavior::default()));
1107        assert!(navigator.history().is_empty());
1108        assert_eq!(navigator.value().get(Primary), Expanded);
1109    }
1110
1111    #[test]
1112    fn back_behaviors_differ_as_composes_do() {
1113        let mut navigator = PaneNavigator::<u32>::list_detail();
1114        navigator.set_directive(pane_directive(class(1000., 800.), PaneRule::Recommended));
1115        navigator.navigate_to(Primary, Some(1));
1116        navigator.navigate_to(Primary, Some(2));
1117        // Both panes show either way, so no scaffold change is available...
1118        assert!(!navigator.can_navigate_back(BackNavigationBehavior::PopUntilScaffoldValueChange));
1119        // ...though the content does change, and the pane does further back.
1120        let mut content = navigator.clone();
1121        assert!(content.navigate_back(BackNavigationBehavior::PopUntilContentChange));
1122        assert_eq!(content.content_key(), Some(&1));
1123        let mut pane = navigator.clone();
1124        assert!(pane.navigate_back(BackNavigationBehavior::PopUntilCurrentDestinationChange));
1125        assert_eq!(pane.current().unwrap().pane, Secondary);
1126    }
1127
1128    fn layout(width: f32, height: f32, history: &[PaneRole]) -> PaneLayout {
1129        let directive = pane_directive(class(width, height), PaneRule::Recommended);
1130        three_pane_layout(&PaneLayoutInput {
1131            width,
1132            height,
1133            directive,
1134            value: three_pane_value(
1135                directive.max_horizontal,
1136                directive.max_vertical,
1137                &LIST_DETAIL_STRATEGIES,
1138                history.iter().copied(),
1139            ),
1140            order: &LIST_DETAIL_ORDER,
1141            strategies: &LIST_DETAIL_STRATEGIES,
1142            present: [true; 3],
1143            preferred_widths: [None; 3],
1144            dragging_offset: None,
1145            first_pane_width: None,
1146        })
1147    }
1148
1149    #[test]
1150    fn the_primary_pane_takes_what_the_others_leave() {
1151        let two = layout(1000., 800., &[Secondary]);
1152        let list = two.panes[Secondary.index()].unwrap();
1153        let detail = two.panes[Primary.index()].unwrap();
1154        assert_eq!((list.x, list.width), (0., 360.));
1155        assert_eq!((detail.x, detail.width), (384., 616.));
1156        assert_eq!(two.handle_offset, Some(372.));
1157        assert!(two.panes[Tertiary.index()].is_none());
1158    }
1159
1160    #[test]
1161    fn panes_that_do_not_fit_shrink_by_the_same_ratio() {
1162        let directive = PaneDirective {
1163            max_horizontal: 2,
1164            ..pane_directive(class(1000., 800.), PaneRule::Recommended)
1165        };
1166        let result = three_pane_layout(&PaneLayoutInput {
1167            width: 500.,
1168            height: 800.,
1169            directive,
1170            value: ThreePaneValue([Expanded, Expanded, Hidden]),
1171            order: &LIST_DETAIL_ORDER,
1172            strategies: &LIST_DETAIL_STRATEGIES,
1173            present: [true; 3],
1174            preferred_widths: [None; 3],
1175            dragging_offset: None,
1176            first_pane_width: None,
1177        });
1178        let (first, second) = (
1179            result.panes[Secondary.index()].unwrap(),
1180            result.panes[Primary.index()].unwrap(),
1181        );
1182        assert_eq!(first.width, second.width);
1183        assert!(first.width + second.width + 24. <= 500.);
1184    }
1185
1186    #[test]
1187    fn a_reflowed_pane_sits_under_its_anchor_at_its_preferred_height() {
1188        let directive = pane_directive(class(700., 1000.), PaneRule::Recommended);
1189        let result = three_pane_layout(&PaneLayoutInput {
1190            width: 700.,
1191            height: 1000.,
1192            directive,
1193            value: ThreePaneValue([Expanded, Reflowed, Hidden]),
1194            order: &SUPPORTING_PANE_ORDER,
1195            strategies: &SUPPORTING_PANE_STRATEGIES,
1196            present: [true; 3],
1197            preferred_widths: [None; 3],
1198            dragging_offset: None,
1199            first_pane_width: None,
1200        });
1201        let main = result.panes[Primary.index()].unwrap();
1202        let supporting = result.panes[Secondary.index()].unwrap();
1203        assert_eq!(main.width, 700.);
1204        assert_eq!(main.height, 1000. - 24. - 420.);
1205        assert_eq!(supporting.y, main.height + 24.);
1206        assert_eq!(supporting.height, 420.);
1207        assert_eq!(result.handle_offset, None);
1208    }
1209
1210    #[test]
1211    fn a_split_at_an_edge_collapses_one_pane() {
1212        let directive = pane_directive(class(1000., 800.), PaneRule::Recommended);
1213        let split = |first_pane_width| {
1214            three_pane_layout(&PaneLayoutInput {
1215                width: 1000.,
1216                height: 800.,
1217                directive,
1218                value: ThreePaneValue([Expanded, Expanded, Hidden]),
1219                order: &LIST_DETAIL_ORDER,
1220                strategies: &LIST_DETAIL_STRATEGIES,
1221                present: [true; 3],
1222                preferred_widths: [None; 3],
1223                dragging_offset: None,
1224                first_pane_width: Some(first_pane_width),
1225            })
1226        };
1227        let all_second = split(0.);
1228        assert_eq!(all_second.panes[Primary.index()].unwrap().width, 1000.);
1229        assert_eq!(all_second.panes[Secondary.index()].unwrap().width, 0.);
1230        assert_eq!(all_second.handle_offset, Some(0.));
1231        let all_first = split(1000.);
1232        assert_eq!(all_first.panes[Secondary.index()].unwrap().width, 1000.);
1233        let middle = split(500.);
1234        assert_eq!(middle.panes[Secondary.index()].unwrap().width, 500.);
1235        assert_eq!(middle.panes[Primary.index()].unwrap().x, 524.);
1236    }
1237
1238    #[test]
1239    fn extreme_sizes_do_not_panic() {
1240        for (width, height) in [(0., 0.), (1., 1.), (20000., 20000.)] {
1241            let result = layout(width, height, &[Secondary]);
1242            assert!(result.panes.iter().flatten().all(|pane| pane.width >= 0.));
1243        }
1244    }
1245
1246    #[test]
1247    fn navigating_in_one_pane_slides_the_new_pane_over_the_old() {
1248        let before = ThreePaneValue([Hidden, Expanded, Hidden]);
1249        let after = ThreePaneValue([Expanded, Hidden, Hidden]);
1250        // The list is left of the detail: forward, the list leaves left and
1251        // the detail enters from the right.
1252        let forward = pane_motions(before, after, &LIST_DETAIL_ORDER);
1253        assert_eq!(forward[Secondary.index()], PaneMotion::ExitToLeft);
1254        assert_eq!(forward[Primary.index()], PaneMotion::EnterFromRight);
1255        assert_eq!(forward[Tertiary.index()], PaneMotion::None);
1256        // Back reverses it.
1257        let back = pane_motions(after, before, &LIST_DETAIL_ORDER);
1258        assert_eq!(back[Primary.index()], PaneMotion::ExitToRight);
1259        assert_eq!(back[Secondary.index()], PaneMotion::EnterFromLeft);
1260    }
1261
1262    #[test]
1263    fn a_pane_stays_shown_and_animates_its_bounds() {
1264        let before = ThreePaneValue([Expanded, Expanded, Hidden]);
1265        let after = ThreePaneValue([Expanded, Hidden, Expanded]);
1266        let motions = pane_motions(before, after, &LIST_DETAIL_ORDER);
1267        assert_eq!(motions[Primary.index()], PaneMotion::AnimateBounds);
1268        assert_eq!(motions[Secondary.index()], PaneMotion::ExitToLeft);
1269        assert_eq!(motions[Tertiary.index()], PaneMotion::EnterFromRight);
1270    }
1271
1272    #[test]
1273    fn transitions_start_and_end_outside_the_scaffold() {
1274        let order = LIST_DETAIL_ORDER;
1275        let list = PaneRect {
1276            x: 0.,
1277            y: 0.,
1278            width: 400.,
1279            height: 800.,
1280        };
1281        let motions = pane_motions(
1282            ThreePaneValue([Hidden, Expanded, Hidden]),
1283            ThreePaneValue([Expanded, Hidden, Hidden]),
1284            &order,
1285        );
1286        let origin = [None, Some(list), None];
1287        let target = [Some(list), None, None];
1288        let result = pane_transitions(&motions, &order, &origin, &target, 400.);
1289        let leaving = result[Secondary.index()].unwrap();
1290        assert_eq!(leaving.from, list);
1291        assert_eq!(leaving.to.x, -400.);
1292        let entering = result[Primary.index()].unwrap();
1293        assert_eq!(entering.to, list);
1294        assert_eq!(entering.from.x, 400.);
1295        assert!(result[Tertiary.index()].is_none());
1296    }
1297
1298    #[test]
1299    fn anchors_resolve_and_wrap() {
1300        let anchors = DEFAULT_PANE_ANCHORS;
1301        assert_eq!(anchors[0].position(1000.), 0.);
1302        assert_eq!(anchors[1].position(1000.), 240.);
1303        assert_eq!(anchors[2].position(1000.), 500.);
1304        assert_eq!(anchors[3].position(1000.), 760.);
1305        assert_eq!(anchors[4].position(1000.), 1000.);
1306        assert_eq!(closest_pane_anchor(&anchors, 1000., 300.), Some(240.));
1307        assert_eq!(next_pane_anchor(&anchors, 1000., 500.), Some(760.));
1308        assert_eq!(next_pane_anchor(&anchors, 1000., 1000.), Some(0.));
1309        assert_eq!(previous_pane_anchor(&anchors, 1000., 500.), Some(240.));
1310        assert_eq!(previous_pane_anchor(&anchors, 1000., 0.), Some(1000.));
1311        assert_eq!(closest_pane_anchor(&[], 1000., 300.), None);
1312    }
1313}