use std::fmt;
use std::str::FromStr;
use densa_ui::{EventCx, Ui, UiError, WidgetId};
use super::{Axis, DockArea, Kind, Node};
pub const MAX_LAYOUT_DEPTH: usize = 64;
const FORMAT: &str = "densa-dock";
const VERSION: &str = "1";
const MIN_RATIO: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockAxis {
Row,
Column,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DockLayout {
Split {
axis: DockAxis,
ratio: f32,
first: Box<DockLayout>,
second: Box<DockLayout>,
},
Tabs {
panels: Vec<String>,
shown: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DockLayoutError {
pub at: usize,
pub kind: DockLayoutErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DockLayoutErrorKind {
NotALayout,
UnsupportedVersion,
ExpectedOpen,
ExpectedClose,
ExpectedNode,
BadRatio,
BadShown,
ShownMissing,
UnclosedKey,
BadEscape,
TooDeep,
TrailingText,
}
impl fmt::Display for DockLayoutErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NotALayout => "not a densa dock layout",
Self::UnsupportedVersion => "a dock layout of a later version",
Self::ExpectedOpen => "expected `(`",
Self::ExpectedClose => "expected `)`",
Self::ExpectedNode => "expected `row`, `column` or `tabs`",
Self::BadRatio => "expected a ratio from 0 to 1",
Self::BadShown => "expected the shown tab's position",
Self::ShownMissing => "a group must show one of its panels",
Self::UnclosedKey => "a key without its closing quote",
Self::BadEscape => "only `\\\"` and `\\\\` are escapes",
Self::TooDeep => "splits nested too deeply",
Self::TrailingText => "text after the layout",
})
}
}
impl fmt::Display for DockLayoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid dock layout at byte {}: {}", self.at, self.kind)
}
}
impl std::error::Error for DockLayoutError {}
impl fmt::Display for DockLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{FORMAT} {VERSION} ")?;
write_node(f, self)
}
}
fn write_node(f: &mut fmt::Formatter<'_>, layout: &DockLayout) -> fmt::Result {
match layout {
DockLayout::Split {
axis,
ratio,
first,
second,
} => {
let axis = match axis {
DockAxis::Row => "row",
DockAxis::Column => "column",
};
write!(f, "({axis} {} ", ratio + 0.0)?;
write_node(f, first)?;
f.write_str(" ")?;
write_node(f, second)?;
f.write_str(")")
}
DockLayout::Tabs { panels, shown } => {
write!(f, "(tabs {shown}")?;
for key in panels {
f.write_str(" \"")?;
for c in key.chars() {
if c == '"' || c == '\\' {
f.write_str("\\")?;
}
write!(f, "{c}")?;
}
f.write_str("\"")?;
}
f.write_str(")")
}
}
}
impl FromStr for DockLayout {
type Err = DockLayoutError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
use DockLayoutErrorKind::{NotALayout, TrailingText, UnsupportedVersion};
let mut reader = Reader { text, at: 0 };
reader.skip_space();
if !reader.rest().starts_with(FORMAT) {
return reader.fail(NotALayout);
}
reader.at += FORMAT.len();
if !reader.rest().starts_with(char::is_whitespace) {
return reader.fail(NotALayout);
}
reader.skip_space();
let start = reader.at;
let version = reader.word();
let known = version == VERSION;
let numbered = !version.is_empty() && version.bytes().all(|b| b.is_ascii_digit());
if !known || !reader.rest().starts_with(char::is_whitespace) {
reader.at = start;
return reader.fail(if numbered && !known {
UnsupportedVersion
} else {
NotALayout
});
}
let layout = reader.node(0)?;
reader.skip_space();
if !reader.rest().is_empty() {
return reader.fail(TrailingText);
}
Ok(layout)
}
}
struct Reader<'a> {
text: &'a str,
at: usize,
}
impl Reader<'_> {
fn rest(&self) -> &str {
&self.text[self.at..]
}
fn fail<T>(&self, kind: DockLayoutErrorKind) -> Result<T, DockLayoutError> {
Err(DockLayoutError { at: self.at, kind })
}
fn skip_space(&mut self) {
let rest = self.rest();
self.at += rest.len() - rest.trim_start().len();
}
fn eat(&mut self, c: char) -> bool {
self.skip_space();
let found = self.rest().starts_with(c);
if found {
self.at += c.len_utf8();
}
found
}
fn word(&mut self) -> &str {
self.skip_space();
let rest = self.rest();
let len = rest
.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+')))
.unwrap_or(rest.len());
let start = self.at;
self.at += len;
&self.text[start..self.at]
}
fn node(&mut self, depth: usize) -> Result<DockLayout, DockLayoutError> {
if depth > MAX_LAYOUT_DEPTH {
return self.fail(DockLayoutErrorKind::TooDeep);
}
if !self.eat('(') {
return self.fail(DockLayoutErrorKind::ExpectedOpen);
}
let start = self.at;
let layout = match self.word() {
"row" => self.split(DockAxis::Row, depth)?,
"column" => self.split(DockAxis::Column, depth)?,
"tabs" => self.tabs()?,
_ => {
self.at = start;
return self.fail(DockLayoutErrorKind::ExpectedNode);
}
};
if !self.eat(')') {
return self.fail(DockLayoutErrorKind::ExpectedClose);
}
Ok(layout)
}
fn split(&mut self, axis: DockAxis, depth: usize) -> Result<DockLayout, DockLayoutError> {
self.skip_space();
let start = self.at;
let ratio = match self.word().parse::<f32>() {
Ok(ratio) if (0.0..=1.0).contains(&ratio) => ratio,
_ => {
self.at = start;
return self.fail(DockLayoutErrorKind::BadRatio);
}
};
let first = Box::new(self.node(depth + 1)?);
let second = Box::new(self.node(depth + 1)?);
Ok(DockLayout::Split {
axis,
ratio,
first,
second,
})
}
fn tabs(&mut self) -> Result<DockLayout, DockLayoutError> {
self.skip_space();
let start = self.at;
let Ok(shown) = self.word().parse::<usize>() else {
self.at = start;
return self.fail(DockLayoutErrorKind::BadShown);
};
let mut panels = Vec::new();
while self.eat('"') {
panels.push(self.key()?);
}
if shown >= panels.len() {
self.at = start;
return self.fail(DockLayoutErrorKind::ShownMissing);
}
Ok(DockLayout::Tabs { panels, shown })
}
fn key(&mut self) -> Result<String, DockLayoutError> {
let mut key = String::new();
let mut chars = self.rest().char_indices();
while let Some((i, c)) = chars.next() {
match c {
'"' => {
self.at += i + 1;
return Ok(key);
}
'\\' => match chars.next() {
Some((_, escaped @ ('"' | '\\'))) => key.push(escaped),
_ => {
self.at += i;
return self.fail(DockLayoutErrorKind::BadEscape);
}
},
c => key.push(c),
}
}
self.at = self.text.len();
self.fail(DockLayoutErrorKind::UnclosedKey)
}
}
impl DockArea {
pub fn layout(&self) -> Option<DockLayout> {
self.layout_of(self.root?, 0)
}
fn layout_of(&self, node: usize, depth: usize) -> Option<DockLayout> {
let kind = &self.nodes.get(node)?.as_ref()?.kind;
if depth == MAX_LAYOUT_DEPTH && matches!(kind, Kind::Split { .. }) {
return self.merged(node);
}
match kind {
Kind::Group { tabs, active, .. } => {
let mut panels = Vec::new();
let mut shown = 0;
for (position, &tab) in tabs.iter().enumerate() {
if let Some(key) = &self.tabs[tab].key {
if position == *active {
shown = panels.len();
}
panels.push(key.clone());
}
}
(!panels.is_empty()).then_some(DockLayout::Tabs { panels, shown })
}
&Kind::Split {
axis,
ratio,
first,
second,
} => match (
self.layout_of(first, depth + 1),
self.layout_of(second, depth + 1),
) {
(Some(first), Some(second)) => Some(DockLayout::Split {
axis: match axis {
Axis::Row => DockAxis::Row,
Axis::Column => DockAxis::Column,
},
ratio,
first: Box::new(first),
second: Box::new(second),
}),
(first, second) => first.or(second),
},
}
}
fn merged(&self, node: usize) -> Option<DockLayout> {
let mut panels = Vec::new();
let mut shown = None;
let mut stack = vec![node];
while let Some(node) = stack.pop() {
match self
.nodes
.get(node)
.and_then(Option::as_ref)
.map(|n| &n.kind)
{
Some(&Kind::Split { first, second, .. }) => stack.extend([second, first]),
Some(Kind::Group { tabs, active, .. }) => {
for (position, &tab) in tabs.iter().enumerate() {
if let Some(key) = &self.tabs[tab].key {
if position == *active && shown.is_none() {
shown = Some(panels.len());
}
panels.push(key.clone());
}
}
}
None => {}
}
}
let shown = shown.unwrap_or(0);
(!panels.is_empty()).then_some(DockLayout::Tabs { panels, shown })
}
pub fn restore_layout(ui: &mut Ui, dock: WidgetId, layout: &DockLayout) -> Result<(), UiError> {
ui.update::<DockArea, _>(dock, |area, cx| area.set_layout(cx, layout))
.ok_or(UiError::NoSuchWidget(dock))
}
pub fn set_layout(&mut self, cx: &mut EventCx<'_>, layout: &DockLayout) {
self.prune(cx);
if self.drag.take().is_some() {
cx.set_cursor(None);
}
self.hovered = None;
let keyboard = self.focused_group().and_then(|group| {
let (tabs, active) = self.group_tabs(group);
tabs.get(active).copied()
});
self.focus_group = None;
self.nodes.clear();
self.root = None;
let mut placed = vec![false; self.tabs.len()];
self.root = self.build(layout, &mut placed, 0);
if let Some(root) = self.root {
if let Some(node) = &mut self.nodes[root] {
node.parent = None;
}
}
let mut first = self.first_group();
let mut shown = first.and_then(|group| {
let (tabs, active) = self.group_tabs(group);
tabs.get(active).copied()
});
for index in (0..self.tabs.len()).filter(|&i| !placed[i]) {
self.join(index, first, None);
first = Some(self.tabs[index].group);
shown = shown.or(Some(index));
}
if let Some(tab) = shown {
self.activate(tab);
}
self.focus_group = keyboard.map(|tab| self.tabs[tab].group);
self.arrange();
self.restyle(cx);
cx.request_layout();
}
fn first_group(&self) -> Option<usize> {
let mut node = self.root?;
loop {
match self.nodes.get(node)?.as_ref()?.kind {
Kind::Split { first, .. } => node = first,
Kind::Group { .. } => return Some(node),
}
}
}
fn build(&mut self, layout: &DockLayout, placed: &mut [bool], depth: usize) -> Option<usize> {
if depth > MAX_LAYOUT_DEPTH {
return None;
}
match layout {
DockLayout::Tabs { panels, shown } => {
let mut tabs = Vec::new();
let mut active = 0;
for (position, key) in panels.iter().enumerate() {
let free = |i: &usize| !placed[*i] && self.tabs[*i].key.as_ref() == Some(key);
let Some(index) = (0..self.tabs.len()).find(free) else {
continue;
};
placed[index] = true;
if position == *shown {
active = tabs.len();
}
tabs.push(index);
}
if tabs.is_empty() {
return None;
}
let serial = self.next_serial();
let group = self.push(Kind::Group {
tabs: tabs.clone(),
active,
scroll: 0.0,
serial,
});
for tab in tabs {
self.tabs[tab].group = group;
}
Some(group)
}
DockLayout::Split {
axis,
ratio,
first,
second,
} => {
let first = self.build(first, placed, depth + 1);
let second = self.build(second, placed, depth + 1);
let (Some(first), Some(second)) = (first, second) else {
return first.or(second);
};
let axis = match axis {
DockAxis::Row => Axis::Row,
DockAxis::Column => Axis::Column,
};
let ratio = if ratio.is_nan() {
0.5
} else {
ratio.clamp(MIN_RATIO, 1.0 - MIN_RATIO)
};
let split = self.push(Kind::Split {
axis,
ratio,
first,
second,
});
for child in [first, second] {
if let Some(Node { parent, .. }) = &mut self.nodes[child] {
*parent = Some(split);
}
}
Some(split)
}
}
}
}