1use std::fmt;
4use std::str::FromStr;
5
6use densa_ui::{EventCx, Ui, UiError, WidgetId};
7
8use super::{Axis, DockArea, Kind, Node};
9
10pub const MAX_LAYOUT_DEPTH: usize = 64;
14
15const FORMAT: &str = "densa-dock";
17const VERSION: &str = "1";
19const MIN_RATIO: f32 = 0.01;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum DockAxis {
25 Row,
27 Column,
29}
30
31#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub enum DockLayout {
45 Split {
47 axis: DockAxis,
49 ratio: f32,
51 first: Box<DockLayout>,
53 second: Box<DockLayout>,
55 },
56 Tabs {
58 panels: Vec<String>,
60 shown: usize,
62 },
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct DockLayoutError {
69 pub at: usize,
71 pub kind: DockLayoutErrorKind,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum DockLayoutErrorKind {
79 NotALayout,
81 UnsupportedVersion,
83 ExpectedOpen,
85 ExpectedClose,
87 ExpectedNode,
89 BadRatio,
91 BadShown,
93 ShownMissing,
95 UnclosedKey,
97 BadEscape,
99 TooDeep,
101 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 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 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
218struct 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 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 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 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 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 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 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 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 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 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 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 cx.request_layout();
484 }
485
486 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 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}