1use serde::{Deserialize, Serialize};
5
6use crate::model::SurfaceId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum SizeClass {
13 Compact,
14 Medium,
15 Expanded,
16}
17
18pub const COMPACT_MAX: f64 = 600.0;
20pub const MEDIUM_MAX: f64 = 840.0;
21pub const DEFAULT_HYSTERESIS: f64 = 24.0;
23
24impl SizeClass {
25 pub fn from_width(width: f64) -> Self {
26 if width < COMPACT_MAX {
27 SizeClass::Compact
28 } else if width <= MEDIUM_MAX {
29 SizeClass::Medium
30 } else {
31 SizeClass::Expanded
32 }
33 }
34
35 pub fn resolve(prev: Option<SizeClass>, width: f64, margin: f64) -> Self {
38 let raw = SizeClass::from_width(width);
39 let Some(prev) = prev else { return raw };
40 if prev == raw {
41 return prev;
42 }
43 let near_lower = (width - COMPACT_MAX).abs() < margin;
45 let near_upper = (width - MEDIUM_MAX).abs() < margin;
46 if near_lower || near_upper { prev } else { raw }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Axis {
53 Horizontal,
54 Vertical,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(tag = "kind", rename_all = "lowercase")]
61pub enum LayoutTree {
62 Leaf {
63 surface_id: SurfaceId,
64 },
65 Split {
66 axis: Axis,
67 children: Vec<LayoutTree>,
68 weights: Vec<f64>,
69 },
70 Tabs {
71 active_id: SurfaceId,
72 children: Vec<SurfaceId>,
73 },
74 Freeform {
76 surface_id: SurfaceId,
77 },
78}
79
80impl LayoutTree {
81 pub fn surface_ids(&self) -> Vec<SurfaceId> {
83 let mut out = Vec::new();
84 self.collect_ids(&mut out);
85 out
86 }
87
88 fn collect_ids(&self, out: &mut Vec<SurfaceId>) {
89 match self {
90 LayoutTree::Leaf { surface_id } | LayoutTree::Freeform { surface_id } => {
91 out.push(surface_id.clone());
92 }
93 LayoutTree::Tabs { children, .. } => out.extend(children.iter().cloned()),
94 LayoutTree::Split { children, .. } => {
95 for c in children {
96 c.collect_ids(out);
97 }
98 }
99 }
100 }
101
102 pub fn validate(&self) -> Result<(), String> {
105 match self {
106 LayoutTree::Leaf { .. } | LayoutTree::Freeform { .. } => Ok(()),
107 LayoutTree::Tabs {
108 active_id,
109 children,
110 } => {
111 if children.is_empty() {
112 return Err("tabs node has no children".into());
113 }
114 if !children.contains(active_id) {
115 return Err(format!("tabs.activeId '{active_id}' not in children"));
116 }
117 Ok(())
118 }
119 LayoutTree::Split {
120 children, weights, ..
121 } => {
122 if children.len() < 2 {
123 return Err("split node needs >= 2 children".into());
124 }
125 if weights.len() != children.len() {
126 return Err("split weights length != children length".into());
127 }
128 if weights.iter().any(|w| *w <= 0.0) {
129 return Err("split weights must be > 0".into());
130 }
131 for c in children {
132 c.validate()?;
133 }
134 Ok(())
135 }
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "lowercase")]
143pub enum SwitcherForm {
144 None,
145 Sidebar,
146 Rail,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub enum SplitForm {
153 None,
154 Split,
155 Collapsible,
156 FullScreen,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "lowercase")]
162pub enum BottomOwner {
163 App,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct DerivedLayout {
173 pub size_class: SizeClass,
174 pub switcher_form: SwitcherForm,
175 pub split_form: SplitForm,
176 pub bottom_owner: BottomOwner,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub layout_tree: Option<LayoutTree>,
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct PlanAside {
187 pub id: SurfaceId,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub edge: Option<crate::model::Edge>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub preferred_size: Option<f64>,
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct PlanFloat {
204 pub id: SurfaceId,
205 pub anchor: crate::model::FloatAnchor,
207 pub dismiss: crate::model::FloatDismiss,
209 pub modal: bool,
211}
212
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct LayoutPresentationPlan {
221 pub size_class: SizeClass,
222 pub bottom_owner: BottomOwner,
223 pub switcher_form: SwitcherForm,
224 pub split_form: SplitForm,
225 pub mains: Vec<SurfaceId>,
227 #[serde(skip_serializing_if = "Option::is_none")]
231 pub active_main_id: Option<SurfaceId>,
232 pub asides: Vec<PlanAside>,
235 pub floats: Vec<PlanFloat>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub tree: Option<LayoutTree>,
241}