1use super::adaptive::{WindowHeightClass, WindowSizeClass, WindowWidthClass};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum PaneRole {
21 Primary,
23 Secondary,
25 Tertiary,
27}
28
29impl PaneRole {
30 pub const ALL: [Self; 3] = [Self::Primary, Self::Secondary, Self::Tertiary];
32
33 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum AdaptStrategy {
50 Hide,
52 Reflow(PaneRole),
54}
55
56pub type AdaptStrategies = [AdaptStrategy; 3];
58
59pub const LIST_DETAIL_STRATEGIES: AdaptStrategies = [AdaptStrategy::Hide; 3];
61pub const SUPPORTING_PANE_STRATEGIES: AdaptStrategies = [
63 AdaptStrategy::Hide,
64 AdaptStrategy::Reflow(PaneRole::Primary),
65 AdaptStrategy::Hide,
66];
67pub const LIST_DETAIL_ORDER: [PaneRole; 3] =
69 [PaneRole::Secondary, PaneRole::Primary, PaneRole::Tertiary];
70pub const SUPPORTING_PANE_ORDER: [PaneRole; 3] =
72 [PaneRole::Primary, PaneRole::Secondary, PaneRole::Tertiary];
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum PaneAdaptedValue {
77 Expanded,
79 Reflowed,
81 Hidden,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct ThreePaneValue(pub [PaneAdaptedValue; 3]);
88
89impl ThreePaneValue {
90 pub const fn get(&self, role: PaneRole) -> PaneAdaptedValue {
92 self.0[role.index()]
93 }
94}
95
96pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum PaneRule {
148 #[default]
151 Recommended,
152 Compose,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct PaneDirective {
159 pub max_horizontal: usize,
161 pub horizontal_spacer: f32,
163 pub max_vertical: usize,
165 pub vertical_spacer: f32,
167 pub preferred_width: f32,
169 pub preferred_height: f32,
171}
172
173pub 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
202pub enum BackNavigationBehavior {
203 PopLatest,
205 #[default]
207 PopUntilScaffoldValueChange,
208 PopUntilCurrentDestinationChange,
210 PopUntilContentChange,
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct PaneDestination<K> {
217 pub pane: PaneRole,
219 pub content_key: Option<K>,
221}
222
223#[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 pub fn list_detail() -> Self {
265 Self::new(PaneRole::Secondary, LIST_DETAIL_STRATEGIES)
266 }
267
268 pub fn supporting_pane() -> Self {
270 Self::new(PaneRole::Primary, SUPPORTING_PANE_STRATEGIES)
271 }
272
273 pub fn history(&self) -> &[PaneDestination<K>] {
275 &self.history
276 }
277
278 pub fn current(&self) -> Option<&PaneDestination<K>> {
280 self.history.last()
281 }
282
283 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 pub fn directive(&self) -> PaneDirective {
293 self.directive
294 }
295
296 pub fn set_directive(&mut self, directive: PaneDirective) -> bool {
298 let changed = self.directive != directive;
299 self.directive = directive;
300 changed
301 }
302
303 pub fn strategies(&self) -> &AdaptStrategies {
305 &self.strategies
306 }
307
308 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 pub fn is_expanded(&self, pane: PaneRole) -> bool {
325 self.value().get(pane) == PaneAdaptedValue::Expanded
326 }
327
328 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 pub fn can_navigate_back(&self, behavior: BackNavigationBehavior) -> bool {
357 self.previous_index(behavior).is_some()
358 }
359
360 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
378pub struct PaneRect {
379 pub x: f32,
381 pub y: f32,
383 pub width: f32,
385 pub height: f32,
387}
388
389impl PaneRect {
390 pub fn right(&self) -> f32 {
392 self.x + self.width
393 }
394
395 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#[derive(Clone, Copy, Debug)]
409pub struct PaneLayoutInput<'a> {
410 pub width: f32,
412 pub height: f32,
414 pub directive: PaneDirective,
416 pub value: ThreePaneValue,
418 pub order: &'a [PaneRole; 3],
420 pub strategies: &'a AdaptStrategies,
422 pub present: [bool; 3],
424 pub preferred_widths: [Option<f32>; 3],
426 pub dragging_offset: Option<f32>,
428 pub first_pane_width: Option<f32>,
430}
431
432#[derive(Clone, Copy, Debug, Default, PartialEq)]
434pub struct PaneLayout {
435 pub panes: [Option<PaneRect>; 3],
438 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#[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
470pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
629pub enum PaneMotion {
630 None,
632 AnimateBounds,
634 EnterFromLeft,
636 EnterFromRight,
638 EnterFromLeftDelayed,
640 EnterFromRightDelayed,
642 ExitToLeft,
644 ExitToRight,
646 EnterWithExpand,
648 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 pub const fn delayed(self) -> bool {
676 matches!(
677 self,
678 Self::EnterFromLeftDelayed | Self::EnterFromRightDelayed
679 )
680 }
681}
682
683pub 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#[derive(Clone, Copy, Debug, PartialEq)]
769pub struct PaneTransition {
770 pub motion: PaneMotion,
772 pub from: PaneRect,
774 pub to: PaneRect,
776}
777
778pub 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#[derive(Clone, Copy, Debug, PartialEq)]
917pub enum PaneAnchor {
918 Proportion(f32),
920 FromStart(f32),
922 FromEnd(f32),
924}
925
926impl PaneAnchor {
927 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
937pub 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
947pub 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
958pub 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
972pub 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 assert!(navigator.navigate_back(BackNavigationBehavior::default()));
1103 assert_eq!(navigator.current().unwrap().pane, Secondary);
1104 assert_eq!(navigator.history().len(), 1);
1105 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 assert!(!navigator.can_navigate_back(BackNavigationBehavior::PopUntilScaffoldValueChange));
1119 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 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 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}