Skip to main content

halley_core/
stacking.rs

1use crate::cluster_layout::{ClusterCycleDirection, ClusterWorkspacePlacement};
2use crate::field::NodeId;
3use crate::tiling::Rect;
4
5pub fn stacking_visible_limit(max_visible: usize) -> usize {
6    if max_visible == 0 {
7        usize::MAX
8    } else {
9        max_visible
10    }
11}
12
13pub fn layout_stacking_workspace(
14    bounds: Rect,
15    members: &[NodeId],
16    frame_pad: f32,
17) -> Vec<ClusterWorkspacePlacement> {
18    if members.is_empty() {
19        return Vec::new();
20    }
21
22    const STACK_CARD_WIDTH_FRAC: f32 = 0.74;
23    const STACK_CARD_HEIGHT_FRAC: f32 = 0.84;
24    const STACK_PEEK_X_FRAC: f32 = 0.04;
25    const STACK_SCALE_STEP: f32 = 0.035;
26    const STACK_MIN_SCALE: f32 = 0.84;
27
28    let frame_pad = frame_pad.max(0.0);
29    let base_frame_w = (bounds.w * STACK_CARD_WIDTH_FRAC).clamp(1.0, bounds.w.max(1.0));
30    let base_frame_h = (bounds.h * STACK_CARD_HEIGHT_FRAC).clamp(1.0, bounds.h.max(1.0));
31    let center_x = bounds.x + bounds.w * 0.5;
32    let center_y = bounds.y + bounds.h * 0.5;
33    let active_frame_x = center_x - base_frame_w * 0.5;
34    let active_frame_y = center_y - base_frame_h * 0.5;
35    let peek_x = (bounds.w * STACK_PEEK_X_FRAC).max(24.0) + frame_pad * 2.0;
36
37    let mut layered = Vec::with_capacity(members.len());
38    for (index, node_id) in members.iter().copied().enumerate() {
39        let scale = (1.0 - index as f32 * STACK_SCALE_STEP).clamp(STACK_MIN_SCALE, 1.0);
40        let frame_w = (base_frame_w * scale).clamp(1.0, bounds.w.max(1.0));
41        let frame_h = (base_frame_h * scale).clamp(1.0, bounds.h.max(1.0));
42        let width = (frame_w - frame_pad * 2.0).clamp(1.0, bounds.w.max(1.0));
43        let height = (frame_h - frame_pad * 2.0).clamp(1.0, bounds.h.max(1.0));
44        let offset_x = peek_x * index as f32;
45        let rect = Rect {
46            x: active_frame_x + offset_x + frame_pad,
47            y: active_frame_y + frame_pad,
48            w: width,
49            h: height,
50        };
51        layered.push((index, node_id, rect));
52    }
53
54    layered.sort_by_key(|(index, _, _)| std::cmp::Reverse(*index));
55    layered
56        .into_iter()
57        .enumerate()
58        .map(|(depth, (_, node_id, rect))| ClusterWorkspacePlacement {
59            node_id,
60            rect,
61            depth,
62        })
63        .collect()
64}
65
66pub fn cycle_stacking_members(
67    members: &mut Vec<NodeId>,
68    direction: ClusterCycleDirection,
69) -> Option<NodeId> {
70    if members.is_empty() {
71        return None;
72    }
73
74    match direction {
75        ClusterCycleDirection::Prev => members.rotate_right(1),
76        ClusterCycleDirection::Next => members.rotate_left(1),
77    }
78    members.first().copied()
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn ids(n: u64) -> Vec<NodeId> {
86        (0..n).map(NodeId::new).collect()
87    }
88
89    #[test]
90    fn zero_max_visible_means_unlimited_for_stacking() {
91        assert_eq!(stacking_visible_limit(0), usize::MAX);
92    }
93
94    #[test]
95    fn stacking_front_card_is_centered_and_front_most() {
96        let members = ids(4);
97        let result = layout_stacking_workspace(
98            Rect {
99                x: 0.0,
100                y: 0.0,
101                w: 1000.0,
102                h: 600.0,
103            },
104            &members,
105            12.0,
106        );
107
108        let active = result
109            .iter()
110            .find(|placement| placement.node_id == members[0])
111            .expect("active placement");
112
113        assert_eq!(active.depth, result.len() - 1);
114        assert!((active.rect.x - 12.0 + (active.rect.w + 24.0) * 0.5 - 500.0).abs() <= 0.5);
115        assert!((active.rect.y - 12.0 + (active.rect.h + 24.0) * 0.5 - 300.0).abs() <= 0.5);
116    }
117
118    #[test]
119    fn stacking_visible_cards_step_outward_in_one_direction() {
120        let members = ids(5);
121        let result = layout_stacking_workspace(
122            Rect {
123                x: 0.0,
124                y: 0.0,
125                w: 1000.0,
126                h: 600.0,
127            },
128            &members,
129            12.0,
130        );
131
132        let active = result
133            .iter()
134            .find(|placement| placement.node_id == members[0])
135            .expect("active placement");
136        let second = result
137            .iter()
138            .find(|placement| placement.node_id == members[1])
139            .expect("second placement");
140        let third = result
141            .iter()
142            .find(|placement| placement.node_id == members[2])
143            .expect("third placement");
144
145        assert!(second.rect.x > active.rect.x);
146        assert!((second.rect.y - active.rect.y).abs() <= 0.5);
147        assert!(third.rect.x > second.rect.x);
148        assert!((third.rect.y - second.rect.y).abs() <= 0.5);
149        assert!(second.rect.w < active.rect.w);
150        assert!(third.rect.w < second.rect.w);
151        assert!(second.rect.h < active.rect.h);
152        assert!(third.rect.h < second.rect.h);
153        assert!(second.rect.x - active.rect.x > 24.0);
154    }
155
156    #[test]
157    fn cycling_stacking_members_rotates_order() {
158        let mut members = ids(4);
159
160        assert_eq!(
161            cycle_stacking_members(&mut members, ClusterCycleDirection::Next),
162            Some(NodeId::new(1))
163        );
164        assert_eq!(
165            members,
166            vec![
167                NodeId::new(1),
168                NodeId::new(2),
169                NodeId::new(3),
170                NodeId::new(0)
171            ]
172        );
173    }
174}