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