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)]
47#[non_exhaustive]
48pub enum DockLayout {
49 Split {
51 axis: DockAxis,
53 ratio: f32,
55 first: Box<DockLayout>,
57 second: Box<DockLayout>,
59 },
60 Tabs {
62 panels: Vec<String>,
64 shown: usize,
66 },
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71#[non_exhaustive]
72pub struct DockLayoutError {
73 pub at: usize,
75 pub kind: DockLayoutErrorKind,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum DockLayoutErrorKind {
83 NotALayout,
85 UnsupportedVersion,
87 ExpectedOpen,
89 ExpectedClose,
91 ExpectedNode,
93 BadRatio,
95 BadShown,
97 ShownMissing,
99 UnclosedKey,
101 BadEscape,
103 TooDeep,
105 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 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 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
222struct 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 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 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 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 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 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 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 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 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 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 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 cx.request_layout();
488 }
489
490 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 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}