1use serde::{Deserialize, Serialize};
5
6use crate::model::SurfaceId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum SizeClass {
15 Compact,
16 Medium,
17 Expanded,
18}
19
20pub const COMPACT_MAX: f64 = 600.0;
22pub const MEDIUM_MAX: f64 = 840.0;
23pub const DEFAULT_HYSTERESIS: f64 = 24.0;
25
26impl SizeClass {
27 pub fn from_width(width: f64) -> Self {
28 if width < COMPACT_MAX {
29 SizeClass::Compact
30 } else if width <= MEDIUM_MAX {
31 SizeClass::Medium
32 } else {
33 SizeClass::Expanded
34 }
35 }
36
37 pub fn resolve(prev: Option<SizeClass>, width: f64, margin: f64) -> Self {
40 let raw = SizeClass::from_width(width);
41 let Some(prev) = prev else { return raw };
42 if prev == raw {
43 return prev;
44 }
45 let boundary =
48 match (prev, raw) {
49 (SizeClass::Compact, SizeClass::Medium)
50 | (SizeClass::Medium, SizeClass::Compact) => Some(COMPACT_MAX),
51 (SizeClass::Medium, SizeClass::Expanded)
52 | (SizeClass::Expanded, SizeClass::Medium) => Some(MEDIUM_MAX),
53 _ => None,
54 };
55 if boundary.is_some_and(|boundary| (width - boundary).abs() < margin) {
56 prev
57 } else {
58 raw
59 }
60 }
61
62 pub fn to_content(self) -> ContentSizeClass {
65 ContentSizeClass::from(self)
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum ContentSizeClass {
75 Compact,
76 Regular,
77}
78
79impl ContentSizeClass {
80 pub fn from_width(width: f64) -> Self {
81 if width < COMPACT_MAX {
82 ContentSizeClass::Compact
83 } else {
84 ContentSizeClass::Regular
85 }
86 }
87
88 pub fn resolve(prev: Option<Self>, width: f64, margin: f64) -> Self {
91 let raw = Self::from_width(width);
92 let Some(prev) = prev else { return raw };
93 if prev == raw {
94 return prev;
95 }
96 if (width - COMPACT_MAX).abs() < margin {
97 prev
98 } else {
99 raw
100 }
101 }
102
103 pub fn as_str(self) -> &'static str {
104 match self {
105 ContentSizeClass::Compact => "compact",
106 ContentSizeClass::Regular => "regular",
107 }
108 }
109}
110
111impl From<SizeClass> for ContentSizeClass {
112 fn from(value: SizeClass) -> Self {
113 match value {
114 SizeClass::Compact => ContentSizeClass::Compact,
115 SizeClass::Medium | SizeClass::Expanded => ContentSizeClass::Regular,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "lowercase")]
122pub enum Axis {
123 Horizontal,
124 Vertical,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[serde(tag = "kind", rename_all = "lowercase")]
131pub enum LayoutTree {
132 Leaf {
133 surface_id: SurfaceId,
134 },
135 Split {
136 axis: Axis,
137 children: Vec<LayoutTree>,
138 weights: Vec<f64>,
139 },
140 Tabs {
141 active_id: SurfaceId,
142 children: Vec<SurfaceId>,
143 },
144 Freeform {
146 surface_id: SurfaceId,
147 },
148}
149
150impl LayoutTree {
151 pub fn surface_ids(&self) -> Vec<SurfaceId> {
153 let mut out = Vec::new();
154 self.collect_ids(&mut out);
155 out
156 }
157
158 fn collect_ids(&self, out: &mut Vec<SurfaceId>) {
159 match self {
160 LayoutTree::Leaf { surface_id } | LayoutTree::Freeform { surface_id } => {
161 out.push(surface_id.clone());
162 }
163 LayoutTree::Tabs { children, .. } => out.extend(children.iter().cloned()),
164 LayoutTree::Split { children, .. } => {
165 for c in children {
166 c.collect_ids(out);
167 }
168 }
169 }
170 }
171
172 pub fn validate(&self) -> Result<(), String> {
175 match self {
176 LayoutTree::Leaf { .. } | LayoutTree::Freeform { .. } => Ok(()),
177 LayoutTree::Tabs {
178 active_id,
179 children,
180 } => {
181 if children.is_empty() {
182 return Err("tabs node has no children".into());
183 }
184 if !children.contains(active_id) {
185 return Err(format!("tabs.activeId '{active_id}' not in children"));
186 }
187 Ok(())
188 }
189 LayoutTree::Split {
190 children, weights, ..
191 } => {
192 if children.len() < 2 {
193 return Err("split node needs >= 2 children".into());
194 }
195 if weights.len() != children.len() {
196 return Err("split weights length != children length".into());
197 }
198 if weights.iter().any(|w| *w <= 0.0) {
199 return Err("split weights must be > 0".into());
200 }
201 for c in children {
202 c.validate()?;
203 }
204 Ok(())
205 }
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "lowercase")]
213pub enum SwitcherForm {
214 None,
215 Sidebar,
216 Rail,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub enum SplitForm {
223 None,
224 Split,
225 Collapsible,
226 FullScreen,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "lowercase")]
232pub enum BottomOwner {
233 App,
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct DerivedLayout {
243 pub size_class: SizeClass,
244 pub switcher_form: SwitcherForm,
245 pub split_form: SplitForm,
246 pub bottom_owner: BottomOwner,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub layout_tree: Option<LayoutTree>,
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct PlanAside {
257 pub id: SurfaceId,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub edge: Option<crate::model::Edge>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub preferred_size: Option<f64>,
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct PlanAsideSlot {
273 pub kind: crate::SlotKind,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub edge: Option<crate::model::Edge>,
277 pub children: Vec<SurfaceId>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub active_child: Option<SurfaceId>,
282 pub visible: bool,
285 #[serde(default)]
289 pub overlay: bool,
290 #[serde(default)]
294 pub collapsed: bool,
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct PlanFloat {
305 pub id: SurfaceId,
306 pub anchor: crate::model::FloatAnchor,
308 pub dismiss: crate::model::FloatDismiss,
310 pub modal: bool,
312 pub close_button: bool,
314}
315
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct LayoutPresentationPlan {
324 pub size_class: SizeClass,
325 pub bottom_owner: BottomOwner,
326 pub switcher_form: SwitcherForm,
327 pub split_form: SplitForm,
328 pub mains: Vec<SurfaceId>,
330 #[serde(skip_serializing_if = "Option::is_none")]
334 pub active_main_id: Option<SurfaceId>,
335 pub main_switcher: crate::SurfaceSwitcherSnapshot,
338 pub asides: Vec<PlanAside>,
341 #[serde(default)]
345 pub aside_slots: Vec<PlanAsideSlot>,
346 pub floats: Vec<PlanFloat>,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub tree: Option<LayoutTree>,
352}