use serde::{Deserialize, Serialize};
use crate::model::SurfaceId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SizeClass {
Compact,
Medium,
Expanded,
}
pub const COMPACT_MAX: f64 = 600.0;
pub const MEDIUM_MAX: f64 = 840.0;
pub const DEFAULT_HYSTERESIS: f64 = 24.0;
impl SizeClass {
pub fn from_width(width: f64) -> Self {
if width < COMPACT_MAX {
SizeClass::Compact
} else if width <= MEDIUM_MAX {
SizeClass::Medium
} else {
SizeClass::Expanded
}
}
pub fn resolve(prev: Option<SizeClass>, width: f64, margin: f64) -> Self {
let raw = SizeClass::from_width(width);
let Some(prev) = prev else { return raw };
if prev == raw {
return prev;
}
let boundary =
match (prev, raw) {
(SizeClass::Compact, SizeClass::Medium)
| (SizeClass::Medium, SizeClass::Compact) => Some(COMPACT_MAX),
(SizeClass::Medium, SizeClass::Expanded)
| (SizeClass::Expanded, SizeClass::Medium) => Some(MEDIUM_MAX),
_ => None,
};
if boundary.is_some_and(|boundary| (width - boundary).abs() < margin) {
prev
} else {
raw
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Axis {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum LayoutTree {
Leaf {
surface_id: SurfaceId,
},
Split {
axis: Axis,
children: Vec<LayoutTree>,
weights: Vec<f64>,
},
Tabs {
active_id: SurfaceId,
children: Vec<SurfaceId>,
},
Freeform {
surface_id: SurfaceId,
},
}
impl LayoutTree {
pub fn surface_ids(&self) -> Vec<SurfaceId> {
let mut out = Vec::new();
self.collect_ids(&mut out);
out
}
fn collect_ids(&self, out: &mut Vec<SurfaceId>) {
match self {
LayoutTree::Leaf { surface_id } | LayoutTree::Freeform { surface_id } => {
out.push(surface_id.clone());
}
LayoutTree::Tabs { children, .. } => out.extend(children.iter().cloned()),
LayoutTree::Split { children, .. } => {
for c in children {
c.collect_ids(out);
}
}
}
}
pub fn validate(&self) -> Result<(), String> {
match self {
LayoutTree::Leaf { .. } | LayoutTree::Freeform { .. } => Ok(()),
LayoutTree::Tabs {
active_id,
children,
} => {
if children.is_empty() {
return Err("tabs node has no children".into());
}
if !children.contains(active_id) {
return Err(format!("tabs.activeId '{active_id}' not in children"));
}
Ok(())
}
LayoutTree::Split {
children, weights, ..
} => {
if children.len() < 2 {
return Err("split node needs >= 2 children".into());
}
if weights.len() != children.len() {
return Err("split weights length != children length".into());
}
if weights.iter().any(|w| *w <= 0.0) {
return Err("split weights must be > 0".into());
}
for c in children {
c.validate()?;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SwitcherForm {
None,
Sidebar,
Rail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SplitForm {
None,
Split,
Collapsible,
FullScreen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BottomOwner {
App,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DerivedLayout {
pub size_class: SizeClass,
pub switcher_form: SwitcherForm,
pub split_form: SplitForm,
pub bottom_owner: BottomOwner,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_tree: Option<LayoutTree>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanAside {
pub id: SurfaceId,
#[serde(skip_serializing_if = "Option::is_none")]
pub edge: Option<crate::model::Edge>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preferred_size: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanAsideSlot {
pub kind: crate::model::SlotKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub edge: Option<crate::model::Edge>,
pub children: Vec<SurfaceId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_child: Option<SurfaceId>,
pub visible: bool,
#[serde(default)]
pub overlay: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanFloat {
pub id: SurfaceId,
pub anchor: crate::model::FloatAnchor,
pub dismiss: crate::model::FloatDismiss,
pub modal: bool,
pub close_button: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LayoutPresentationPlan {
pub size_class: SizeClass,
pub bottom_owner: BottomOwner,
pub switcher_form: SwitcherForm,
pub split_form: SplitForm,
pub mains: Vec<SurfaceId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_main_id: Option<SurfaceId>,
pub asides: Vec<PlanAside>,
#[serde(default)]
pub aside_slots: Vec<PlanAsideSlot>,
pub floats: Vec<PlanFloat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tree: Option<LayoutTree>,
}