Skip to main content

fret_core/dock/
query.rs

1use super::*;
2
3#[derive(Debug, Default)]
4struct DockParentIndex {
5    root_for: HashMap<DockNodeId, DockNodeId>,
6    parent: HashMap<DockNodeId, DockNodeId>,
7    split_child_index: HashMap<DockNodeId, usize>,
8}
9
10impl DockGraph {
11    fn build_parent_index_for_window(&self, window: AppWindowId) -> DockParentIndex {
12        fn index_subtree(graph: &DockGraph, root: DockNodeId, index: &mut DockParentIndex) {
13            let mut stack: Vec<DockNodeId> = vec![root];
14            while let Some(node) = stack.pop() {
15                if index.root_for.contains_key(&node) {
16                    continue;
17                }
18                index.root_for.insert(node, root);
19
20                let Some(n) = graph.nodes.get(node) else {
21                    continue;
22                };
23                match n {
24                    DockNode::Tabs { .. } => {}
25                    DockNode::Floating { child } => {
26                        index.parent.insert(*child, node);
27                        stack.push(*child);
28                    }
29                    DockNode::Split { children, .. } => {
30                        for (i, child) in children.iter().copied().enumerate() {
31                            index.parent.insert(child, node);
32                            index.split_child_index.insert(child, i);
33                            stack.push(child);
34                        }
35                    }
36                }
37            }
38        }
39
40        let mut index = DockParentIndex::default();
41        if let Some(root) = self.window_root(window) {
42            index_subtree(self, root, &mut index);
43        }
44        if let Some(list) = self.window_floatings.get(&window) {
45            for w in list {
46                index_subtree(self, w.floating, &mut index);
47            }
48        }
49        index
50    }
51
52    /// Decide whether an edge-dock into `target` will insert into an existing same-axis split, or
53    /// wrap the target in a new split.
54    ///
55    /// Returns `None` for `DropZone::Center` (not an edge dock) or if the target is not present in
56    /// the window's dock forest.
57    pub fn edge_dock_decision(
58        &self,
59        window: AppWindowId,
60        target: DockNodeId,
61        zone: DropZone,
62    ) -> Option<EdgeDockDecision> {
63        if zone == DropZone::Center {
64            return None;
65        }
66
67        let axis = match zone {
68            DropZone::Left | DropZone::Right => Axis::Horizontal,
69            DropZone::Top | DropZone::Bottom => Axis::Vertical,
70            DropZone::Center => unreachable!(),
71        };
72
73        // Index parent links for the window's docking forest so we can answer:
74        // - is the target in the forest?
75        // - what is the nearest same-axis split ancestor?
76        //
77        // This avoids repeated subtree scans in edge-dock hot paths.
78        let index = self.build_parent_index_for_window(window);
79        if !index.root_for.contains_key(&target) {
80            return None;
81        }
82
83        // Outer docking can target an existing split container. In that case we can insert at the
84        // boundary without searching for an ancestor.
85        if let Some(DockNode::Split {
86            axis: split_axis,
87            children,
88            fractions,
89        }) = self.nodes.get(target)
90            && *split_axis == axis
91            && !children.is_empty()
92            && children.len() == fractions.len()
93        {
94            let len = children.len();
95            let (anchor_index, insert_index) = match zone {
96                DropZone::Left | DropZone::Top => (0, 0),
97                DropZone::Right | DropZone::Bottom => {
98                    let last = len.saturating_sub(1);
99                    (last, last.saturating_add(1))
100                }
101                DropZone::Center => unreachable!(),
102            };
103            return Some(EdgeDockDecision::InsertIntoSplit {
104                split: target,
105                anchor_index,
106                insert_index,
107            });
108        }
109
110        let mut cur = target;
111        while let Some(parent) = index.parent.get(&cur).copied() {
112            let Some(DockNode::Split {
113                axis: split_axis,
114                children,
115                fractions,
116            }) = self.nodes.get(parent)
117            else {
118                cur = parent;
119                continue;
120            };
121
122            if *split_axis == axis && !children.is_empty() && children.len() == fractions.len() {
123                let Some(anchor_index) = index.split_child_index.get(&cur).copied() else {
124                    break;
125                };
126
127                let insert_index = match zone {
128                    DropZone::Left | DropZone::Top => anchor_index,
129                    DropZone::Right | DropZone::Bottom => anchor_index.saturating_add(1),
130                    DropZone::Center => unreachable!(),
131                };
132
133                return Some(EdgeDockDecision::InsertIntoSplit {
134                    split: parent,
135                    anchor_index,
136                    insert_index,
137                });
138            }
139
140            cur = parent;
141        }
142
143        Some(EdgeDockDecision::WrapNewSplit)
144    }
145
146    pub fn compute_layout(
147        &self,
148        root: DockNodeId,
149        bounds: Rect,
150        out: &mut HashMap<DockNodeId, Rect>,
151    ) {
152        let Some(node) = self.nodes.get(root) else {
153            return;
154        };
155
156        out.insert(root, bounds);
157        match node {
158            DockNode::Tabs { .. } => {}
159            DockNode::Split {
160                axis,
161                children,
162                fractions,
163            } => {
164                if children.is_empty() {
165                    return;
166                }
167
168                // Layout should not silently truncate children if invariants are violated. If the
169                // split is non-canonical (mismatched lengths, non-finite values), repair the shares
170                // locally for deterministic layout.
171                let cleaned_share_at = |i: usize| -> f32 {
172                    let raw = fractions.get(i).copied().unwrap_or(1.0);
173                    if raw.is_finite() && raw > 0.0 {
174                        raw
175                    } else {
176                        0.0
177                    }
178                };
179
180                let mut total = 0.0;
181                for i in 0..children.len() {
182                    total += cleaned_share_at(i);
183                }
184                let uniform = 1.0 / children.len() as f32;
185                let inv_total = if total > 0.0 { 1.0 / total } else { 0.0 };
186
187                let mut cursor = 0.0;
188                for (i, child) in children.iter().copied().enumerate() {
189                    let f = if total > 0.0 {
190                        cleaned_share_at(i) * inv_total
191                    } else {
192                        uniform
193                    };
194                    let (child_rect, next_cursor) = match axis {
195                        Axis::Horizontal => {
196                            let w = bounds.size.width.0 * f;
197                            let rect = Rect {
198                                origin: Point::new(Px(bounds.origin.x.0 + cursor), bounds.origin.y),
199                                size: Size::new(Px(w), bounds.size.height),
200                            };
201                            (rect, cursor + w)
202                        }
203                        Axis::Vertical => {
204                            let h = bounds.size.height.0 * f;
205                            let rect = Rect {
206                                origin: Point::new(bounds.origin.x, Px(bounds.origin.y.0 + cursor)),
207                                size: Size::new(bounds.size.width, Px(h)),
208                            };
209                            (rect, cursor + h)
210                        }
211                    };
212
213                    cursor = next_cursor;
214                    self.compute_layout(child, child_rect, out);
215                }
216            }
217            DockNode::Floating { child } => {
218                self.compute_layout(*child, bounds, out);
219            }
220        }
221    }
222
223    pub fn find_panel_in_window(
224        &self,
225        window: AppWindowId,
226        panel: &PanelKey,
227    ) -> Option<(DockNodeId, usize)> {
228        if let Some(root) = self.window_root(window)
229            && let Some(found) = self.find_panel_in_subtree(root, panel)
230        {
231            return Some(found);
232        }
233
234        self.window_floatings.get(&window).and_then(|list| {
235            list.iter()
236                .find_map(|w| self.find_panel_in_subtree(w.floating, panel))
237        })
238    }
239
240    pub fn windows(&self) -> Vec<AppWindowId> {
241        let mut windows: Vec<AppWindowId> = self.window_roots.keys().copied().collect();
242        windows.sort_by_key(|w| w.data().as_ffi());
243        windows
244    }
245
246    pub fn collect_panels_in_window(&self, window: AppWindowId) -> Vec<PanelKey> {
247        let mut out: Vec<PanelKey> = Vec::new();
248        if let Some(root) = self.window_root(window) {
249            out.extend(self.collect_panels_in_subtree(root));
250        }
251        if let Some(list) = self.window_floatings.get(&window) {
252            for w in list {
253                out.extend(self.collect_panels_in_subtree(w.floating));
254            }
255        }
256        out
257    }
258
259    pub fn first_tabs_in_window(&self, window: AppWindowId) -> Option<DockNodeId> {
260        if let Some(root) = self.window_root(window)
261            && let Some(tabs) = self.first_tabs_in_subtree(root)
262        {
263            return Some(tabs);
264        }
265
266        self.window_floatings.get(&window).and_then(|list| {
267            list.iter()
268                .find_map(|w| self.first_tabs_in_subtree(w.floating))
269        })
270    }
271
272    pub(super) fn collect_panels_in_subtree(&self, node: DockNodeId) -> Vec<PanelKey> {
273        let Some(n) = self.nodes.get(node) else {
274            return Vec::new();
275        };
276        match n {
277            DockNode::Tabs { tabs, .. } => tabs.clone(),
278            DockNode::Split { children, .. } => {
279                let mut out = Vec::new();
280                for child in children {
281                    out.extend(self.collect_panels_in_subtree(*child));
282                }
283                out
284            }
285            DockNode::Floating { child } => self.collect_panels_in_subtree(*child),
286        }
287    }
288
289    fn first_tabs_in_subtree(&self, node: DockNodeId) -> Option<DockNodeId> {
290        let n = self.nodes.get(node)?;
291        match n {
292            DockNode::Tabs { .. } => Some(node),
293            DockNode::Split { children, .. } => children
294                .iter()
295                .copied()
296                .find_map(|child| self.first_tabs_in_subtree(child)),
297            DockNode::Floating { child } => self.first_tabs_in_subtree(*child),
298        }
299    }
300
301    fn find_panel_in_subtree(
302        &self,
303        node: DockNodeId,
304        panel: &PanelKey,
305    ) -> Option<(DockNodeId, usize)> {
306        let n = self.nodes.get(node)?;
307        match n {
308            DockNode::Tabs { tabs, .. } => tabs.iter().position(|p| p == panel).map(|i| (node, i)),
309            DockNode::Split { children, .. } => children
310                .iter()
311                .copied()
312                .find_map(|child| self.find_panel_in_subtree(child, panel)),
313            DockNode::Floating { child } => self.find_panel_in_subtree(*child, panel),
314        }
315    }
316}