Skip to main content

lingxia_surface/
manager.rs

1//! `SurfaceManager` — the stateful per-window driver platforms bind to.
2//!
3//! Wraps a [`SurfaceGraph`] with the current size band and arbitration policy:
4//! open/close requests go through the pure arbiter, width changes resolve the
5//! `SizeClass` with hysteresis, and `derive()` produces the `DerivedLayout` the
6//! skin renders. All layout decisions stay in the shared core; the platform
7//! only maps legacy primitives in and binds the output.
8
9use crate::arbitrate::{Decision, Policy, arbitrate};
10use crate::graph::SurfaceGraph;
11use crate::layout::{DEFAULT_HYSTERESIS, DerivedLayout, LayoutPresentationPlan, SizeClass};
12use crate::model::{Surface, SurfaceId};
13
14/// One window's stateful surface driver.
15#[derive(Debug, Clone)]
16pub struct SurfaceManager {
17    graph: SurfaceGraph,
18    policy: Policy,
19    width: f64,
20    hysteresis: f64,
21    size_class: SizeClass,
22}
23
24impl SurfaceManager {
25    /// New manager for a container of `width` logical px, default policy.
26    pub fn new(width: f64) -> Self {
27        Self::with_policy(width, Policy::default())
28    }
29
30    pub fn with_policy(width: f64, policy: Policy) -> Self {
31        Self {
32            graph: SurfaceGraph::new(),
33            policy,
34            width,
35            hysteresis: DEFAULT_HYSTERESIS,
36            size_class: SizeClass::from_width(width),
37        }
38    }
39
40    pub fn graph(&self) -> &SurfaceGraph {
41        &self.graph
42    }
43    pub fn size_class(&self) -> SizeClass {
44        self.size_class
45    }
46    pub fn width(&self) -> f64 {
47        self.width
48    }
49
50    /// Update the container width. Returns `true` if the `SizeClass` changed
51    /// (after hysteresis) — i.e. when the skin must re-derive its layout.
52    pub fn set_width(&mut self, width: f64) -> bool {
53        self.width = width;
54        let next = SizeClass::resolve(Some(self.size_class), width, self.hysteresis);
55        let changed = next != self.size_class;
56        self.size_class = next;
57        changed
58    }
59
60    /// Open (or replace by id) a surface through the arbiter at the current size.
61    /// Always leaves the graph valid; returns the structured decision.
62    pub fn open(&mut self, request: Surface) -> Decision {
63        let (next, decision) = arbitrate(&self.graph, request, &self.policy, self.size_class);
64        self.graph = next;
65        decision
66    }
67
68    /// Close a surface; returns the ids actually removed (target + cascades).
69    pub fn close(&mut self, id: &str) -> Vec<SurfaceId> {
70        self.graph.remove(id)
71    }
72
73    pub fn set_active_main(&mut self, id: &str) -> bool {
74        self.graph.set_active_main(id)
75    }
76    pub fn set_focus(&mut self, id: &str) -> bool {
77        self.graph.set_focus(id)
78    }
79
80    /// Derive the platform-agnostic layout output at the current size.
81    pub fn derive(&self) -> DerivedLayout {
82        self.graph.derive_layout(self.size_class)
83    }
84
85    /// Build the stable, skin-bindable [`LayoutPresentationPlan`] at the current
86    /// size — the renderable contract platforms reconcile against.
87    pub fn presentation_plan(&self) -> LayoutPresentationPlan {
88        self.graph.presentation_plan(self.size_class)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::layout::{SplitForm, SwitcherForm};
96    use crate::model::{Edge, Role, Surface};
97
98    fn main_s(id: &str) -> Surface {
99        Surface::entry(id, Role::Main, id)
100    }
101    fn aside_s(id: &str, edge: Edge) -> Surface {
102        let mut s = Surface::entry(id, Role::Aside, id);
103        s.placement.edge = Some(edge);
104        s
105    }
106
107    #[test]
108    fn open_then_derive_on_expanded() {
109        let mut m = SurfaceManager::new(1200.0);
110        assert_eq!(m.size_class(), SizeClass::Expanded);
111        assert_eq!(m.open(main_s("home")), Decision::Accepted);
112        assert_eq!(
113            m.open(aside_s("assistant", Edge::Right)),
114            Decision::Accepted
115        );
116        let d = m.derive();
117        assert_eq!(d.split_form, SplitForm::Split);
118        assert!(m.graph().is_valid());
119    }
120
121    #[test]
122    fn aside_on_compact_promotes_without_host_switcher() {
123        let mut m = SurfaceManager::new(390.0); // phone width
124        assert_eq!(m.size_class(), SizeClass::Compact);
125        m.open(main_s("home"));
126        // arbitration promotes the aside to a main on compact.
127        assert_eq!(
128            m.open(aside_s("assistant", Edge::Right)),
129            Decision::FullScreenFallback
130        );
131        let d = m.derive();
132        assert_eq!(d.switcher_form, SwitcherForm::None);
133        assert_eq!(d.bottom_owner, crate::BottomOwner::App);
134        assert!(m.graph().is_valid());
135    }
136
137    #[test]
138    fn width_change_reports_sizeclass_flip_with_hysteresis() {
139        let mut m = SurfaceManager::new(1200.0);
140        // small nudge that stays expanded → no change reported.
141        assert!(!m.set_width(900.0));
142        assert_eq!(m.size_class(), SizeClass::Expanded);
143        // drop to phone width → flips to compact.
144        assert!(m.set_width(390.0));
145        assert_eq!(m.size_class(), SizeClass::Compact);
146        // hovering just under the 600 boundary keeps compact (hysteresis).
147        assert!(!m.set_width(590.0));
148        assert_eq!(m.size_class(), SizeClass::Compact);
149    }
150
151    #[test]
152    fn resize_reflows_existing_aside_without_mutating_roles() {
153        let mut m = SurfaceManager::new(1200.0);
154        m.open(main_s("home"));
155        m.open(aside_s("assistant", Edge::Right));
156        // expanded: real split, aside stays an aside.
157        assert_eq!(m.derive().split_form, SplitForm::Split);
158        assert_eq!(m.graph().role_of("assistant"), Some(Role::Aside));
159        // shrink to compact: same graph, layout re-flows to full-screen.
160        m.set_width(390.0);
161        let d = m.derive();
162        assert_eq!(d.split_form, SplitForm::FullScreen);
163        assert_eq!(d.switcher_form, SwitcherForm::None);
164        // role unchanged → widening back restores the split (reversible).
165        assert_eq!(m.graph().role_of("assistant"), Some(Role::Aside));
166        m.set_width(1200.0);
167        assert_eq!(m.derive().split_form, SplitForm::Split);
168    }
169}