1use serde::{Deserialize, Serialize};
5
6use crate::content::SlotKind;
7use crate::layout::{
8 Axis, BottomOwner, DerivedLayout, LayoutPresentationPlan, LayoutTree, PlanAside, SizeClass,
9 SplitForm, SwitcherForm,
10};
11use crate::model::{Role, Surface, SurfaceId, SurfaceState};
12use crate::switcher::CloseOutcome;
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct SurfaceGraph {
19 surfaces: Vec<Surface>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 root_main_id: Option<SurfaceId>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub active_main_id: Option<SurfaceId>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub focused_surface_id: Option<SurfaceId>,
26 #[serde(default, skip)]
28 modal_focus_stack: Vec<Option<SurfaceId>>,
29 #[serde(default)]
32 aside_slot_mru: Vec<SlotKind>,
33 #[serde(default)]
37 aside_child_mru: Vec<SurfaceId>,
38 #[serde(default)]
42 collapsed_slots: Vec<SlotKind>,
43}
44
45impl SurfaceGraph {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn surfaces(&self) -> &[Surface] {
51 &self.surfaces
52 }
53
54 pub fn get(&self, id: &str) -> Option<&Surface> {
55 self.surfaces.iter().find(|s| s.id == id)
56 }
57
58 pub fn role_of(&self, id: &str) -> Option<Role> {
59 self.get(id).map(|s| s.role)
60 }
61
62 pub fn root_main_id(&self) -> Option<&str> {
63 self.root_main_id.as_deref()
64 }
65
66 pub fn is_root_main(&self, id: &str) -> bool {
67 self.root_main_id.as_deref() == Some(id)
68 }
69
70 pub(crate) fn replace_mains(&mut self, mains: Vec<Surface>) {
71 debug_assert!(mains.iter().all(|surface| surface.role == Role::Main));
72 let old_main_ids: std::collections::HashSet<_> = self.main_ids().into_iter().collect();
73 self.surfaces
74 .retain(|surface| !old_main_ids.contains(&surface.id));
75 self.root_main_id = None;
76 self.active_main_id = None;
77
78 let mut retained = std::mem::take(&mut self.surfaces);
79 self.surfaces = mains;
80 self.surfaces.append(&mut retained);
81 if self.mains().is_empty() {
82 self.surfaces.retain(|surface| surface.role != Role::Aside);
83 }
84 self.converge_after_insert();
85 if self
86 .focused_surface_id
87 .as_deref()
88 .is_none_or(|id| self.get(id).is_none())
89 {
90 self.focused_surface_id = self.active_main_id.clone();
91 }
92 self.prune_aside_slot_mru();
93 }
94
95 pub fn mains(&self) -> Vec<&Surface> {
96 self.by_role(Role::Main)
97 }
98 pub fn asides(&self) -> Vec<&Surface> {
99 self.by_role(Role::Aside)
100 }
101 pub fn floats(&self) -> Vec<&Surface> {
102 self.by_role(Role::Float)
103 }
104
105 fn by_role(&self, role: Role) -> Vec<&Surface> {
106 self.surfaces.iter().filter(|s| s.role == role).collect()
107 }
108
109 fn main_ids(&self) -> Vec<SurfaceId> {
110 self.surfaces
111 .iter()
112 .filter(|s| s.role == Role::Main)
113 .map(|s| s.id.clone())
114 .collect()
115 }
116
117 pub fn insert(&mut self, surface: Surface) {
119 if self.is_root_main(&surface.id) && surface.role != Role::Main {
122 return;
123 }
124 let surface_id = surface.id.clone();
125 let modal = surface.is_modal_float();
126 let was_modal = self.get(&surface.id).is_some_and(Surface::is_modal_float);
127 let previous_slot = self
128 .get(&surface.id)
129 .filter(|existing| existing.role == Role::Aside)
130 .map(|existing| existing.content.slot_kind());
131 let next_slot = (surface.role == Role::Aside).then(|| surface.content.slot_kind());
132 if modal && !was_modal {
133 self.modal_focus_stack.push(self.focused_surface_id.clone());
134 } else if !modal && was_modal {
135 let _ = self.modal_focus_stack.pop();
136 }
137 if let Some(existing) = self.surfaces.iter_mut().find(|s| s.id == surface.id) {
138 *existing = surface;
139 } else {
140 self.surfaces.push(surface);
141 }
142 if let Some(previous) = previous_slot
143 && Some(previous) != next_slot
144 && !self
145 .asides()
146 .iter()
147 .any(|aside| aside.content.slot_kind() == previous)
148 {
149 self.aside_slot_mru.retain(|kind| *kind != previous);
150 }
151 if let Some(kind) = next_slot {
152 self.touch_aside_slot(kind);
153 self.touch_aside_child(&surface_id);
154 }
155 self.converge_after_insert();
156 }
157
158 fn touch_aside_child(&mut self, id: &str) {
159 self.aside_child_mru.retain(|entry| entry != id);
160 self.aside_child_mru.push(id.to_string());
161 }
162
163 fn touch_aside_slot(&mut self, kind: SlotKind) {
164 self.aside_slot_mru.retain(|entry| *entry != kind);
165 self.aside_slot_mru.push(kind);
166 self.collapsed_slots.retain(|entry| *entry != kind);
169 }
170
171 pub fn slot_collapsed(&self, kind: SlotKind) -> bool {
172 self.collapsed_slots.contains(&kind)
173 }
174
175 pub fn set_slot_collapsed(&mut self, kind: SlotKind, collapsed: bool) -> bool {
177 if collapsed == self.slot_collapsed(kind) {
178 return false;
179 }
180 if collapsed {
181 self.collapsed_slots.push(kind);
182 } else {
183 self.collapsed_slots.retain(|entry| *entry != kind);
184 }
185 true
186 }
187
188 fn prune_aside_slot_mru(&mut self) {
189 let live: std::collections::HashSet<SlotKind> = self
190 .asides()
191 .iter()
192 .map(|aside| aside.content.slot_kind())
193 .collect();
194 self.aside_slot_mru.retain(|kind| live.contains(kind));
195 self.collapsed_slots.retain(|kind| live.contains(kind));
197 let live_children: std::collections::HashSet<SurfaceId> = self
198 .asides()
199 .into_iter()
200 .map(|surface| surface.id.clone())
201 .collect();
202 self.aside_child_mru.retain(|id| live_children.contains(id));
203 }
204
205 fn converge_after_insert(&mut self) {
206 let first_main = self.main_ids().first().cloned();
207 if self
208 .root_main_id
209 .as_deref()
210 .is_none_or(|id| self.role_of(id) != Some(Role::Main))
211 {
212 self.root_main_id = first_main.clone();
213 }
214 if self
215 .active_main_id
216 .as_deref()
217 .is_none_or(|id| self.role_of(id) != Some(Role::Main))
218 {
219 self.active_main_id = first_main.clone();
220 if self.focused_surface_id.is_none() {
221 self.focused_surface_id = first_main;
222 }
223 }
224 if self.focused_surface_id.is_none()
226 && let Some(s) = self.surfaces.last()
227 {
228 self.focused_surface_id = Some(s.id.clone());
229 }
230 }
231
232 pub fn close(&mut self, id: &str) -> CloseOutcome {
235 if self.is_root_main(id) {
236 return CloseOutcome::RejectedRoot {
237 surface_id: id.to_string(),
238 };
239 }
240 let Some(pos) = self.surfaces.iter().position(|s| s.id == id) else {
241 return CloseOutcome::NotFound;
242 };
243 let removed = self.surfaces.remove(pos);
244 let removed_aside_kind = (removed.role == Role::Aside).then(|| removed.content.slot_kind());
245 let mut removed_ids = vec![removed.id.clone()];
246
247 if self.active_main_id.as_deref() == Some(id) {
249 self.active_main_id = pick_successor_main(&self.surfaces, pos);
250 }
251
252 if self.mains().is_empty() {
254 let aside_ids: Vec<SurfaceId> = self
255 .surfaces
256 .iter()
257 .filter(|s| s.role == Role::Aside)
258 .map(|s| s.id.clone())
259 .collect();
260 self.surfaces.retain(|s| s.role != Role::Aside);
261 removed_ids.extend(aside_ids);
262 }
263
264 if removed.is_modal_float()
266 && let Some(snapshot) = self.modal_focus_stack.pop()
267 {
268 self.focused_surface_id = snapshot;
269 }
270
271 if self
273 .focused_surface_id
274 .as_deref()
275 .is_none_or(|f| self.get(f).is_none())
276 {
277 self.focused_surface_id = removed_aside_kind
278 .and_then(|kind| {
279 self.aside_child_mru.iter().rev().find_map(|candidate| {
280 self.get(candidate).and_then(|surface| {
281 (surface.state == SurfaceState::Mounted
282 && surface.role == Role::Aside
283 && surface.content.slot_kind() == kind)
284 .then(|| surface.id.clone())
285 })
286 })
287 })
288 .or_else(|| self.focus_fallback());
289 }
290 self.prune_aside_slot_mru();
291 CloseOutcome::Closed {
292 removed: removed_ids,
293 }
294 }
295
296 pub fn close_other_mains(&mut self, keeping: &str) -> Vec<SurfaceId> {
300 if self.role_of(keeping) != Some(Role::Main) {
301 return Vec::new();
302 }
303 let targets: Vec<_> = self
304 .main_ids()
305 .into_iter()
306 .filter(|id| id != keeping && !self.is_root_main(id))
307 .collect();
308 targets
309 .into_iter()
310 .flat_map(|id| self.close(&id).into_removed())
311 .collect()
312 }
313
314 pub fn close_mains_after(&mut self, id: &str) -> Vec<SurfaceId> {
318 let mains = self.main_ids();
319 let Some(index) = mains.iter().position(|candidate| candidate == id) else {
320 return Vec::new();
321 };
322 let root_main_id = self.root_main_id.clone();
323 mains
324 .into_iter()
325 .skip(index + 1)
326 .filter(|candidate| root_main_id.as_ref() != Some(candidate))
327 .flat_map(|candidate| self.close(&candidate).into_removed())
328 .collect()
329 }
330
331 fn focus_fallback(&self) -> Option<SurfaceId> {
333 if let Some(active) = &self.active_main_id
334 && self.get(active).is_some()
335 {
336 return Some(active.clone());
337 }
338 self.asides().first().map(|s| s.id.clone())
339 }
340
341 pub fn set_active_main(&mut self, id: &str) -> bool {
343 if self.role_of(id) == Some(Role::Main) {
344 self.active_main_id = Some(id.to_string());
345 true
346 } else {
347 false
348 }
349 }
350
351 pub fn set_focus(&mut self, id: &str) -> bool {
353 let aside_kind = self
354 .get(id)
355 .filter(|surface| surface.role == Role::Aside)
356 .map(|surface| surface.content.slot_kind());
357 if self.get(id).is_some() {
358 if let Some(surface) = self.surfaces.iter_mut().find(|surface| surface.id == id) {
359 surface.state = SurfaceState::Mounted;
360 }
361 self.focused_surface_id = Some(id.to_string());
362 if let Some(kind) = aside_kind {
363 self.touch_aside_slot(kind);
364 self.touch_aside_child(id);
365 }
366 true
367 } else {
368 false
369 }
370 }
371
372 pub fn show(&mut self, id: &str) -> bool {
375 let Some(role) = self.role_of(id) else {
376 return false;
377 };
378 if let Some(surface) = self.surfaces.iter_mut().find(|surface| surface.id == id) {
379 surface.state = SurfaceState::Mounted;
380 }
381 match role {
382 Role::Main => self.set_active_main(id),
383 Role::Aside | Role::Float => self.set_focus(id),
384 }
385 }
386
387 pub fn hide(&mut self, id: &str) -> bool {
391 let Some(surface) = self.get(id) else {
392 return false;
393 };
394 if surface.role == Role::Main {
395 return false;
396 }
397 let role = surface.role;
398 let slot_kind = (role == Role::Aside).then(|| surface.content.slot_kind());
399 if let Some(surface) = self.surfaces.iter_mut().find(|surface| surface.id == id) {
400 surface.state = SurfaceState::Hidden;
401 }
402 if self.focused_surface_id.as_deref() == Some(id) {
403 let sibling = slot_kind.and_then(|kind| {
404 self.aside_child_mru.iter().rev().find_map(|candidate| {
405 self.get(candidate).and_then(|surface| {
406 (surface.id != id
407 && surface.role == Role::Aside
408 && surface.state == SurfaceState::Mounted
409 && surface.content.slot_kind() == kind)
410 .then(|| surface.id.clone())
411 })
412 })
413 });
414 self.focused_surface_id = sibling.or_else(|| self.active_main_id.clone());
415 }
416 true
417 }
418
419 pub fn aside_slots_admitted(
430 &self,
431 size_class: SizeClass,
432 width: f64,
433 policy: &crate::arbitrate::Policy,
434 ) -> Vec<crate::layout::PlanAsideSlot> {
435 let max_visible = policy.max_asides(size_class);
436 let mut slots = self.aside_slots_recency(usize::MAX);
437 for slot in &mut slots {
438 slot.visible = false;
439 }
440 let candidates = self.slot_indices_by_recency(&slots);
447 let mut horizontal_used = policy.main_min_width;
448 let mut admitted = 0;
449 for i in candidates {
450 if admitted == max_visible {
451 break;
452 }
453 if slots[i].active_child.is_none() || slots[i].collapsed {
454 continue;
455 }
456 if size_class == SizeClass::Compact {
457 slots[i].visible = true;
458 admitted += 1;
459 continue;
460 }
461 let horizontal = !matches!(
462 slots[i].edge,
463 Some(crate::model::Edge::Top) | Some(crate::model::Edge::Bottom)
464 );
465 if horizontal && horizontal_used + policy.aside_min_width > width {
466 continue;
467 }
468 if horizontal {
469 horizontal_used += policy.aside_min_width;
470 }
471 slots[i].visible = true;
472 admitted += 1;
473 }
474 slots
475 }
476
477 pub fn aside_slots(&self, size_class: SizeClass) -> Vec<crate::layout::PlanAsideSlot> {
480 self.aside_slots_recency(crate::arbitrate::Policy::default().max_asides(size_class))
481 }
482
483 fn aside_slots_recency(&self, max_visible: usize) -> Vec<crate::layout::PlanAsideSlot> {
487 let asides = self.asides();
488 let mut slots: Vec<crate::layout::PlanAsideSlot> = Vec::new();
489 for surface in &asides {
492 let kind = surface.content.slot_kind();
493 let slot = match slots.iter_mut().find(|slot| slot.kind == kind) {
494 Some(slot) => slot,
495 None => {
496 slots.push(crate::layout::PlanAsideSlot {
497 kind,
498 edge: None,
499 children: Vec::new(),
500 active_child: None,
501 visible: false,
502 overlay: false,
503 collapsed: self.slot_collapsed(kind),
504 });
505 slots.last_mut().expect("just pushed")
506 }
507 };
508 slot.children.push(surface.id.clone());
509 if surface.placement.edge.is_some() {
511 slot.edge = surface.placement.edge;
512 }
513 }
514 for slot in &mut slots {
515 slot.active_child = self
518 .focused_surface_id
519 .as_ref()
520 .filter(|id| {
521 slot.children.contains(id)
522 && self
523 .get(id)
524 .is_some_and(|surface| surface.state == SurfaceState::Mounted)
525 })
526 .cloned()
527 .or_else(|| {
528 self.aside_child_mru.iter().rev().find_map(|id| {
529 (slot.children.contains(id)
530 && self
531 .get(id)
532 .is_some_and(|surface| surface.state == SurfaceState::Mounted))
533 .then(|| id.clone())
534 })
535 });
536 }
537 let admitted: Vec<usize> = self
538 .slot_indices_by_recency(&slots)
539 .into_iter()
540 .filter(|index| !slots[*index].collapsed)
541 .take(max_visible)
542 .collect();
543 for slot_index in admitted {
544 slots[slot_index].visible = true;
545 }
546 slots
547 }
548
549 fn slot_indices_by_recency(&self, slots: &[crate::layout::PlanAsideSlot]) -> Vec<usize> {
553 let asides = self.asides();
554 let mut indices: Vec<usize> = (0..slots.len()).collect();
555 indices.sort_by_key(|index| {
556 let slot = &slots[*index];
557 let explicit = self
558 .aside_slot_mru
559 .iter()
560 .position(|kind| *kind == slot.kind);
561 let fallback = asides
562 .iter()
563 .rposition(|surface| slot.children.contains(&surface.id))
564 .unwrap_or(0);
565 std::cmp::Reverse((explicit.is_some(), explicit.unwrap_or(fallback), fallback))
566 });
567 indices
568 }
569
570 pub fn check_invariants(&self) -> Vec<String> {
572 let mut v = Vec::new();
573 let mains = self.mains().len();
574 let asides = self.asides().len();
575 if asides > 0 && mains == 0 {
576 v.push("asides>0 but mains==0 (no primary, no companion)".into());
577 }
578 match &self.active_main_id {
579 Some(id) if self.role_of(id) != Some(Role::Main) => {
580 v.push(format!("activeMainId '{id}' is not a main"));
581 }
582 None if mains > 0 => v.push("mains exist but activeMainId is None".into()),
583 _ => {}
584 }
585 match &self.root_main_id {
586 Some(id) if self.role_of(id) != Some(Role::Main) => {
587 v.push(format!("rootMainId '{id}' is not a main"));
588 }
589 None if mains > 0 => v.push("mains exist but rootMainId is None".into()),
590 _ => {}
591 }
592 if let Some(f) = &self.focused_surface_id
593 && self.get(f).is_none()
594 {
595 v.push(format!("focusedSurfaceId '{f}' does not exist"));
596 }
597 let mut seen = std::collections::HashSet::new();
599 for s in &self.surfaces {
600 if !seen.insert(&s.id) {
601 v.push(format!("duplicate surface id '{}'", s.id));
602 }
603 }
604 v
605 }
606
607 pub fn is_valid(&self) -> bool {
608 self.check_invariants().is_empty()
609 }
610
611 pub fn derive_layout(&self, size_class: SizeClass) -> DerivedLayout {
613 let main_count = self.mains().len();
614 let aside_count = self.asides().len();
615 let switcher_form = if size_class != SizeClass::Compact && main_count > 1 {
616 match size_class {
617 SizeClass::Expanded => SwitcherForm::Sidebar,
618 SizeClass::Medium => SwitcherForm::Rail,
619 SizeClass::Compact => SwitcherForm::None,
620 }
621 } else {
622 SwitcherForm::None
623 };
624
625 let split_form = if aside_count > 0 {
626 match size_class {
627 SizeClass::Expanded => SplitForm::Split,
628 SizeClass::Medium => SplitForm::Collapsible,
629 SizeClass::Compact => SplitForm::FullScreen,
631 }
632 } else {
633 SplitForm::None
634 };
635
636 DerivedLayout {
637 size_class,
638 switcher_form,
639 split_form,
640 bottom_owner: BottomOwner::App,
641 layout_tree: self.canonical_layout(size_class),
642 }
643 }
644
645 pub(crate) fn presentation_plan(
651 &self,
652 size_class: SizeClass,
653 width: f64,
654 policy: &crate::arbitrate::Policy,
655 ) -> LayoutPresentationPlan {
656 let derived = self.derive_layout(size_class);
657
658 let asides: Vec<PlanAside> = self
659 .asides()
660 .iter()
661 .filter(|s| s.state == SurfaceState::Mounted)
662 .map(|s| PlanAside {
663 id: s.id.clone(),
664 edge: s.placement.edge,
665 preferred_size: s.placement.preferred_size,
666 })
667 .collect();
668 let aside_slots = self.aside_slots_admitted(size_class, width, policy);
669
670 LayoutPresentationPlan {
671 size_class: derived.size_class,
672 bottom_owner: derived.bottom_owner,
673 switcher_form: derived.switcher_form,
674 split_form: derived.split_form,
675 mains: self.main_ids(),
676 active_main_id: self
680 .active_main_id
681 .clone()
682 .or_else(|| self.main_ids().first().cloned()),
683 main_switcher: crate::SurfaceSwitcherSnapshot::derive(
684 self,
685 &std::collections::HashMap::new(),
686 ),
687 asides,
688 aside_slots,
689 floats: self
694 .floats()
695 .iter()
696 .filter(|s| s.state == SurfaceState::Mounted)
697 .map(|s| {
698 let spec = s.float.clone().unwrap_or_default();
699 crate::layout::PlanFloat {
700 id: s.id.clone(),
701 anchor: spec.anchor,
702 dismiss: spec.dismiss,
703 modal: spec.modal,
704 close_button: spec.close_button,
705 }
706 })
707 .collect(),
708 tree: derived.layout_tree,
709 }
710 }
711
712 pub fn canonical_layout(&self, size_class: SizeClass) -> Option<LayoutTree> {
716 let main_ids = self.main_ids();
717 if main_ids.is_empty() {
718 return None;
719 }
720 let aside_ids: Vec<SurfaceId> = self
721 .asides()
722 .iter()
723 .filter(|surface| surface.state == SurfaceState::Mounted)
724 .map(|s| s.id.clone())
725 .collect();
726 let active = self
727 .active_main_id
728 .clone()
729 .unwrap_or_else(|| main_ids[0].clone());
730
731 let tabs_of = |ids: Vec<SurfaceId>| {
732 if ids.len() == 1 {
733 LayoutTree::Leaf {
734 surface_id: ids[0].clone(),
735 }
736 } else {
737 LayoutTree::Tabs {
738 active_id: active.clone(),
739 children: ids,
740 }
741 }
742 };
743
744 if size_class == SizeClass::Compact {
746 let mut ids = main_ids;
747 ids.extend(aside_ids);
748 return Some(tabs_of(ids));
749 }
750
751 let main_node = tabs_of(main_ids);
752 if aside_ids.is_empty() {
753 return Some(main_node);
754 }
755 let mut children = vec![main_node];
756 children.extend(
757 aside_ids
758 .into_iter()
759 .map(|id| LayoutTree::Leaf { surface_id: id }),
760 );
761 let n = children.len();
762 Some(LayoutTree::Split {
763 axis: Axis::Horizontal,
764 weights: vec![1.0 / n as f64; n],
765 children,
766 })
767 }
768}
769
770fn pick_successor_main(surfaces: &[Surface], removed_pos: usize) -> Option<SurfaceId> {
773 surfaces
774 .iter()
775 .skip(removed_pos)
776 .find(|s| s.role == Role::Main)
777 .or_else(|| {
778 surfaces
779 .iter()
780 .take(removed_pos)
781 .rev()
782 .find(|s| s.role == Role::Main)
783 })
784 .map(|s| s.id.clone())
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790
791 #[test]
792 fn replacing_modal_float_does_not_push_another_focus_snapshot() {
793 let mut graph = SurfaceGraph::new();
794 graph.insert(Surface::lxapp("home", Role::Main, "home"));
795 let mut modal = Surface::native("dialog", Role::Float, "dialog");
796 modal.float = Some(crate::model::FloatSpec {
797 modal: true,
798 ..Default::default()
799 });
800
801 graph.insert(modal.clone());
802 graph.set_focus("dialog");
803 graph.insert(modal);
804
805 assert_eq!(graph.modal_focus_stack.len(), 1);
806 graph.close("dialog");
807 assert_eq!(graph.focused_surface_id.as_deref(), Some("home"));
808 assert!(graph.modal_focus_stack.is_empty());
809 }
810
811 #[test]
812 fn inserting_another_role_cannot_replace_the_stable_root() {
813 let mut graph = SurfaceGraph::new();
814 graph.insert(Surface::native("terminal", Role::Main, "terminal"));
815 graph.insert(Surface::lxapp("tools", Role::Main, "tools"));
816
817 graph.insert(Surface::native("terminal", Role::Aside, "terminal"));
818
819 assert_eq!(graph.root_main_id(), Some("terminal"));
820 assert_eq!(graph.role_of("terminal"), Some(Role::Main));
821 assert!(graph.asides().is_empty());
822 assert!(graph.is_valid());
823 }
824}