Skip to main content

densa_widgets/dock/
layout.rs

1//! Saving and restoring how a dock's panels are arranged, by the panels' keys.
2
3use std::fmt;
4use std::str::FromStr;
5
6use densa_ui::{EventCx, Ui, UiError, WidgetId};
7
8use super::{Axis, DockArea, Kind, Node};
9
10/// How deeply splits may nest in a layout: far more than any workspace has, few enough that
11/// reading a damaged or hostile layout cannot exhaust the stack. [`DockArea::layout`] keeps
12/// within it (deeper groups are saved as one), and reading refuses deeper text.
13pub const MAX_LAYOUT_DEPTH: usize = 64;
14
15/// The format's name, before its version.
16const FORMAT: &str = "densa-dock";
17/// The version written, and the only one read.
18const VERSION: &str = "1";
19/// The smallest share of a split either part keeps when restored, so no group vanishes.
20const MIN_RATIO: f32 = 0.01;
21
22/// Which way a split's two parts sit.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum DockAxis {
25    /// Side by side: the first part on the left.
26    Row,
27    /// One above the other: the first part on top.
28    Column,
29}
30
31/// How a dock's panels are arranged, naming panels by their keys
32/// ([`DockPanel::with_key`](super::DockPanel::with_key)): what
33/// [`DockArea::layout`] saves and [`DockArea::restore_layout`] puts back, for example when an
34/// application starts again.
35///
36/// It is written as text (`to_string()`) and read back with [`str::parse`]:
37/// `densa-dock 1 (row 0.6 (tabs 0 "orders" "trades") (tabs 0 "chart"))`, a version then the
38/// tree, each split with its axis (`row`: side by side, `column`: one above the other) and
39/// the fraction its first part takes, each group with the position of its shown tab and
40/// its panels' keys (quoted, `\"` and `\\` escaped). Splits nest:
41/// `densa-dock 1 (column 0.25 (tabs 0 "log") (row 0.6 (tabs 0 "chart") (tabs 0 "hosts")))`
42/// puts the log above, then the chart beside the hosts. A text that is not a layout is
43/// refused with a [`DockLayoutError`] naming where and why. Layouts
44/// built in code are written however deep they are, but only those within
45/// [`MAX_LAYOUT_DEPTH`] read back.
46#[derive(Debug, Clone, PartialEq)]
47#[non_exhaustive]
48pub enum DockLayout {
49    /// Two parts sharing a rectangle along `axis`; `ratio` (0 to 1) of it is the first's.
50    Split {
51        /// Which way the parts sit.
52        axis: DockAxis,
53        /// The fraction of the rectangle the first part takes.
54        ratio: f32,
55        /// The left or upper part.
56        first: Box<DockLayout>,
57        /// The right or lower part.
58        second: Box<DockLayout>,
59    },
60    /// A group of tabs: its panels' keys in tab order, and the position of the shown one.
61    Tabs {
62        /// The panels' keys, in tab order.
63        panels: Vec<String>,
64        /// Which of them is shown.
65        shown: usize,
66    },
67}
68
69/// Why text is not a [`DockLayout`], and where reading stopped.
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[non_exhaustive]
72pub struct DockLayoutError {
73    /// The byte offset in the text where reading stopped.
74    pub at: usize,
75    /// What was wrong there.
76    pub kind: DockLayoutErrorKind,
77}
78
79/// What was wrong with a layout's text.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum DockLayoutErrorKind {
83    /// It does not start as a dock layout.
84    NotALayout,
85    /// A dock layout of another version, written by a later densa.
86    UnsupportedVersion,
87    /// `(` was expected.
88    ExpectedOpen,
89    /// `)` was expected.
90    ExpectedClose,
91    /// `row`, `column` or `tabs` was expected.
92    ExpectedNode,
93    /// A split's ratio is not a number from 0 to 1.
94    BadRatio,
95    /// A group's shown tab is not a position.
96    BadShown,
97    /// A group has no panels, or shows one it does not have.
98    ShownMissing,
99    /// A key has no closing quote.
100    UnclosedKey,
101    /// A key has an escape other than `\"` and `\\`.
102    BadEscape,
103    /// Splits nest deeper than [`MAX_LAYOUT_DEPTH`].
104    TooDeep,
105    /// Text follows the layout.
106    TrailingText,
107}
108
109impl fmt::Display for DockLayoutErrorKind {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(match self {
112            Self::NotALayout => "not a densa dock layout",
113            Self::UnsupportedVersion => "a dock layout of a later version",
114            Self::ExpectedOpen => "expected `(`",
115            Self::ExpectedClose => "expected `)`",
116            Self::ExpectedNode => "expected `row`, `column` or `tabs`",
117            Self::BadRatio => "expected a ratio from 0 to 1",
118            Self::BadShown => "expected the shown tab's position",
119            Self::ShownMissing => "a group must show one of its panels",
120            Self::UnclosedKey => "a key without its closing quote",
121            Self::BadEscape => "only `\\\"` and `\\\\` are escapes",
122            Self::TooDeep => "splits nested too deeply",
123            Self::TrailingText => "text after the layout",
124        })
125    }
126}
127
128impl fmt::Display for DockLayoutError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "invalid dock layout at byte {}: {}", self.at, self.kind)
131    }
132}
133
134impl std::error::Error for DockLayoutError {}
135
136impl fmt::Display for DockLayout {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{FORMAT} {VERSION} ")?;
139        write_node(f, self)
140    }
141}
142
143fn write_node(f: &mut fmt::Formatter<'_>, layout: &DockLayout) -> fmt::Result {
144    match layout {
145        DockLayout::Split {
146            axis,
147            ratio,
148            first,
149            second,
150        } => {
151            let axis = match axis {
152                DockAxis::Row => "row",
153                DockAxis::Column => "column",
154            };
155            // `{}` writes the shortest text that reads back as the same f32; adding 0 turns
156            // -0 into 0.
157            write!(f, "({axis} {} ", ratio + 0.0)?;
158            write_node(f, first)?;
159            f.write_str(" ")?;
160            write_node(f, second)?;
161            f.write_str(")")
162        }
163        DockLayout::Tabs { panels, shown } => {
164            write!(f, "(tabs {shown}")?;
165            for key in panels {
166                f.write_str(" \"")?;
167                for c in key.chars() {
168                    if c == '"' || c == '\\' {
169                        f.write_str("\\")?;
170                    }
171                    write!(f, "{c}")?;
172                }
173                f.write_str("\"")?;
174            }
175            f.write_str(")")
176        }
177    }
178}
179
180impl FromStr for DockLayout {
181    type Err = DockLayoutError;
182
183    /// Reads a layout written by `to_string()`.
184    ///
185    /// # Errors
186    /// [`DockLayoutError`] for another format or version, a ratio that is not a number from
187    /// 0 to 1, a group without panels or showing a tab it lacks, splits nested deeper than
188    /// [`MAX_LAYOUT_DEPTH`], or anything after the layout; see [`DockLayoutErrorKind`].
189    fn from_str(text: &str) -> Result<Self, Self::Err> {
190        use DockLayoutErrorKind::{NotALayout, TrailingText, UnsupportedVersion};
191        let mut reader = Reader { text, at: 0 };
192        reader.skip_space();
193        if !reader.rest().starts_with(FORMAT) {
194            return reader.fail(NotALayout);
195        }
196        reader.at += FORMAT.len();
197        if !reader.rest().starts_with(char::is_whitespace) {
198            return reader.fail(NotALayout);
199        }
200        reader.skip_space();
201        let start = reader.at;
202        let version = reader.word();
203        let known = version == VERSION;
204        let numbered = !version.is_empty() && version.bytes().all(|b| b.is_ascii_digit());
205        if !known || !reader.rest().starts_with(char::is_whitespace) {
206            reader.at = start;
207            return reader.fail(if numbered && !known {
208                UnsupportedVersion
209            } else {
210                NotALayout
211            });
212        }
213        let layout = reader.node(0)?;
214        reader.skip_space();
215        if !reader.rest().is_empty() {
216            return reader.fail(TrailingText);
217        }
218        Ok(layout)
219    }
220}
221
222/// Reads a layout from its text, a token at a time.
223struct Reader<'a> {
224    text: &'a str,
225    at: usize,
226}
227
228impl Reader<'_> {
229    fn rest(&self) -> &str {
230        &self.text[self.at..]
231    }
232
233    fn fail<T>(&self, kind: DockLayoutErrorKind) -> Result<T, DockLayoutError> {
234        Err(DockLayoutError { at: self.at, kind })
235    }
236
237    fn skip_space(&mut self) {
238        let rest = self.rest();
239        self.at += rest.len() - rest.trim_start().len();
240    }
241
242    /// Takes `c` after any space.
243    fn eat(&mut self, c: char) -> bool {
244        self.skip_space();
245        let found = self.rest().starts_with(c);
246        if found {
247            self.at += c.len_utf8();
248        }
249        found
250    }
251
252    /// A run of letters, digits and number signs after any space.
253    fn word(&mut self) -> &str {
254        self.skip_space();
255        let rest = self.rest();
256        let len = rest
257            .find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+')))
258            .unwrap_or(rest.len());
259        let start = self.at;
260        self.at += len;
261        &self.text[start..self.at]
262    }
263
264    fn node(&mut self, depth: usize) -> Result<DockLayout, DockLayoutError> {
265        if depth > MAX_LAYOUT_DEPTH {
266            return self.fail(DockLayoutErrorKind::TooDeep);
267        }
268        if !self.eat('(') {
269            return self.fail(DockLayoutErrorKind::ExpectedOpen);
270        }
271        let start = self.at;
272        let layout = match self.word() {
273            "row" => self.split(DockAxis::Row, depth)?,
274            "column" => self.split(DockAxis::Column, depth)?,
275            "tabs" => self.tabs()?,
276            _ => {
277                self.at = start;
278                return self.fail(DockLayoutErrorKind::ExpectedNode);
279            }
280        };
281        if !self.eat(')') {
282            return self.fail(DockLayoutErrorKind::ExpectedClose);
283        }
284        Ok(layout)
285    }
286
287    fn split(&mut self, axis: DockAxis, depth: usize) -> Result<DockLayout, DockLayoutError> {
288        self.skip_space();
289        let start = self.at;
290        let ratio = match self.word().parse::<f32>() {
291            Ok(ratio) if (0.0..=1.0).contains(&ratio) => ratio,
292            _ => {
293                self.at = start;
294                return self.fail(DockLayoutErrorKind::BadRatio);
295            }
296        };
297        let first = Box::new(self.node(depth + 1)?);
298        let second = Box::new(self.node(depth + 1)?);
299        Ok(DockLayout::Split {
300            axis,
301            ratio,
302            first,
303            second,
304        })
305    }
306
307    fn tabs(&mut self) -> Result<DockLayout, DockLayoutError> {
308        self.skip_space();
309        let start = self.at;
310        let Ok(shown) = self.word().parse::<usize>() else {
311            self.at = start;
312            return self.fail(DockLayoutErrorKind::BadShown);
313        };
314        let mut panels = Vec::new();
315        while self.eat('"') {
316            panels.push(self.key()?);
317        }
318        if shown >= panels.len() {
319            self.at = start;
320            return self.fail(DockLayoutErrorKind::ShownMissing);
321        }
322        Ok(DockLayout::Tabs { panels, shown })
323    }
324
325    /// A key's text after its opening quote, up to and past its closing one.
326    fn key(&mut self) -> Result<String, DockLayoutError> {
327        let mut key = String::new();
328        let mut chars = self.rest().char_indices();
329        while let Some((i, c)) = chars.next() {
330            match c {
331                '"' => {
332                    self.at += i + 1;
333                    return Ok(key);
334                }
335                '\\' => match chars.next() {
336                    Some((_, escaped @ ('"' | '\\'))) => key.push(escaped),
337                    _ => {
338                        self.at += i;
339                        return self.fail(DockLayoutErrorKind::BadEscape);
340                    }
341                },
342                c => key.push(c),
343            }
344        }
345        self.at = self.text.len();
346        self.fail(DockLayoutErrorKind::UnclosedKey)
347    }
348}
349
350impl DockArea {
351    /// How the panels with keys are arranged now; `None` if none has a key. Panels without
352    /// a key are left out, and a group with none of them with them.
353    pub fn layout(&self) -> Option<DockLayout> {
354        self.layout_of(self.root?, 0)
355    }
356
357    fn layout_of(&self, node: usize, depth: usize) -> Option<DockLayout> {
358        let kind = &self.nodes.get(node)?.as_ref()?.kind;
359        if depth == MAX_LAYOUT_DEPTH && matches!(kind, Kind::Split { .. }) {
360            return self.merged(node);
361        }
362        match kind {
363            Kind::Group { tabs, active, .. } => {
364                let mut panels = Vec::new();
365                let mut shown = 0;
366                for (position, &tab) in tabs.iter().enumerate() {
367                    if let Some(key) = &self.tabs[tab].key {
368                        if position == *active {
369                            shown = panels.len();
370                        }
371                        panels.push(key.clone());
372                    }
373                }
374                (!panels.is_empty()).then_some(DockLayout::Tabs { panels, shown })
375            }
376            &Kind::Split {
377                axis,
378                ratio,
379                first,
380                second,
381            } => match (
382                self.layout_of(first, depth + 1),
383                self.layout_of(second, depth + 1),
384            ) {
385                (Some(first), Some(second)) => Some(DockLayout::Split {
386                    axis: match axis {
387                        Axis::Row => DockAxis::Row,
388                        Axis::Column => DockAxis::Column,
389                    },
390                    ratio,
391                    first: Box::new(first),
392                    second: Box::new(second),
393                }),
394                (first, second) => first.or(second),
395            },
396        }
397    }
398
399    /// The keyed panels of `node`'s groups, in order, as one group showing the first group's
400    /// shown panel: what is saved of splits nested too deeply to read back.
401    fn merged(&self, node: usize) -> Option<DockLayout> {
402        let mut panels = Vec::new();
403        let mut shown = None;
404        let mut stack = vec![node];
405        while let Some(node) = stack.pop() {
406            match self
407                .nodes
408                .get(node)
409                .and_then(Option::as_ref)
410                .map(|n| &n.kind)
411            {
412                Some(&Kind::Split { first, second, .. }) => stack.extend([second, first]),
413                Some(Kind::Group { tabs, active, .. }) => {
414                    for (position, &tab) in tabs.iter().enumerate() {
415                        if let Some(key) = &self.tabs[tab].key {
416                            if position == *active && shown.is_none() {
417                                shown = Some(panels.len());
418                            }
419                            panels.push(key.clone());
420                        }
421                    }
422                }
423                None => {}
424            }
425        }
426        let shown = shown.unwrap_or(0);
427        (!panels.is_empty()).then_some(DockLayout::Tabs { panels, shown })
428    }
429
430    /// Arranges `dock`'s panels as `layout` says, matching them by key; see
431    /// [`set_layout`](Self::set_layout).
432    ///
433    /// # Errors
434    /// [`UiError::NoSuchWidget`] if `dock` is gone or is not a [`DockArea`].
435    pub fn restore_layout(ui: &mut Ui, dock: WidgetId, layout: &DockLayout) -> Result<(), UiError> {
436        ui.update::<DockArea, _>(dock, |area, cx| area.set_layout(cx, layout))
437            .ok_or(UiError::NoSuchWidget(dock))
438    }
439
440    /// Arranges the panels as `layout` says (from an [`Ui::update`] on the dock): each key
441    /// takes the next panel with that key. Keys without a panel are passed over (a group
442    /// left empty goes, its space to its neighbor), and panels the layout does not name
443    /// (without a key, or added since it was saved) join the first group, so none is lost.
444    /// Ratios are kept from 0 to 1, and a group shows its first tab if its shown one is
445    /// missing. Either part of a split keeps at least 1% of it, so no group vanishes. A drag
446    /// in progress ends, and the keyboard stays with the group of the panel it was on.
447    pub fn set_layout(&mut self, cx: &mut EventCx<'_>, layout: &DockLayout) {
448        self.prune(cx);
449        if self.drag.take().is_some() {
450            cx.set_cursor(None);
451        }
452        self.hovered = None;
453        // The tab the keyboard was on, to find its group again.
454        let keyboard = self.focused_group().and_then(|group| {
455            let (tabs, active) = self.group_tabs(group);
456            tabs.get(active).copied()
457        });
458        self.focus_group = None;
459        self.nodes.clear();
460        self.root = None;
461        let mut placed = vec![false; self.tabs.len()];
462        self.root = self.build(layout, &mut placed, 0);
463        if let Some(root) = self.root {
464            if let Some(node) = &mut self.nodes[root] {
465                node.parent = None;
466            }
467        }
468        // The rest join the first group, which keeps showing what the layout shows.
469        let mut first = self.first_group();
470        let mut shown = first.and_then(|group| {
471            let (tabs, active) = self.group_tabs(group);
472            tabs.get(active).copied()
473        });
474        for index in (0..self.tabs.len()).filter(|&i| !placed[i]) {
475            self.join(index, first, None);
476            first = Some(self.tabs[index].group);
477            // Nothing was placed: the first of the rest is shown.
478            shown = shown.or(Some(index));
479        }
480        if let Some(tab) = shown {
481            self.activate(tab);
482        }
483        self.focus_group = keyboard.map(|tab| self.tabs[tab].group);
484        self.arrange();
485        self.restyle(cx);
486        // Titles may sit in groups of other widths: `layout` brings each shown tab into view.
487        cx.request_layout();
488    }
489
490    /// The group in the top-left corner.
491    fn first_group(&self) -> Option<usize> {
492        let mut node = self.root?;
493        loop {
494            match self.nodes.get(node)?.as_ref()?.kind {
495                Kind::Split { first, .. } => node = first,
496                Kind::Group { .. } => return Some(node),
497            }
498        }
499    }
500
501    /// Makes the nodes `layout` describes, with the tabs it names that are not `placed` yet;
502    /// the node made, or `None` if no tab was left for it.
503    fn build(&mut self, layout: &DockLayout, placed: &mut [bool], depth: usize) -> Option<usize> {
504        if depth > MAX_LAYOUT_DEPTH {
505            return None;
506        }
507        match layout {
508            DockLayout::Tabs { panels, shown } => {
509                let mut tabs = Vec::new();
510                let mut active = 0;
511                for (position, key) in panels.iter().enumerate() {
512                    let free = |i: &usize| !placed[*i] && self.tabs[*i].key.as_ref() == Some(key);
513                    let Some(index) = (0..self.tabs.len()).find(free) else {
514                        continue;
515                    };
516                    placed[index] = true;
517                    if position == *shown {
518                        active = tabs.len();
519                    }
520                    tabs.push(index);
521                }
522                if tabs.is_empty() {
523                    return None;
524                }
525                let serial = self.next_serial();
526                let group = self.push(Kind::Group {
527                    tabs: tabs.clone(),
528                    active,
529                    scroll: 0.0,
530                    serial,
531                });
532                for tab in tabs {
533                    self.tabs[tab].group = group;
534                }
535                Some(group)
536            }
537            DockLayout::Split {
538                axis,
539                ratio,
540                first,
541                second,
542            } => {
543                let first = self.build(first, placed, depth + 1);
544                let second = self.build(second, placed, depth + 1);
545                let (Some(first), Some(second)) = (first, second) else {
546                    return first.or(second);
547                };
548                let axis = match axis {
549                    DockAxis::Row => Axis::Row,
550                    DockAxis::Column => Axis::Column,
551                };
552                let ratio = if ratio.is_nan() {
553                    0.5
554                } else {
555                    ratio.clamp(MIN_RATIO, 1.0 - MIN_RATIO)
556                };
557                let split = self.push(Kind::Split {
558                    axis,
559                    ratio,
560                    first,
561                    second,
562                });
563                for child in [first, second] {
564                    if let Some(Node { parent, .. }) = &mut self.nodes[child] {
565                        *parent = Some(split);
566                    }
567                }
568                Some(split)
569            }
570        }
571    }
572}