1use serde::{Deserialize, Serialize};
5
6use crate::layout::{
7 Axis, BottomOwner, DerivedLayout, LayoutPresentationPlan, LayoutTree, PlanAside, SizeClass,
8 SplitForm, SwitcherForm,
9};
10use crate::model::{Role, Surface, SurfaceId};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct SurfaceGraph {
17 surfaces: Vec<Surface>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub active_main_id: Option<SurfaceId>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub focused_surface_id: Option<SurfaceId>,
22 #[serde(default, skip)]
24 modal_focus_stack: Vec<Option<SurfaceId>>,
25}
26
27impl SurfaceGraph {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn surfaces(&self) -> &[Surface] {
33 &self.surfaces
34 }
35
36 pub fn get(&self, id: &str) -> Option<&Surface> {
37 self.surfaces.iter().find(|s| s.id == id)
38 }
39
40 pub fn role_of(&self, id: &str) -> Option<Role> {
41 self.get(id).map(|s| s.role)
42 }
43
44 pub fn mains(&self) -> Vec<&Surface> {
45 self.by_role(Role::Main)
46 }
47 pub fn asides(&self) -> Vec<&Surface> {
48 self.by_role(Role::Aside)
49 }
50 pub fn floats(&self) -> Vec<&Surface> {
51 self.by_role(Role::Float)
52 }
53
54 fn by_role(&self, role: Role) -> Vec<&Surface> {
55 self.surfaces.iter().filter(|s| s.role == role).collect()
56 }
57
58 fn main_ids(&self) -> Vec<SurfaceId> {
59 self.surfaces
60 .iter()
61 .filter(|s| s.role == Role::Main)
62 .map(|s| s.id.clone())
63 .collect()
64 }
65
66 pub fn insert(&mut self, surface: Surface) {
68 let modal = surface.is_modal_float();
69 if modal {
70 self.modal_focus_stack.push(self.focused_surface_id.clone());
71 }
72 if let Some(existing) = self.surfaces.iter_mut().find(|s| s.id == surface.id) {
73 *existing = surface;
74 } else {
75 self.surfaces.push(surface);
76 }
77 self.converge_after_insert();
78 }
79
80 fn converge_after_insert(&mut self) {
81 if self.active_main_id.is_none()
83 && let Some(first) = self.main_ids().first().cloned()
84 {
85 self.active_main_id = Some(first.clone());
86 if self.focused_surface_id.is_none() {
87 self.focused_surface_id = Some(first);
88 }
89 }
90 if self.focused_surface_id.is_none()
92 && let Some(s) = self.surfaces.last()
93 {
94 self.focused_surface_id = Some(s.id.clone());
95 }
96 }
97
98 pub fn remove(&mut self, id: &str) -> Vec<SurfaceId> {
101 let Some(pos) = self.surfaces.iter().position(|s| s.id == id) else {
102 return Vec::new();
103 };
104 let removed = self.surfaces.remove(pos);
105 let mut removed_ids = vec![removed.id.clone()];
106
107 if self.active_main_id.as_deref() == Some(id) {
109 self.active_main_id = pick_successor_main(&self.surfaces, pos);
110 }
111
112 if self.mains().is_empty() {
114 let aside_ids: Vec<SurfaceId> = self
115 .surfaces
116 .iter()
117 .filter(|s| s.role == Role::Aside)
118 .map(|s| s.id.clone())
119 .collect();
120 self.surfaces.retain(|s| s.role != Role::Aside);
121 removed_ids.extend(aside_ids);
122 }
123
124 if removed.is_modal_float()
126 && let Some(snapshot) = self.modal_focus_stack.pop()
127 {
128 self.focused_surface_id = snapshot;
129 }
130
131 if self
133 .focused_surface_id
134 .as_deref()
135 .is_none_or(|f| self.get(f).is_none())
136 {
137 self.focused_surface_id = self.focus_fallback();
138 }
139 removed_ids
140 }
141
142 fn focus_fallback(&self) -> Option<SurfaceId> {
144 if let Some(active) = &self.active_main_id
145 && self.get(active).is_some()
146 {
147 return Some(active.clone());
148 }
149 self.asides().first().map(|s| s.id.clone())
150 }
151
152 pub fn set_active_main(&mut self, id: &str) -> bool {
154 if self.role_of(id) == Some(Role::Main) {
155 self.active_main_id = Some(id.to_string());
156 true
157 } else {
158 false
159 }
160 }
161
162 pub fn set_focus(&mut self, id: &str) -> bool {
164 if self.get(id).is_some() {
165 self.focused_surface_id = Some(id.to_string());
166 true
167 } else {
168 false
169 }
170 }
171
172 pub fn check_invariants(&self) -> Vec<String> {
174 let mut v = Vec::new();
175 let mains = self.mains().len();
176 let asides = self.asides().len();
177 if asides > 0 && mains == 0 {
178 v.push("asides>0 but mains==0 (no primary, no companion)".into());
179 }
180 match &self.active_main_id {
181 Some(id) if self.role_of(id) != Some(Role::Main) => {
182 v.push(format!("activeMainId '{id}' is not a main"));
183 }
184 None if mains > 0 => v.push("mains exist but activeMainId is None".into()),
185 _ => {}
186 }
187 if let Some(f) = &self.focused_surface_id
188 && self.get(f).is_none()
189 {
190 v.push(format!("focusedSurfaceId '{f}' does not exist"));
191 }
192 let mut seen = std::collections::HashSet::new();
194 for s in &self.surfaces {
195 if !seen.insert(&s.id) {
196 v.push(format!("duplicate surface id '{}'", s.id));
197 }
198 }
199 v
200 }
201
202 pub fn is_valid(&self) -> bool {
203 self.check_invariants().is_empty()
204 }
205
206 pub fn derive_layout(&self, size_class: SizeClass) -> DerivedLayout {
208 let main_count = self.mains().len();
209 let aside_count = self.asides().len();
210 let switcher_form = if size_class != SizeClass::Compact && main_count > 1 {
211 match size_class {
212 SizeClass::Expanded => SwitcherForm::Sidebar,
213 SizeClass::Medium => SwitcherForm::Rail,
214 SizeClass::Compact => SwitcherForm::None,
215 }
216 } else {
217 SwitcherForm::None
218 };
219
220 let split_form = if aside_count > 0 {
221 match size_class {
222 SizeClass::Expanded => SplitForm::Split,
223 SizeClass::Medium => SplitForm::Collapsible,
224 SizeClass::Compact => SplitForm::FullScreen,
226 }
227 } else {
228 SplitForm::None
229 };
230
231 DerivedLayout {
232 size_class,
233 switcher_form,
234 split_form,
235 bottom_owner: BottomOwner::App,
236 layout_tree: self.canonical_layout(size_class),
237 }
238 }
239
240 pub fn presentation_plan(&self, size_class: SizeClass) -> LayoutPresentationPlan {
244 let derived = self.derive_layout(size_class);
245
246 let asides = self
247 .asides()
248 .iter()
249 .map(|s| PlanAside {
250 id: s.id.clone(),
251 edge: s.placement.edge,
252 preferred_size: s.placement.preferred_size,
253 })
254 .collect();
255
256 LayoutPresentationPlan {
257 size_class: derived.size_class,
258 bottom_owner: derived.bottom_owner,
259 switcher_form: derived.switcher_form,
260 split_form: derived.split_form,
261 mains: self.main_ids(),
262 active_main_id: self
266 .active_main_id
267 .clone()
268 .or_else(|| self.main_ids().first().cloned()),
269 asides,
270 floats: self
275 .floats()
276 .iter()
277 .map(|s| {
278 let spec = s.float.clone().unwrap_or_default();
279 crate::layout::PlanFloat {
280 id: s.id.clone(),
281 anchor: spec.anchor,
282 dismiss: spec.dismiss,
283 modal: spec.modal,
284 }
285 })
286 .collect(),
287 tree: derived.layout_tree,
288 }
289 }
290
291 pub fn canonical_layout(&self, size_class: SizeClass) -> Option<LayoutTree> {
295 let main_ids = self.main_ids();
296 if main_ids.is_empty() {
297 return None;
298 }
299 let aside_ids: Vec<SurfaceId> = self.asides().iter().map(|s| s.id.clone()).collect();
300 let active = self
301 .active_main_id
302 .clone()
303 .unwrap_or_else(|| main_ids[0].clone());
304
305 let tabs_of = |ids: Vec<SurfaceId>| {
306 if ids.len() == 1 {
307 LayoutTree::Leaf {
308 surface_id: ids[0].clone(),
309 }
310 } else {
311 LayoutTree::Tabs {
312 active_id: active.clone(),
313 children: ids,
314 }
315 }
316 };
317
318 if size_class == SizeClass::Compact {
320 let mut ids = main_ids;
321 ids.extend(aside_ids);
322 return Some(tabs_of(ids));
323 }
324
325 let main_node = tabs_of(main_ids);
326 if aside_ids.is_empty() {
327 return Some(main_node);
328 }
329 let mut children = vec![main_node];
330 children.extend(
331 aside_ids
332 .into_iter()
333 .map(|id| LayoutTree::Leaf { surface_id: id }),
334 );
335 let n = children.len();
336 Some(LayoutTree::Split {
337 axis: Axis::Horizontal,
338 weights: vec![1.0 / n as f64; n],
339 children,
340 })
341 }
342}
343
344fn pick_successor_main(surfaces: &[Surface], removed_pos: usize) -> Option<SurfaceId> {
347 surfaces
348 .iter()
349 .skip(removed_pos)
350 .find(|s| s.role == Role::Main)
351 .or_else(|| {
352 surfaces
353 .iter()
354 .take(removed_pos)
355 .rev()
356 .find(|s| s.role == Role::Main)
357 })
358 .map(|s| s.id.clone())
359}