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
9/// Stable identifier of a surface within a graph.
10pub type SurfaceId = String;
11
12/// Relationship of a surface to the main content. The single core abstraction
13/// behind every platform skin (window / panel / sheet / tab …).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Role {
17    /// Switchable top-level content. Only one is the active `primary` at a time.
18    Main,
19    /// Companion shown beside the main content (split axis).
20    Aside,
21    /// Floats above, never occupies the main layout.
22    Float,
23}
24
25/// Which edge a surface docks to (asides) or anchors from.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum Edge {
29    Left,
30    Right,
31    Top,
32    Bottom,
33}
34
35/// What a surface shows. Only two kinds: a declared catalog `entry`
36/// (resolved by the host from `lingxia.toml`) or an ad-hoc `web` page.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(tag = "kind", rename_all = "lowercase")]
39pub enum SurfaceContent {
40    /// A `lingxia.toml`-declared app / system feature, opened by id.
41    Entry {
42        id: String,
43        /// Initial route only; subsequent navigation goes through `lx.navigator`.
44        #[serde(default, skip_serializing_if = "Option::is_none")]
45        path: Option<String>,
46    },
47    /// An ad-hoc web page / PDF rendered by the in-app chromed browser. Whether
48    /// it presents as a main browser tab or a docked browser aside is decided by
49    /// `role`, not by the content — the browser always carries its own chrome.
50    Web { url: String },
51}
52
53/// Owner scope: decides when the surface is closed (§5).
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(tag = "scope", rename_all = "camelCase")]
56pub enum SurfaceOwner {
57    Page { page_instance_id: String },
58    Lxapp { app_id: String },
59    Host,
60}
61
62/// Placement hint (input). Authoritative layout is the `LayoutTree` (output).
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Placement {
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub edge: Option<Edge>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub preferred_size: Option<f64>,
70}
71
72/// Runtime state of a surface.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum SurfaceState {
76    Mounted,
77    Hidden,
78    Minimized,
79}
80
81/// How a `float` surface anchors.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(tag = "to", rename_all = "lowercase")]
84pub enum FloatAnchor {
85    Screen,
86    Surface { surface_id: SurfaceId },
87}
88
89/// How a `float` surface is dismissed.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub enum FloatDismiss {
93    TapOutside,
94    Manual,
95}
96
97/// Minimal semantics carried only by `float` surfaces (§1.1).
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct FloatSpec {
101    pub anchor: FloatAnchor,
102    pub dismiss: FloatDismiss,
103    /// Whether it blocks input to layers below (drives focus-restore, §1.5).
104    pub modal: bool,
105}
106
107impl Default for FloatSpec {
108    fn default() -> Self {
109        Self {
110            anchor: FloatAnchor::Screen,
111            dismiss: FloatDismiss::TapOutside,
112            modal: false,
113        }
114    }
115}
116
117/// One node in the Surface Graph.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct Surface {
121    pub id: SurfaceId,
122    pub role: Role,
123    pub content: SurfaceContent,
124    pub owner: SurfaceOwner,
125    #[serde(default)]
126    pub placement: Placement,
127    pub state: SurfaceState,
128    /// Present only for `role == Float`.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub float: Option<FloatSpec>,
131}
132
133impl Surface {
134    /// Convenience constructor for an entry-backed surface.
135    pub fn entry(id: impl Into<SurfaceId>, role: Role, entry_id: impl Into<String>) -> Self {
136        Self {
137            id: id.into(),
138            role,
139            content: SurfaceContent::Entry {
140                id: entry_id.into(),
141                path: None,
142            },
143            owner: SurfaceOwner::Host,
144            placement: Placement::default(),
145            state: SurfaceState::Mounted,
146            float: if role == Role::Float {
147                Some(FloatSpec::default())
148            } else {
149                None
150            },
151        }
152    }
153
154    pub fn is_modal_float(&self) -> bool {
155        self.role == Role::Float && self.float.as_ref().is_some_and(|f| f.modal)
156    }
157}