1use std::collections::HashSet;
5use std::fmt;
6
7use ravenrs_layout::Axis;
8use serde::{Deserialize, Serialize};
9
10use crate::lists::reorder;
11
12const FORMAT_VERSION: u64 = 1;
14const SHARES_SUM_TOLERANCE: f32 = 1e-3;
17
18#[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 Left,
26 Right,
28 Bottom,
30 Center,
32}
33
34#[cfg_attr(feature = "live", ravenrs_live::expose)]
35impl DockSide {
36 pub(crate) const DOCKS: [Self; 3] = [Self::Left, Self::Right, Self::Bottom];
38 pub(crate) const ALL: [Self; 4] = [Self::Center, Self::Left, Self::Right, Self::Bottom];
40 pub(crate) const ALL_IN_MENU: [Self; 4] = [Self::Left, Self::Right, Self::Bottom, Self::Center];
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
48pub enum DockEdge {
49 Left,
51 Right,
53 Top,
55 Bottom,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
62#[non_exhaustive]
63#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
64pub enum DockDrop {
65 Tab {
68 beside: String,
70 at: usize,
72 },
73 Split {
75 beside: String,
77 edge: DockEdge,
79 },
80 Side(DockSide),
82}
83
84#[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 Tabs {
92 panels: Vec<String>,
94 #[serde(default)]
96 active: usize,
97 },
98 Split {
100 #[serde(with = "axis_name")]
102 axis: Axis,
103 children: Vec<DockChild>,
105 },
106}
107
108#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
111pub struct DockChild {
112 pub share: f32,
114 pub node: DockNode,
116}
117
118#[cfg_attr(feature = "live", ravenrs_live::expose)]
119impl DockNode {
120 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 pub fn horizontal(children: impl IntoIterator<Item = DockNode>) -> Self {
127 Self::split(Axis::Horizontal, children)
128 }
129
130 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 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 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 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 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 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 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
235fn 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 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#[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
339struct Closed {
340 panel: String,
341 place: DockSide,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 with: Option<String>,
345}
346
347#[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#[derive(Clone, Debug, PartialEq, Eq)]
388#[non_exhaustive]
389#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
390pub enum DockLayoutError {
391 Syntax {
393 line: usize,
395 column: usize,
397 message: String,
399 },
400 NotALayout,
402 UnsupportedVersion(u64),
404 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 pub fn new() -> Self {
437 Self { center: None, left: left(), right: right(), bottom: bottom(), closed: Vec::new() }
438 }
439
440 pub fn center(mut self, node: DockNode) -> Self {
442 self.center = Some(node);
443 self.tidy();
444 self
445 }
446
447 pub fn left(self, node: DockNode, size: f32) -> Self {
449 self.dock(DockSide::Left, node, size)
450 }
451
452 pub fn right(self, node: DockNode, size: f32) -> Self {
454 self.dock(DockSide::Right, node, size)
455 }
456
457 pub fn bottom(self, node: DockNode, size: f32) -> Self {
459 self.dock(DockSide::Bottom, node, size)
460 }
461
462 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 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 pub fn size(&self, side: DockSide) -> f32 {
515 self.region(side).map_or(0.0, |region| region.size)
516 }
517
518 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 pub fn is_open(&self, side: DockSide) -> bool {
529 self.region(side).is_none_or(|region| region.open)
530 }
531
532 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 pub fn contains(&self, id: &str) -> bool {
541 self.side_of(id).is_some()
542 }
543
544 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 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 pub fn closed_panels(&self) -> Vec<&str> {
562 self.closed.iter().map(|closed| closed.panel.as_str()).collect()
563 }
564
565 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 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 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 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 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 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 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 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 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 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 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 pub fn to_text(&self) -> String {
752 serde_json::to_string_pretty(&Saved { version: FORMAT_VERSION, layout: self }).unwrap_or_default()
754 }
755
756 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 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 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}