Skip to main content

ravenrs_widgets/dock/
layout.rs

1//! The arrangement of a [`DockArea`](crate::DockArea), with no drawing: which panel is where, what
2//! happens as they move, and the text it is saved as.
3
4use std::collections::HashSet;
5use std::fmt;
6
7use ravenrs_layout::Axis;
8use serde::{Deserialize, Serialize};
9
10use crate::lists::reorder;
11
12/// The version of the saved text this build writes, and the newest it reads.
13const FORMAT_VERSION: u64 = 1;
14/// How far the shares of a split may stray from a sum of one before they are put right. Saved
15/// shares land within it, so reading a text and writing it again gives the same text.
16const SHARES_SUM_TOLERANCE: f32 = 1e-3;
17
18/// One of the four places a [`DockArea`](crate::DockArea) has.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21#[non_exhaustive]
22#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
23pub enum DockSide {
24    /// The column on the left.
25    Left,
26    /// The column on the right.
27    Right,
28    /// The strip under the center, between the columns.
29    Bottom,
30    /// What takes the room left.
31    Center,
32}
33
34#[cfg_attr(feature = "live", ravenrs_live::expose)]
35impl DockSide {
36    /// The three docks that collapse and have a size of their own.
37    pub(crate) const DOCKS: [Self; 3] = [Self::Left, Self::Right, Self::Bottom];
38    /// All four, in the order they are searched.
39    pub(crate) const ALL: [Self; 4] = [Self::Center, Self::Left, Self::Right, Self::Bottom];
40    /// All four, in the order a menu lists them.
41    pub(crate) const ALL_IN_MENU: [Self; 4] = [Self::Left, Self::Right, Self::Bottom, Self::Center];
42}
43
44/// A side of a group of tabs, where a split opens.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
48pub enum DockEdge {
49    /// To its left.
50    Left,
51    /// To its right.
52    Right,
53    /// Above it.
54    Top,
55    /// Below it.
56    Bottom,
57}
58
59/// Where a panel is sent by [`DockLayout::move_panel`]. Places are named by a panel that is there,
60/// since the id of a panel is what stays the same as the tree changes around it.
61#[derive(Clone, Debug, PartialEq, Eq)]
62#[non_exhaustive]
63#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
64pub enum DockDrop {
65    /// Into the group of `beside`, in the gap `at`: 0 is before its first tab, and anything past
66    /// the end is after its last.
67    Tab {
68        /// A panel of the group.
69        beside: String,
70        /// The gap among its tabs.
71        at: usize,
72    },
73    /// A group of its own, opened on one side of the group of `beside`.
74    Split {
75        /// A panel of the group to split.
76        beside: String,
77        /// The side the new group opens on.
78        edge: DockEdge,
79    },
80    /// Into a place as a whole: its first group, or a new one if it holds nothing.
81    Side(DockSide),
82}
83
84/// A part of the tree: tabs, or parts side by side or stacked.
85#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
86#[serde(rename_all = "lowercase")]
87#[non_exhaustive]
88#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
89pub enum DockNode {
90    /// A group of panels, of which one shows.
91    Tabs {
92        /// The ids, in the order of the tabs.
93        panels: Vec<String>,
94        /// Which of them shows.
95        #[serde(default)]
96        active: usize,
97    },
98    /// Parts along an axis.
99    Split {
100        /// `Horizontal` puts them side by side, and `Vertical` stacks them.
101        #[serde(with = "axis_name")]
102        axis: Axis,
103        /// The parts, in order.
104        children: Vec<DockChild>,
105    },
106}
107
108/// A part of a split and how much of it it takes.
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
111pub struct DockChild {
112    /// Its part of the room, as a fraction: the parts of a split add up to one.
113    pub share: f32,
114    /// What is there.
115    pub node: DockNode,
116}
117
118#[cfg_attr(feature = "live", ravenrs_live::expose)]
119impl DockNode {
120    /// A group of these panels, with the first one showing.
121    pub fn tabs<S: Into<String>>(panels: impl IntoIterator<Item = S>) -> Self {
122        Self::Tabs { panels: panels.into_iter().map(Into::into).collect(), active: 0 }
123    }
124
125    /// These parts side by side, in equal shares.
126    pub fn horizontal(children: impl IntoIterator<Item = DockNode>) -> Self {
127        Self::split(Axis::Horizontal, children)
128    }
129
130    /// These parts stacked, in equal shares.
131    pub fn vertical(children: impl IntoIterator<Item = DockNode>) -> Self {
132        Self::split(Axis::Vertical, children)
133    }
134
135    fn split(axis: Axis, children: impl IntoIterator<Item = DockNode>) -> Self {
136        let nodes: Vec<DockNode> = children.into_iter().collect();
137        let share = 1.0 / nodes.len().max(1) as f32;
138        Self::Split { axis, children: nodes.into_iter().map(|node| DockChild { share, node }).collect() }
139    }
140
141    /// The group under this node that has this panel.
142    fn group_of(&mut self, id: &str) -> Option<(&mut Vec<String>, &mut usize)> {
143        match self {
144            Self::Tabs { panels, active } => {
145                if panels.iter().any(|panel| panel == id) {
146                    Some((panels, active))
147                } else {
148                    None
149                }
150            }
151            Self::Split { children, .. } => children.iter_mut().find_map(|c| c.node.group_of(id)),
152        }
153    }
154
155    /// Whether this panel is what its group shows, if the panel is under this node.
156    fn shows(&self, id: &str) -> Option<bool> {
157        match self {
158            Self::Tabs { panels, active } => {
159                panels.iter().any(|panel| panel == id).then(|| panels.get(*active).is_some_and(|p| p == id))
160            }
161            Self::Split { children, .. } => children.iter().find_map(|c| c.node.shows(id)),
162        }
163    }
164
165    fn holds(&self, id: &str) -> bool {
166        match self {
167            Self::Tabs { panels, .. } => panels.iter().any(|panel| panel == id),
168            Self::Split { children, .. } => children.iter().any(|c| c.node.holds(id)),
169        }
170    }
171
172    fn first_group(&mut self) -> Option<(&mut Vec<String>, &mut usize)> {
173        match self {
174            Self::Tabs { panels, active } => Some((panels, active)),
175            Self::Split { children, .. } => children.first_mut().and_then(|c| c.node.first_group()),
176        }
177    }
178
179    fn collect_ids<'a>(&'a self, out: &mut Vec<&'a str>) {
180        match self {
181            Self::Tabs { panels, .. } => out.extend(panels.iter().map(String::as_str)),
182            Self::Split { children, .. } => children.iter().for_each(|c| c.node.collect_ids(out)),
183        }
184    }
185
186    /// Takes the panel out, and says who stayed in its group. It can leave a group empty.
187    fn remove(&mut self, id: &str) -> Option<Option<String>> {
188        match self {
189            Self::Tabs { panels, active } => {
190                let at = panels.iter().position(|panel| panel == id)?;
191                panels.remove(at);
192                // The tab showing goes on showing, and a closed one gives way to its neighbor.
193                if at < *active {
194                    *active -= 1;
195                }
196                *active = (*active).min(panels.len().saturating_sub(1));
197                Some(panels.first().cloned())
198            }
199            Self::Split { children, .. } => children.iter_mut().find_map(|c| c.node.remove(id)),
200        }
201    }
202
203    /// Opens a group of `id` on the edge of the group of `beside`.
204    fn split_group(&mut self, beside: &str, id: &str, edge: DockEdge) -> bool {
205        match self {
206            Self::Tabs { panels, .. } if panels.iter().any(|panel| panel == beside) => {
207                let axis = match edge {
208                    DockEdge::Left | DockEdge::Right => Axis::Horizontal,
209                    DockEdge::Top | DockEdge::Bottom => Axis::Vertical,
210                };
211                let old = std::mem::replace(self, Self::tabs(Vec::<String>::new()));
212                let new = Self::tabs([id]);
213                let pair = match edge {
214                    DockEdge::Left | DockEdge::Top => [new, old],
215                    DockEdge::Right | DockEdge::Bottom => [old, new],
216                };
217                *self = Self::split(axis, pair);
218                true
219            }
220            Self::Tabs { .. } => false,
221            Self::Split { children, .. } => children.iter_mut().any(|c| c.node.split_group(beside, id, edge)),
222        }
223    }
224
225    /// The split at `path`, each step an index among the children.
226    fn split_at(&mut self, path: &[usize]) -> Option<&mut Vec<DockChild>> {
227        let Self::Split { children, .. } = self else { return None };
228        match path.split_first() {
229            None => Some(children),
230            Some((first, rest)) => children.get_mut(*first)?.node.split_at(rest),
231        }
232    }
233}
234
235/// Puts a node in its simplest form: panels seen once, no empty group, no split of one part or
236/// inside another along the same axis, and shares that add up to one.
237fn tidied(node: DockNode, seen: &mut HashSet<String>) -> Option<DockNode> {
238    match node {
239        DockNode::Tabs { panels, active } => {
240            let shown = panels.get(active).cloned();
241            let mut kept = Vec::with_capacity(panels.len());
242            for panel in panels {
243                if seen.insert(panel.clone()) {
244                    kept.push(panel);
245                }
246            }
247            if kept.is_empty() {
248                return None;
249            }
250            let active = shown
251                .and_then(|shown| kept.iter().position(|panel| *panel == shown))
252                .unwrap_or_else(|| active.min(kept.len() - 1));
253            Some(DockNode::Tabs { panels: kept, active })
254        }
255        DockNode::Split { axis, children } => {
256            let mut flat: Vec<DockChild> = Vec::with_capacity(children.len());
257            let valid = children.iter().filter(|c| c.share.is_finite() && c.share > 0.0).count().max(1);
258            // A share that makes no sense becomes an even one, and what is lost stays in the sum.
259            let fallback = 1.0 / valid as f32;
260            for child in children {
261                let share = if child.share.is_finite() && child.share > 0.0 { child.share } else { fallback };
262                let Some(node) = tidied(child.node, seen) else { continue };
263                match node {
264                    DockNode::Split { axis: inner, children: grand } if inner == axis => {
265                        flat.extend(
266                            grand.into_iter().map(|g| DockChild { share: g.share * share, node: g.node }),
267                        );
268                    }
269                    node => flat.push(DockChild { share, node }),
270                }
271            }
272            match flat.len() {
273                0 => None,
274                1 => flat.pop().map(|only| only.node),
275                _ => {
276                    let sum: f32 = flat.iter().map(|c| c.share).sum();
277                    if (sum - 1.0).abs() > SHARES_SUM_TOLERANCE {
278                        flat.iter_mut().for_each(|c| c.share /= sum);
279                    }
280                    Some(DockNode::Split { axis, children: flat })
281                }
282            }
283        }
284    }
285}
286
287mod axis_name {
288    use ravenrs_layout::Axis;
289    use serde::{Deserialize, Deserializer, Serializer, de};
290
291    pub(super) fn serialize<S: Serializer>(axis: &Axis, out: S) -> Result<S::Ok, S::Error> {
292        out.serialize_str(if *axis == Axis::Vertical { "vertical" } else { "horizontal" })
293    }
294
295    pub(super) fn deserialize<'de, D: Deserializer<'de>>(input: D) -> Result<Axis, D::Error> {
296        match String::deserialize(input)?.as_str() {
297            "horizontal" => Ok(Axis::Horizontal),
298            "vertical" => Ok(Axis::Vertical),
299            other => Err(de::Error::custom(format!("no axis called {other}"))),
300        }
301    }
302}
303
304/// A dock: what is there, its size in logical pixels, and whether it shows.
305#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
306struct Region {
307    #[serde(default)]
308    node: Option<DockNode>,
309    size: f32,
310    #[serde(default = "shown")]
311    open: bool,
312}
313
314fn shown() -> bool {
315    true
316}
317
318#[cfg_attr(feature = "live", ravenrs_live::expose)]
319impl Region {
320    fn new(size: f32) -> Self {
321        Self { node: None, size, open: true }
322    }
323}
324
325fn left() -> Region {
326    Region::new(260.0)
327}
328
329fn right() -> Region {
330    Region::new(280.0)
331}
332
333fn bottom() -> Region {
334    Region::new(200.0)
335}
336
337/// A panel out of the tree, with where to bring it back.
338#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
339struct Closed {
340    panel: String,
341    place: DockSide,
342    /// Who stayed in its group as it closed.
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    with: Option<String>,
345}
346
347/// The arrangement of a [`DockArea`](crate::DockArea): which panel is where, the sizes, and what
348/// is collapsed or closed. It holds ids only, so it can be kept in a `State`, compared and saved;
349/// what a panel shows comes from the app, by id, each time it is drawn.
350///
351/// ```
352/// use ravenrs_widgets::{DockDrop, DockLayout, DockNode, DockSide};
353///
354/// let mut layout = DockLayout::new()
355///     .center(DockNode::tabs(["photo"]))
356///     .left(DockNode::tabs(["folders", "search"]), 240.0);
357/// layout.move_panel("search", DockDrop::Side(DockSide::Center));
358/// assert_eq!(layout.side_of("search"), Some(DockSide::Center));
359///
360/// let saved = layout.to_text();
361/// assert_eq!(DockLayout::parse(&saved), Ok(layout));
362/// assert!(DockLayout::parse("not a layout").is_err());
363/// ```
364#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
365#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
366pub struct DockLayout {
367    #[serde(default)]
368    center: Option<DockNode>,
369    #[serde(default = "left")]
370    left: Region,
371    #[serde(default = "right")]
372    right: Region,
373    #[serde(default = "bottom")]
374    bottom: Region,
375    #[serde(default)]
376    closed: Vec<Closed>,
377}
378
379impl Default for DockLayout {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385/// Why a saved text could not be read. Nothing was changed; the app goes on with the arrangement
386/// it has, or the one it starts from.
387#[derive(Clone, Debug, PartialEq, Eq)]
388#[non_exhaustive]
389#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
390pub enum DockLayoutError {
391    /// It is not well-formed text in the format.
392    Syntax {
393        /// The line of the problem, from one.
394        line: usize,
395        /// The column of the problem, from one.
396        column: usize,
397        /// What was wrong.
398        message: String,
399    },
400    /// It is well-formed, but says nothing of being an arrangement: it has no version.
401    NotALayout,
402    /// It was saved by a newer build, in a version this one cannot read.
403    UnsupportedVersion(u64),
404    /// It has the version, but a part of it is not what the version says.
405    Invalid(String),
406}
407
408impl fmt::Display for DockLayoutError {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            Self::Syntax { line, column, message } => {
412                write!(f, "the dock layout cannot be read at line {line}, column {column}: {message}")
413            }
414            Self::NotALayout => f.write_str("this text is not a dock layout: it has no version"),
415            Self::UnsupportedVersion(version) => write!(
416                f,
417                "this dock layout has version {version}, and this build reads up to {FORMAT_VERSION}"
418            ),
419            Self::Invalid(message) => write!(f, "this dock layout is invalid: {message}"),
420        }
421    }
422}
423
424impl std::error::Error for DockLayoutError {}
425
426#[derive(Serialize)]
427struct Saved<'a> {
428    version: u64,
429    #[serde(flatten)]
430    layout: &'a DockLayout,
431}
432
433#[cfg_attr(feature = "live", ravenrs_live::expose)]
434impl DockLayout {
435    /// Nothing anywhere yet, with the docks at their usual sizes.
436    pub fn new() -> Self {
437        Self { center: None, left: left(), right: right(), bottom: bottom(), closed: Vec::new() }
438    }
439
440    /// Puts this in the center.
441    pub fn center(mut self, node: DockNode) -> Self {
442        self.center = Some(node);
443        self.tidy();
444        self
445    }
446
447    /// Puts this in the left dock, `size` logical pixels wide.
448    pub fn left(self, node: DockNode, size: f32) -> Self {
449        self.dock(DockSide::Left, node, size)
450    }
451
452    /// Puts this in the right dock, `size` logical pixels wide.
453    pub fn right(self, node: DockNode, size: f32) -> Self {
454        self.dock(DockSide::Right, node, size)
455    }
456
457    /// Puts this in the bottom dock, `size` logical pixels tall.
458    pub fn bottom(self, node: DockNode, size: f32) -> Self {
459        self.dock(DockSide::Bottom, node, size)
460    }
461
462    /// Starts with these panels closed, ready to be reopened in `place`.
463    pub fn closed<S: Into<String>>(mut self, panels: impl IntoIterator<Item = S>, place: DockSide) -> Self {
464        for panel in panels {
465            self.closed.push(Closed { panel: panel.into(), place, with: None });
466        }
467        self.tidy();
468        self
469    }
470
471    fn dock(mut self, side: DockSide, node: DockNode, size: f32) -> Self {
472        if let Some(region) = self.region_mut(side) {
473            region.node = Some(node);
474        }
475        self.set_size(side, size);
476        self.tidy();
477        self
478    }
479
480    fn region(&self, side: DockSide) -> Option<&Region> {
481        match side {
482            DockSide::Left => Some(&self.left),
483            DockSide::Right => Some(&self.right),
484            DockSide::Bottom => Some(&self.bottom),
485            DockSide::Center => None,
486        }
487    }
488
489    fn region_mut(&mut self, side: DockSide) -> Option<&mut Region> {
490        match side {
491            DockSide::Left => Some(&mut self.left),
492            DockSide::Right => Some(&mut self.right),
493            DockSide::Bottom => Some(&mut self.bottom),
494            DockSide::Center => None,
495        }
496    }
497
498    /// What is in this place.
499    pub fn node(&self, side: DockSide) -> Option<&DockNode> {
500        match side {
501            DockSide::Center => self.center.as_ref(),
502            dock => self.region(dock).and_then(|region| region.node.as_ref()),
503        }
504    }
505
506    fn node_mut(&mut self, side: DockSide) -> Option<&mut DockNode> {
507        match side {
508            DockSide::Center => self.center.as_mut(),
509            dock => self.region_mut(dock).and_then(|region| region.node.as_mut()),
510        }
511    }
512
513    /// The size of a dock in logical pixels, kept while it is collapsed. The center has none.
514    pub fn size(&self, side: DockSide) -> f32 {
515        self.region(side).map_or(0.0, |region| region.size)
516    }
517
518    /// Sets the size of a dock. A size that is no number is ignored.
519    pub fn set_size(&mut self, side: DockSide, size: f32) {
520        if size.is_finite()
521            && let Some(region) = self.region_mut(side)
522        {
523            region.size = size.max(0.0);
524        }
525    }
526
527    /// Whether a dock shows; the center always does.
528    pub fn is_open(&self, side: DockSide) -> bool {
529        self.region(side).is_none_or(|region| region.open)
530    }
531
532    /// Shows or collapses a dock. It keeps its size and its panels for when it comes back.
533    pub fn set_open(&mut self, side: DockSide, open: bool) {
534        if let Some(region) = self.region_mut(side) {
535            region.open = open;
536        }
537    }
538
539    /// Whether a panel is in the tree, whether or not it is what shows.
540    pub fn contains(&self, id: &str) -> bool {
541        self.side_of(id).is_some()
542    }
543
544    /// The place a panel is in, if it is not closed.
545    pub fn side_of(&self, id: &str) -> Option<DockSide> {
546        DockSide::ALL.into_iter().find(|side| self.node(*side).is_some_and(|node| node.holds(id)))
547    }
548
549    /// The ids in the tree, center first.
550    pub fn panels(&self) -> Vec<&str> {
551        let mut out = Vec::new();
552        for side in DockSide::ALL {
553            if let Some(node) = self.node(side) {
554                node.collect_ids(&mut out);
555            }
556        }
557        out
558    }
559
560    /// The ids closed, in the order they closed.
561    pub fn closed_panels(&self) -> Vec<&str> {
562        self.closed.iter().map(|closed| closed.panel.as_str()).collect()
563    }
564
565    /// Whether this panel is the one its group shows.
566    pub fn is_active(&self, id: &str) -> bool {
567        self.side_of(id).and_then(|side| self.node(side)).and_then(|node| node.shows(id)) == Some(true)
568    }
569
570    fn group_of(&mut self, id: &str) -> Option<(&mut Vec<String>, &mut usize)> {
571        let side = self.side_of(id)?;
572        self.node_mut(side)?.group_of(id)
573    }
574
575    /// Brings a panel to the front of its group, and shows its dock if that was collapsed. It
576    /// says whether the panel is there at all; for one that was closed, see [`Self::reopen`].
577    pub fn activate(&mut self, id: &str) -> bool {
578        let Some(side) = self.side_of(id) else { return false };
579        if let Some((panels, active)) = self.group_of(id) {
580            *active = panels.iter().position(|panel| panel == id).unwrap_or(*active);
581        }
582        self.set_open(side, true);
583        true
584    }
585
586    /// Closes a panel, remembering where it was for [`Self::reopen`]. A group left empty goes
587    /// away, and a split left with one part gives way to it. The last panel can close too: an
588    /// arrangement with nothing open is still valid.
589    pub fn close(&mut self, id: &str) -> bool {
590        let Some((place, with)) = self.take_out(id) else { return false };
591        self.closed.retain(|closed| closed.panel != id);
592        self.closed.push(Closed { panel: id.to_owned(), place, with });
593        self.tidy();
594        true
595    }
596
597    /// Brings a closed panel back to the group it was in, or to the place if that group is gone,
598    /// and shows it. It says whether there was such a panel to bring back.
599    pub fn reopen(&mut self, id: &str) -> bool {
600        let Some(at) = self.closed.iter().position(|closed| closed.panel == id) else { return false };
601        let closed = self.closed.remove(at);
602        let group = closed.with.filter(|with| self.side_of(with) == Some(closed.place));
603        match group {
604            Some(beside) => {
605                self.insert_tab(id, &beside, usize::MAX);
606            }
607            None => self.insert_in(id, closed.place),
608        }
609        self.tidy();
610        true
611    }
612
613    /// Sends a panel somewhere: into a group, beside it, or into a place; a closed one reopens
614    /// there. It says whether anything changed, which it does not if the target is gone, or the
615    /// panel is already what it would become.
616    pub fn move_panel(&mut self, id: &str, target: DockDrop) -> bool {
617        let closed = self.closed.iter().any(|closed| closed.panel == id);
618        if !closed && !self.contains(id) {
619            return false;
620        }
621        let done = match target {
622            DockDrop::Tab { beside, at } => self.move_to_tab(id, &beside, at),
623            DockDrop::Split { beside, edge } => self.move_to_split(id, &beside, edge),
624            DockDrop::Side(side) => self.move_to_side(id, side),
625        };
626        if done {
627            self.closed.retain(|closed| closed.panel != id);
628            self.tidy();
629        }
630        done
631    }
632
633    fn move_to_tab(&mut self, id: &str, beside: &str, at: usize) -> bool {
634        let Some((panels, active)) = self.group_of(beside) else { return false };
635        if let Some(from) = panels.iter().position(|panel| panel == id) {
636            // Within the group the gaps count the tab itself, as in any strip.
637            let Some(to) = reorder::landing(from, at.min(panels.len())) else { return false };
638            let shown = panels.get(*active).cloned();
639            let moved = panels.remove(from);
640            panels.insert(to, moved);
641            *active = shown.and_then(|s| panels.iter().position(|p| *p == s)).unwrap_or(to);
642            return true;
643        }
644        self.take_out(id);
645        self.insert_tab(id, beside, at)
646    }
647
648    fn move_to_split(&mut self, id: &str, beside: &str, edge: DockEdge) -> bool {
649        // Splitting its own group leaves the rest of it as the other part.
650        let beside = if beside == id {
651            let Some((panels, _)) = self.group_of(id) else { return false };
652            let Some(other) = panels.iter().find(|panel| *panel != id).cloned() else { return false };
653            other
654        } else {
655            beside.to_owned()
656        };
657        let Some(side) = self.side_of(&beside) else { return false };
658        self.take_out(id);
659        let done = self.node_mut(side).is_some_and(|node| node.split_group(&beside, id, edge));
660        self.set_open(side, true);
661        done
662    }
663
664    fn move_to_side(&mut self, id: &str, side: DockSide) -> bool {
665        if self.side_of(id) == Some(side) {
666            return false;
667        }
668        self.take_out(id);
669        self.insert_in(id, side);
670        true
671    }
672
673    /// Takes a panel out of the tree, leaving the rest to be tidied.
674    fn take_out(&mut self, id: &str) -> Option<(DockSide, Option<String>)> {
675        let side = self.side_of(id)?;
676        let with = self.node_mut(side)?.remove(id)?;
677        Some((side, with))
678    }
679
680    fn insert_tab(&mut self, id: &str, beside: &str, at: usize) -> bool {
681        let side = self.side_of(beside);
682        let Some((panels, active)) = self.group_of(beside) else { return false };
683        let at = at.min(panels.len());
684        panels.insert(at, id.to_owned());
685        *active = at;
686        if let Some(side) = side {
687            self.set_open(side, true);
688        }
689        true
690    }
691
692    fn insert_in(&mut self, id: &str, side: DockSide) {
693        if self.node(side).is_none() {
694            let node = Some(DockNode::tabs([id]));
695            match self.region_mut(side) {
696                Some(region) => region.node = node,
697                None => self.center = node,
698            }
699        } else if let Some((panels, active)) = self.node_mut(side).and_then(DockNode::first_group) {
700            panels.push(id.to_owned());
701            *active = panels.len() - 1;
702        }
703        self.set_open(side, true);
704    }
705
706    /// Fits the arrangement to the panels that exist: an id not among them is left out, as if it
707    /// had never been saved, and one of them that is nowhere becomes closed, ready to be reopened.
708    /// It says whether anything changed.
709    pub fn reconcile(&mut self, known: &[&str]) -> bool {
710        let before = self.clone();
711        let unknown: Vec<String> =
712            self.panels().into_iter().filter(|id| !known.contains(id)).map(str::to_owned).collect();
713        for id in unknown {
714            self.take_out(&id);
715        }
716        self.closed.retain(|closed| known.contains(&closed.panel.as_str()));
717        for id in known {
718            if !self.contains(id) && !self.closed.iter().any(|closed| closed.panel == *id) {
719                self.closed.push(Closed { panel: (*id).to_owned(), place: DockSide::Center, with: None });
720            }
721        }
722        self.tidy();
723        *self != before
724    }
725
726    /// The first part of a pair in a split gets `first` out of `both` logical pixels; the pair
727    /// keeps the share it has together. Other parts do not move.
728    pub(crate) fn resize_split(
729        &mut self,
730        side: DockSide,
731        path: &[usize],
732        index: usize,
733        first: f32,
734        both: f32,
735    ) {
736        if both <= 0.0 || !first.is_finite() {
737            return;
738        }
739        let Some(children) = self.node_mut(side).and_then(|node| node.split_at(path)) else { return };
740        let total = match (children.get(index), children.get(index + 1)) {
741            (Some(a), Some(b)) => a.share + b.share,
742            _ => return,
743        };
744        // Never quite nothing: a share of zero would read as a share that makes no sense.
745        let part = (first / both).clamp(0.01, 0.99) * total;
746        children[index].share = part;
747        children[index + 1].share = total - part;
748    }
749
750    /// Writes it as text to keep, in a versioned format: JSON with a `version` at the top.
751    pub fn to_text(&self) -> String {
752        // Every map here has string keys and sizes are kept finite, so this cannot fail.
753        serde_json::to_string_pretty(&Saved { version: FORMAT_VERSION, layout: self }).unwrap_or_default()
754    }
755
756    /// Reads a text from [`Self::to_text`]. What makes no sense but does no harm, such as a panel
757    /// twice, an empty group or a share below zero, is put right; what cannot be read is an error,
758    /// never a panic.
759    pub fn parse(text: &str) -> Result<Self, DockLayoutError> {
760        let value: serde_json::Value = serde_json::from_str(text).map_err(|error| {
761            DockLayoutError::Syntax { line: error.line(), column: error.column(), message: error.to_string() }
762        })?;
763        let version = value.get("version").and_then(serde_json::Value::as_u64);
764        match version {
765            None => return Err(DockLayoutError::NotALayout),
766            Some(version) if version > FORMAT_VERSION || version == 0 => {
767                return Err(DockLayoutError::UnsupportedVersion(version));
768            }
769            Some(_) => {}
770        }
771        let mut layout: Self =
772            serde_json::from_value(value).map_err(|error| DockLayoutError::Invalid(error.to_string()))?;
773        for side in DockSide::DOCKS {
774            let size = layout.size(side);
775            layout.set_size(side, size);
776        }
777        layout.tidy();
778        Ok(layout)
779    }
780
781    /// Back to the simplest form after any change.
782    fn tidy(&mut self) {
783        let mut seen = HashSet::new();
784        self.center = self.center.take().and_then(|node| tidied(node, &mut seen));
785        for side in DockSide::DOCKS {
786            if let Some(region) = self.region_mut(side) {
787                region.node = region.node.take().and_then(|node| tidied(node, &mut seen));
788            }
789        }
790        // The tree wins over a closed entry: a panel cannot be in both.
791        self.closed.retain(|closed| seen.insert(closed.panel.clone()));
792    }
793}
794
795#[cfg(test)]
796mod tests {
797    use proptest::prelude::*;
798
799    use super::*;
800
801    fn sample() -> DockLayout {
802        DockLayout::new()
803            .center(DockNode::horizontal([DockNode::tabs(["a", "b"]), DockNode::tabs(["c"])]))
804            .left(DockNode::tabs(["d", "e"]), 240.0)
805            .bottom(DockNode::tabs(["f"]), 180.0)
806    }
807
808    #[test]
809    fn a_group_left_empty_goes_and_its_split_gives_way() {
810        let mut layout = sample();
811        assert!(layout.close("c"));
812        assert_eq!(layout.node(DockSide::Center), Some(&DockNode::tabs(["a", "b"])));
813    }
814
815    #[test]
816    fn closing_keeps_the_tab_that_shows_and_a_neighbor_takes_over() {
817        let mut layout = DockLayout::new().center(DockNode::tabs(["a", "b", "c"]));
818        layout.activate("c");
819        layout.close("a");
820        assert!(layout.is_active("c"), "what showed goes on showing");
821        layout.close("c");
822        assert!(layout.is_active("b"));
823    }
824
825    #[test]
826    fn the_last_panel_can_close_and_comes_back_where_it_was() {
827        let mut layout = DockLayout::new().left(DockNode::tabs(["a"]), 200.0);
828        layout.close("a");
829        assert!(layout.panels().is_empty());
830        assert_eq!(layout.closed_panels(), ["a"]);
831        assert_eq!(DockLayout::parse(&layout.to_text()), Ok(layout.clone()));
832        assert!(layout.reopen("a"));
833        assert_eq!(layout.side_of("a"), Some(DockSide::Left));
834        assert!(layout.closed_panels().is_empty());
835        assert!(!layout.reopen("a"), "it is open already");
836    }
837
838    #[test]
839    fn a_panel_reopens_in_the_group_it_left() {
840        let mut layout = sample();
841        layout.close("b");
842        layout.reopen("b");
843        let Some(DockNode::Split { children, .. }) = layout.node(DockSide::Center) else { panic!("a split") };
844        assert_eq!(children[0].node, DockNode::Tabs { panels: vec!["a".into(), "b".into()], active: 1 });
845    }
846
847    #[test]
848    fn a_tab_reorders_within_its_group_by_gaps() {
849        let mut layout = DockLayout::new().center(DockNode::tabs(["a", "b", "c"]));
850        assert!(layout.move_panel("a", DockDrop::Tab { beside: "c".into(), at: 3 }));
851        assert_eq!(
852            layout.node(DockSide::Center),
853            Some(&DockNode::Tabs { panels: vec!["b".into(), "c".into(), "a".into()], active: 2 })
854        );
855        assert!(!layout.move_panel("a", DockDrop::Tab { beside: "a".into(), at: 3 }), "it is there already");
856        assert!(!layout.move_panel("a", DockDrop::Tab { beside: "a".into(), at: 2 }), "the gap beside it");
857    }
858
859    #[test]
860    fn a_tab_joins_another_group_where_it_is_dropped_and_shows() {
861        let mut layout = sample();
862        assert!(layout.move_panel("d", DockDrop::Tab { beside: "a".into(), at: 1 }));
863        assert!(layout.is_active("d"));
864        let Some(DockNode::Split { children, .. }) = layout.node(DockSide::Center) else { panic!("a split") };
865        assert_eq!(
866            children[0].node,
867            DockNode::Tabs { panels: vec!["a".into(), "d".into(), "b".into()], active: 1 }
868        );
869    }
870
871    #[test]
872    fn a_split_opens_on_the_edge_asked_for() {
873        let mut layout = DockLayout::new().center(DockNode::tabs(["a", "b"]));
874        assert!(layout.move_panel("b", DockDrop::Split { beside: "a".into(), edge: DockEdge::Top }));
875        let Some(DockNode::Split { axis, children }) = layout.node(DockSide::Center) else {
876            panic!("a split")
877        };
878        assert_eq!(*axis, Axis::Vertical);
879        assert_eq!(children[0].node, DockNode::tabs(["b"]));
880        assert_eq!(children[1].node, DockNode::tabs(["a"]));
881    }
882
883    #[test]
884    fn a_panel_alone_cannot_split_from_itself() {
885        let mut layout = DockLayout::new().center(DockNode::tabs(["a"]));
886        assert!(!layout.move_panel("a", DockDrop::Split { beside: "a".into(), edge: DockEdge::Left }));
887        assert!(layout.contains("a"));
888        let mut two = DockLayout::new().center(DockNode::tabs(["a", "b"]));
889        assert!(two.move_panel("a", DockDrop::Split { beside: "a".into(), edge: DockEdge::Left }));
890    }
891
892    #[test]
893    fn a_split_along_the_same_axis_joins_its_parent_with_the_same_room() {
894        let mut layout = DockLayout::new()
895            .center(DockNode::horizontal([DockNode::tabs(["a", "x"]), DockNode::tabs(["b"])]));
896        assert!(layout.move_panel("x", DockDrop::Split { beside: "a".into(), edge: DockEdge::Right }));
897        let Some(DockNode::Split { children, .. }) = layout.node(DockSide::Center) else { panic!("a split") };
898        let shares: Vec<f32> = children.iter().map(|c| c.share).collect();
899        assert_eq!(shares, [0.25, 0.25, 0.5]);
900    }
901
902    #[test]
903    fn moving_into_a_collapsed_dock_shows_it() {
904        let mut layout = sample();
905        layout.set_open(DockSide::Left, false);
906        layout.move_panel("a", DockDrop::Side(DockSide::Left));
907        assert!(layout.is_open(DockSide::Left));
908        assert!(!layout.move_panel("a", DockDrop::Side(DockSide::Left)), "it is there already");
909    }
910
911    #[test]
912    fn a_target_that_is_gone_changes_nothing() {
913        let mut layout = sample();
914        let before = layout.clone();
915        assert!(!layout.move_panel("a", DockDrop::Tab { beside: "nobody".into(), at: 0 }));
916        assert!(!layout.move_panel("a", DockDrop::Split { beside: "nobody".into(), edge: DockEdge::Top }));
917        assert!(!layout.move_panel("nobody", DockDrop::Side(DockSide::Left)));
918        assert_eq!(layout, before);
919    }
920
921    #[test]
922    fn an_unknown_id_is_left_out_and_a_new_panel_is_ready_to_reopen() {
923        let mut layout = sample();
924        layout.close("e");
925        assert!(layout.reconcile(&["a", "c", "d", "f", "new"]));
926        assert_eq!(layout.panels(), ["a", "c", "d", "f"]);
927        assert_eq!(layout.closed_panels(), ["new"]);
928        assert!(!layout.reconcile(&["a", "c", "d", "f", "new"]), "nothing is left to do");
929    }
930
931    #[test]
932    fn a_saved_text_reads_back_the_same_and_stays_the_same_text() {
933        let mut layout = sample();
934        layout.set_open(DockSide::Left, false);
935        layout.close("e");
936        layout.resize_split(DockSide::Center, &[], 0, 100.0, 300.0);
937        let text = layout.to_text();
938        let read = DockLayout::parse(&text).expect("it reads");
939        assert_eq!(read.to_text(), text);
940        assert_eq!(read, layout);
941    }
942
943    #[test]
944    fn what_cannot_be_read_gives_a_clear_error() {
945        let syntax = DockLayout::parse("{ not json").expect_err("broken");
946        assert!(matches!(syntax, DockLayoutError::Syntax { line: 1, .. }));
947        assert!(syntax.to_string().contains("line 1"));
948        assert_eq!(DockLayout::parse("{}"), Err(DockLayoutError::NotALayout));
949        assert_eq!(DockLayout::parse("[1, 2]"), Err(DockLayoutError::NotALayout));
950        assert_eq!(DockLayout::parse(r#"{"version": 99}"#), Err(DockLayoutError::UnsupportedVersion(99)));
951        assert_eq!(DockLayout::parse(r#"{"version": 0}"#), Err(DockLayoutError::UnsupportedVersion(0)));
952        let wrong = r#"{"version": 1, "center": {"split": {"axis": "sideways", "children": []}}}"#;
953        let invalid = DockLayout::parse(wrong).expect_err("no such axis");
954        assert!(invalid.to_string().contains("sideways"), "{invalid}");
955        let deep = format!("{}{}", "[".repeat(100_000), "]".repeat(100_000));
956        assert!(DockLayout::parse(&deep).is_err(), "too deep is an error, not an overflow of the stack");
957    }
958
959    #[test]
960    fn nonsense_in_a_text_is_put_right() {
961        let text = r#"{"version": 1,
962            "center": {"split": {"axis": "horizontal", "children": [
963                {"share": -3, "node": {"tabs": {"panels": ["a", "a", "b"], "active": 9}}},
964                {"share": 1e30, "node": {"tabs": {"panels": []}}},
965                {"share": 2, "node": {"tabs": {"panels": ["a", "c"]}}}
966            ]}},
967            "left": {"size": -5}}"#;
968        let layout = DockLayout::parse(text).expect("it reads");
969        assert_eq!(layout.panels(), ["a", "b", "c"], "each panel once");
970        assert_eq!(layout.size(DockSide::Left), 0.0);
971        let Some(DockNode::Split { children, .. }) = layout.node(DockSide::Center) else { panic!("a split") };
972        assert_eq!(children.len(), 2, "the empty group went");
973        let sum: f32 = children.iter().map(|c| c.share).sum();
974        assert!((sum - 1.0).abs() < 1e-3);
975        assert_eq!(children[0].node, DockNode::Tabs { panels: vec!["a".into(), "b".into()], active: 1 });
976    }
977
978    const IDS: [&str; 7] = ["a", "b", "c", "d", "e", "f", "g"];
979
980    #[derive(Clone, Debug)]
981    enum Op {
982        Close(usize),
983        Reopen(usize),
984        Activate(usize),
985        Tab(usize, usize, usize),
986        Split(usize, usize, u8),
987        Side(usize, u8),
988        Toggle(u8),
989        Resize(f32),
990    }
991
992    fn op() -> impl Strategy<Value = Op> {
993        let id = 0..IDS.len();
994        prop_oneof![
995            id.clone().prop_map(Op::Close),
996            id.clone().prop_map(Op::Reopen),
997            id.clone().prop_map(Op::Activate),
998            (id.clone(), id.clone(), 0..9_usize).prop_map(|(a, b, at)| Op::Tab(a, b, at)),
999            (id.clone(), id.clone(), 0..4_u8).prop_map(|(a, b, edge)| Op::Split(a, b, edge)),
1000            (id, 0..4_u8).prop_map(|(a, side)| Op::Side(a, side)),
1001            (0..3_u8).prop_map(Op::Toggle),
1002            (0.0..800.0_f32).prop_map(Op::Resize),
1003        ]
1004    }
1005
1006    fn side(index: u8) -> DockSide {
1007        [DockSide::Left, DockSide::Right, DockSide::Bottom, DockSide::Center][usize::from(index % 4)]
1008    }
1009
1010    fn apply(layout: &mut DockLayout, op: &Op) {
1011        match *op {
1012            Op::Close(a) => {
1013                layout.close(IDS[a]);
1014            }
1015            Op::Reopen(a) => {
1016                layout.reopen(IDS[a]);
1017            }
1018            Op::Activate(a) => {
1019                layout.activate(IDS[a]);
1020            }
1021            Op::Tab(a, b, at) => {
1022                layout.move_panel(IDS[a], DockDrop::Tab { beside: IDS[b].into(), at });
1023            }
1024            Op::Split(a, b, edge) => {
1025                let edge =
1026                    [DockEdge::Left, DockEdge::Right, DockEdge::Top, DockEdge::Bottom][usize::from(edge % 4)];
1027                layout.move_panel(IDS[a], DockDrop::Split { beside: IDS[b].into(), edge });
1028            }
1029            Op::Side(a, s) => {
1030                layout.move_panel(IDS[a], DockDrop::Side(side(s)));
1031            }
1032            Op::Toggle(s) => layout.set_open(side(s), !layout.is_open(side(s))),
1033            Op::Resize(px) => layout.resize_split(DockSide::Center, &[], 0, px, 800.0),
1034        }
1035    }
1036
1037    fn check_node(node: &DockNode, parent: Option<Axis>) -> Result<(), TestCaseError> {
1038        match node {
1039            DockNode::Tabs { panels, active } => {
1040                prop_assert!(!panels.is_empty(), "no empty group");
1041                prop_assert!(*active < panels.len(), "what shows is there");
1042            }
1043            DockNode::Split { axis, children } => {
1044                prop_assert!(children.len() >= 2, "no split of one part");
1045                prop_assert!(parent != Some(*axis), "no split inside one along the same axis");
1046                let sum: f32 = children.iter().map(|c| c.share).sum();
1047                prop_assert!((sum - 1.0).abs() < 0.01, "shares add up, got {sum}");
1048                for child in children {
1049                    prop_assert!(child.share.is_finite() && child.share >= 0.0);
1050                    check_node(&child.node, Some(*axis))?;
1051                }
1052            }
1053        }
1054        Ok(())
1055    }
1056
1057    proptest! {
1058        #[test]
1059        fn any_series_of_moves_keeps_the_tree_valid_and_no_panel_lost(ops in proptest::collection::vec(op(), 0..60)) {
1060            let mut layout = DockLayout::new()
1061                .center(DockNode::horizontal([DockNode::tabs(["a", "b"]), DockNode::vertical([DockNode::tabs(["c"]), DockNode::tabs(["d"])])]))
1062                .left(DockNode::tabs(["e", "f"]), 200.0)
1063                .bottom(DockNode::tabs(["g"]), 160.0);
1064            for op in &ops {
1065                apply(&mut layout, op);
1066                let mut all: Vec<&str> = layout.panels();
1067                all.extend(layout.closed_panels());
1068                all.sort_unstable();
1069                prop_assert_eq!(all, IDS.to_vec(), "each panel exactly once, after {:?}", op);
1070                for side in DockSide::ALL {
1071                    if let Some(node) = layout.node(side) {
1072                        check_node(node, None)?;
1073                    }
1074                }
1075            }
1076            let text = layout.to_text();
1077            let read = DockLayout::parse(&text);
1078            prop_assert!(read.is_ok(), "it reads back: {:?}", read);
1079            prop_assert_eq!(read.map(|r| r.to_text()).unwrap_or_default(), text);
1080        }
1081
1082        #[test]
1083        fn no_text_makes_it_panic(text in "\\PC{0,200}") {
1084            let _ = DockLayout::parse(&text);
1085        }
1086
1087        #[test]
1088        fn no_tree_in_a_text_makes_it_panic(
1089            active in 0..50_usize,
1090            share in proptest::num::f32::ANY,
1091            size in proptest::num::f32::ANY,
1092            name in "[a-z\"\\\\é]{0,6}",
1093        ) {
1094            let share = serde_json::to_string(&share).unwrap_or_default();
1095            let size = serde_json::to_string(&size).unwrap_or_default();
1096            let name = serde_json::to_string(&name).unwrap_or_default();
1097            let text = format!(
1098                r#"{{"version":1,"center":{{"split":{{"axis":"vertical","children":[
1099                {{"share":{share},"node":{{"tabs":{{"panels":[{name},{name}],"active":{active}}}}}}},
1100                {{"share":{share},"node":{{"tabs":{{"panels":["z"]}}}}}}]}}}},"left":{{"size":{size}}}}}"#
1101            );
1102            if let Ok(layout) = DockLayout::parse(&text) {
1103                prop_assert!(layout.size(DockSide::Left).is_finite());
1104                prop_assert!(DockLayout::parse(&layout.to_text()).is_ok());
1105            }
1106        }
1107    }
1108}