Skip to main content

lingxia_surface/
model.rs

1//! Surface node data model (§1.1 of the Adaptive Surface Layout spec).
2//!
3//! A `Surface` is one content unit in the graph. Its `role` expresses the
4//! relationship to the main content; `content` is what it shows; `owner`
5//! drives lifecycle; `placement` is a non-authoritative hint.
6
7use serde::{Deserialize, Serialize};
8
9use crate::content::SurfaceContent;
10
11/// Stable identifier of a surface within a graph.
12pub type SurfaceId = String;
13
14/// Relationship of a surface to the main content. The single core abstraction
15/// behind every platform skin (window / panel / sheet / tab …).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Role {
19    /// Switchable top-level content. Only one is the active `primary` at a time.
20    Main,
21    /// Companion shown beside the main content (split axis).
22    Aside,
23    /// Floats above, never occupies the main layout.
24    Float,
25}
26
27/// Which edge a surface docks to (asides) or anchors from.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Edge {
31    Left,
32    Right,
33    Top,
34    Bottom,
35}
36
37/// Owner scope: decides when the surface is closed (§5).
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(tag = "scope", rename_all = "camelCase")]
40pub enum SurfaceOwner {
41    Page { page_instance_id: String },
42    Lxapp { app_id: String },
43    Host,
44}
45
46/// Placement hint (input). Authoritative layout is the `LayoutTree` (output).
47#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct Placement {
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub edge: Option<Edge>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub preferred_size: Option<f64>,
54}
55
56/// Runtime state of a surface.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum SurfaceState {
60    Mounted,
61    Hidden,
62    Minimized,
63}
64
65/// How a `float` surface anchors.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(tag = "to", rename_all = "lowercase")]
68pub enum FloatAnchor {
69    Screen,
70    Surface { surface_id: SurfaceId },
71}
72
73/// How a `float` surface is dismissed.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub enum FloatDismiss {
77    TapOutside,
78    Manual,
79}
80
81/// User interaction contract for a dynamically presented surface.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct SurfaceInteraction {
85    pub close_button: bool,
86    pub dismiss: FloatDismiss,
87    pub modal: bool,
88}
89
90impl SurfaceInteraction {
91    pub const fn standard() -> Self {
92        Self {
93            close_button: false,
94            dismiss: FloatDismiss::TapOutside,
95            modal: false,
96        }
97    }
98
99    pub const fn url_callback() -> Self {
100        Self {
101            close_button: true,
102            dismiss: FloatDismiss::Manual,
103            modal: true,
104        }
105    }
106
107    pub const fn window() -> Self {
108        Self {
109            close_button: false,
110            dismiss: FloatDismiss::Manual,
111            modal: false,
112        }
113    }
114}
115
116impl Default for SurfaceInteraction {
117    fn default() -> Self {
118        Self::standard()
119    }
120}
121
122/// Minimal semantics carried only by `float` surfaces (§1.1).
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct FloatSpec {
126    pub anchor: FloatAnchor,
127    pub dismiss: FloatDismiss,
128    /// Whether it blocks input to layers below (drives focus-restore, §1.5).
129    pub modal: bool,
130    /// Whether the platform renders the standard circular close control.
131    pub close_button: bool,
132}
133
134impl Default for FloatSpec {
135    fn default() -> Self {
136        Self {
137            anchor: FloatAnchor::Screen,
138            dismiss: FloatDismiss::TapOutside,
139            modal: false,
140            close_button: false,
141        }
142    }
143}
144
145/// One node in the Surface Graph.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct Surface {
149    pub id: SurfaceId,
150    pub role: Role,
151    pub content: SurfaceContent,
152    pub owner: SurfaceOwner,
153    #[serde(default)]
154    pub placement: Placement,
155    pub state: SurfaceState,
156    /// Present only for `role == Float`.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub float: Option<FloatSpec>,
159}
160
161impl Surface {
162    pub fn lxapp(id: impl Into<SurfaceId>, role: Role, app_id: impl Into<String>) -> Self {
163        Self {
164            id: id.into(),
165            role,
166            content: SurfaceContent::Lxapp {
167                app_id: app_id.into(),
168                path: None,
169            },
170            owner: SurfaceOwner::Host,
171            placement: Placement::default(),
172            state: SurfaceState::Mounted,
173            float: if role == Role::Float {
174                Some(FloatSpec::default())
175            } else {
176                None
177            },
178        }
179    }
180
181    pub fn native(id: impl Into<SurfaceId>, role: Role, capability: impl Into<String>) -> Self {
182        Self::native_instance(id, role, capability, None)
183    }
184
185    pub fn native_instance(
186        id: impl Into<SurfaceId>,
187        role: Role,
188        capability: impl Into<String>,
189        instance_key: Option<String>,
190    ) -> Self {
191        Self {
192            id: id.into(),
193            role,
194            content: SurfaceContent::Native {
195                capability: capability.into(),
196                instance_key,
197            },
198            owner: SurfaceOwner::Host,
199            placement: Placement::default(),
200            state: SurfaceState::Mounted,
201            float: (role == Role::Float).then(FloatSpec::default),
202        }
203    }
204
205    pub fn browser(id: impl Into<SurfaceId>, role: Role, initial_url: impl Into<String>) -> Self {
206        Self {
207            id: id.into(),
208            role,
209            content: SurfaceContent::Browser {
210                initial_url: initial_url.into(),
211                reuse_by_url: true,
212            },
213            owner: SurfaceOwner::Host,
214            placement: Placement::default(),
215            state: SurfaceState::Mounted,
216            float: (role == Role::Float).then(FloatSpec::default),
217        }
218    }
219
220    pub fn is_modal_float(&self) -> bool {
221        self.role == Role::Float && self.float.as_ref().is_some_and(|f| f.modal)
222    }
223}