⎇ 95bc028eeab35a55ae8bef41f820de73348d5baa
02_ruway/pata/pata-core/src/wire.rs
History
Who touched what
raw
1//! Representación **postcard-safe** del marco para akasha (wawa).
2//!
3//! El modelo de [`Config`](crate::Config) está afinado para el loader TOML de
4//! Linux: `WidgetSpec.props` usa `#[serde(flatten)]` (para que las props vivan al
5//! nivel del `kind`) y [`Prop`] es `#[serde(untagged)]` (para que TOML infiera el
6//! tipo). Ambos atributos **rompen postcard** —el codec de akasha—, que no es
7//! auto-descriptivo y no soporta `deserialize_any`.
8//!
9//! Este módulo define un espejo *plano y etiquetado* del modelo: [`WireConfig`]
10//! es exactamente la misma información, pero serializable con cualquier formato
11//! binario no auto-descriptivo. El kernel de wawa serializa el `WireConfig` con
12//! postcard, lo guarda en el grafo direccionado por contenido y lo lee de vuelta
13//! —el config del marco viaja por akasha como cualquier otro objeto—. En Linux
14//! el camino TOML sigue usando `Config` directo; este espejo es sólo para el
15//! cruce a wawa.
16//!
17//! Las conversiones son **sin pérdida**: `Config → WireConfig → Config` devuelve
18//! el mismo modelo (lo fija un test).
19
20use alloc::string::String;
21use alloc::vec::Vec;
22
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26use crate::config::{
27 Anchor, Config, FloatingCard, General, Prop, SidebarTab, Surface, SurfaceKind, WidgetSpec,
28};
29
30/// Valor de propiedad **etiquetado** (a diferencia de [`Prop`], que es
31/// `untagged`): así postcard puede deserializarlo sin auto-descripción.
32#[derive(Debug, Clone, PartialEq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34pub enum WireProp {
35 Bool(bool),
36 Int(i64),
37 Num(f64),
38 Str(String),
39}
40
41impl From<&Prop> for WireProp {
42 fn from(p: &Prop) -> Self {
43 match p {
44 Prop::Bool(b) => WireProp::Bool(*b),
45 Prop::Int(i) => WireProp::Int(*i),
46 Prop::Num(n) => WireProp::Num(*n),
47 Prop::Str(s) => WireProp::Str(s.clone()),
48 }
49 }
50}
51
52impl From<WireProp> for Prop {
53 fn from(p: WireProp) -> Self {
54 match p {
55 WireProp::Bool(b) => Prop::Bool(b),
56 WireProp::Int(i) => Prop::Int(i),
57 WireProp::Num(n) => Prop::Num(n),
58 WireProp::Str(s) => Prop::Str(s),
59 }
60 }
61}
62
63/// Espejo de [`WidgetSpec`] con las props como **lista ordenada** (no `flatten`).
64#[derive(Debug, Clone, PartialEq)]
65#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
66pub struct WireWidget {
67 pub kind: String,
68 pub props: Vec<(String, WireProp)>,
69}
70
71impl From<&WidgetSpec> for WireWidget {
72 fn from(w: &WidgetSpec) -> Self {
73 Self {
74 kind: w.kind.clone(),
75 props: w.props.iter().map(|(k, v)| (k.clone(), WireProp::from(v))).collect(),
76 }
77 }
78}
79
80impl From<WireWidget> for WidgetSpec {
81 fn from(w: WireWidget) -> Self {
82 let mut spec = WidgetSpec::new(w.kind);
83 for (k, v) in w.props {
84 spec = spec.with(k, Prop::from(v));
85 }
86 spec
87 }
88}
89
90fn a_wire(specs: &[WidgetSpec]) -> Vec<WireWidget> {
91 specs.iter().map(WireWidget::from).collect()
92}
93
94fn de_wire(wires: Vec<WireWidget>) -> Vec<WidgetSpec> {
95 wires.into_iter().map(WidgetSpec::from).collect()
96}
97
98/// Espejo de [`FloatingCard`].
99#[derive(Debug, Clone, PartialEq)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub struct WireCard {
102 pub x: f32,
103 pub y: f32,
104 pub w: f32,
105 pub h: f32,
106 pub title: Option<String>,
107 pub widgets: Vec<WireWidget>,
108}
109
110impl From<&FloatingCard> for WireCard {
111 fn from(c: &FloatingCard) -> Self {
112 Self {
113 x: c.x,
114 y: c.y,
115 w: c.w,
116 h: c.h,
117 title: c.title.clone(),
118 widgets: a_wire(&c.widgets),
119 }
120 }
121}
122
123impl From<WireCard> for FloatingCard {
124 fn from(c: WireCard) -> Self {
125 FloatingCard {
126 x: c.x,
127 y: c.y,
128 w: c.w,
129 h: c.h,
130 title: c.title,
131 widgets: de_wire(c.widgets),
132 }
133 }
134}
135
136/// Espejo de [`SidebarTab`].
137#[derive(Debug, Clone, PartialEq)]
138#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
139pub struct WireTab {
140 pub icon: String,
141 pub label: String,
142 pub content: WireWidget,
143}
144
145impl From<&SidebarTab> for WireTab {
146 fn from(t: &SidebarTab) -> Self {
147 Self {
148 icon: t.icon.clone(),
149 label: t.label.clone(),
150 content: WireWidget::from(&t.content),
151 }
152 }
153}
154
155impl From<WireTab> for SidebarTab {
156 fn from(t: WireTab) -> Self {
157 SidebarTab {
158 icon: t.icon,
159 label: t.label,
160 content: WidgetSpec::from(t.content),
161 }
162 }
163}
164
165/// Espejo de [`Surface`].
166#[derive(Debug, Clone, PartialEq)]
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168pub struct WireSurface {
169 pub kind: SurfaceKind,
170 pub anchor: Anchor,
171 pub thickness: f32,
172 pub autohide: bool,
173 pub padding: f32,
174 pub gap: f32,
175 pub start: Vec<WireWidget>,
176 pub center: Vec<WireWidget>,
177 pub end: Vec<WireWidget>,
178 pub cards: Vec<WireCard>,
179 pub output: String,
180 pub tabs: Vec<WireTab>,
181 pub panel_width: f32,
182}
183
184impl From<&Surface> for WireSurface {
185 fn from(s: &Surface) -> Self {
186 Self {
187 kind: s.kind,
188 anchor: s.anchor,
189 thickness: s.thickness,
190 autohide: s.autohide,
191 padding: s.padding,
192 gap: s.gap,
193 start: a_wire(&s.start),
194 center: a_wire(&s.center),
195 end: a_wire(&s.end),
196 cards: s.cards.iter().map(WireCard::from).collect(),
197 output: s.output.clone(),
198 tabs: s.tabs.iter().map(WireTab::from).collect(),
199 panel_width: s.panel_width,
200 }
201 }
202}
203
204impl From<WireSurface> for Surface {
205 fn from(s: WireSurface) -> Self {
206 Surface {
207 kind: s.kind,
208 anchor: s.anchor,
209 thickness: s.thickness,
210 autohide: s.autohide,
211 padding: s.padding,
212 gap: s.gap,
213 start: de_wire(s.start),
214 center: de_wire(s.center),
215 end: de_wire(s.end),
216 cards: s.cards.into_iter().map(FloatingCard::from).collect(),
217 output: s.output,
218 tabs: s.tabs.into_iter().map(SidebarTab::from).collect(),
219 panel_width: s.panel_width,
220 }
221 }
222}
223
224/// Espejo postcard-safe de [`Config`]: la misma información, sin `flatten` ni
225/// `untagged`. Es lo que viaja por akasha en wawa.
226#[derive(Debug, Clone, PartialEq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct WireConfig {
229 pub general: General,
230 pub surfaces: Vec<WireSurface>,
231}
232
233impl From<&Config> for WireConfig {
234 fn from(c: &Config) -> Self {
235 Self {
236 general: c.general.clone(),
237 surfaces: c.surfaces.iter().map(WireSurface::from).collect(),
238 }
239 }
240}
241
242impl From<WireConfig> for Config {
243 fn from(c: WireConfig) -> Self {
244 Config {
245 general: c.general,
246 surfaces: c.surfaces.into_iter().map(Surface::from).collect(),
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::config::{Prop, SidebarTab};
255 use crate::{Config, Surface, WidgetSpec};
256 use alloc::string::ToString;
257
258 /// Un config con todos los rincones poblados (props de cada tipo, una
259 /// tarjeta flotante) para que el round-trip cubra el caso difícil.
260 fn config_rico() -> Config {
261 let mut top = Surface::bar(Anchor::Top);
262 top.thickness = 32.0;
263 top.start = alloc::vec![WidgetSpec::new("start_button").with("label", Prop::Str("⊞".to_string()))];
264 top.center = alloc::vec![WidgetSpec::new("clock")
265 .with("format", Prop::Str("%H:%M".to_string()))
266 .with("size", Prop::Int(14))
267 .with("ratio", Prop::Num(0.5))
268 .with("flag", Prop::Bool(true))];
269 top.end = alloc::vec![WidgetSpec::new("ram_meter")];
270
271 let mut panel = Surface::default();
272 panel.kind = SurfaceKind::Panel;
273 panel.cards = alloc::vec![FloatingCard {
274 x: 40.0,
275 y: 80.0,
276 w: 220.0,
277 h: 110.0,
278 title: Some("sistema".to_string()),
279 widgets: alloc::vec![WidgetSpec::new("cpu_meter"), WidgetSpec::new("ram_meter")],
280 }];
281
282 let mut sidebar = Surface::sidebar(Anchor::Left);
283 sidebar.panel_width = 300.0;
284 sidebar.tabs = alloc::vec![
285 SidebarTab::new(
286 "monads",
287 "Mónadas",
288 WidgetSpec::new("navigator").with("source", Prop::Str("nouser".to_string())),
289 ),
290 SidebarTab::new("files", "Archivos", WidgetSpec::new("navigator")),
291 ];
292
293 let mut cfg = Config::default();
294 cfg.surfaces.push(top);
295 cfg.surfaces.push(panel);
296 cfg.surfaces.push(sidebar);
297 cfg
298 }
299
300 #[test]
301 fn round_trip_sin_perdida() {
302 let cfg = config_rico();
303 let wire = WireConfig::from(&cfg);
304 let vuelta: Config = wire.into();
305 assert_eq!(cfg, vuelta);
306 }
307
308 #[test]
309 fn round_trip_postcard() {
310 // El caso real de akasha: serializar con postcard (no auto-descriptivo,
311 // sin soporte de flatten/untagged) y volver. Falla con `Config` directo;
312 // funciona con `WireConfig`.
313 let cfg = config_rico();
314 let wire = WireConfig::from(&cfg);
315 let bytes = postcard::to_allocvec(&wire).expect("postcard serializa el wire");
316 let wire2: WireConfig = postcard::from_bytes(&bytes).expect("postcard deserializa el wire");
317 let vuelta: Config = wire2.into();
318 assert_eq!(cfg, vuelta);
319 }
320}