1use crate::model::{DockModel, DockNode, FloatingPanel, PanelId, SplitAxis};
2
3const TAG: &str = "dock-v1";
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum DecodeError {
8 Expected { what: &'static str, pos: usize },
9
10 Number { pos: usize },
11
12 Trailing { pos: usize },
13}
14
15impl std::fmt::Display for DecodeError {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 DecodeError::Expected { what, pos } => {
19 write!(f, "dock: oczekiwano {what} @{pos}")
20 }
21 DecodeError::Number { pos } => write!(f, "dock: wrong number @{pos}"),
22 DecodeError::Trailing { pos } => write!(f, "dock: nadmiar danych @{pos}"),
23 }
24 }
25}
26
27impl std::error::Error for DecodeError {}
28
29pub fn encode(model: &DockModel) -> String {
30 let mut s = String::from(TAG);
31 s.push_str(" root=");
32 match &model.root {
33 None => s.push('-'),
34 Some(n) => encode_node(n, &mut s),
35 }
36 s.push_str(" float=");
37 for (i, f) in model.floating.iter().enumerate() {
38 if i > 0 {
39 s.push(';');
40 }
41 encode_float(f, &mut s);
42 }
43 s
44}
45
46pub fn decode(text: &str) -> Result<DockModel, DecodeError> {
47 let rest = text.strip_prefix(TAG).ok_or(DecodeError::Expected {
48 what: "tag `dock-v1`",
49 pos: 0,
50 })?;
51 let rest = rest.strip_prefix(' ').ok_or(DecodeError::Expected {
52 what: "spacja po tagu",
53 pos: TAG.len(),
54 })?;
55
56 let base = TAG.len() + 1;
57 let split_at = rest.find(" float=").ok_or(DecodeError::Expected {
58 what: "` float=`",
59 pos: base,
60 })?;
61 let (root_part, float_part) = (&rest[..split_at], &rest[split_at + " float=".len()..]);
62 let float_base = base + split_at + " float=".len();
63 let root_src = root_part
64 .strip_prefix("root=")
65 .ok_or(DecodeError::Expected {
66 what: "`root=`",
67 pos: base,
68 })?;
69 let root = if root_src == "-" {
70 None
71 } else {
72 let mut p = Parser {
73 src: root_src.as_bytes(),
74 pos: 0,
75 base: base + "root=".len(),
76 };
77 let n = parse_node(&mut p)?;
78 p.expect_end()?;
79 Some(n)
80 };
81 let mut floating = Vec::new();
82 if !float_part.is_empty() {
83 let mut off = float_base;
84 for item in float_part.split(';') {
85 floating.push(parse_float(item, off)?);
86 off += item.len() + 1;
87 }
88 }
89 let mut model = DockModel { root, floating };
90 model.sanitize();
91 Ok(model)
92}
93
94fn split_once<'a>(s: &'a str, pat: &str) -> Option<(&'a str, &'a str)> {
95 let i = s.find(pat)?;
96 Some((&s[..i], &s[i + pat.len()..]))
97}
98
99fn encode_node(n: &DockNode, s: &mut String) {
100 use std::fmt::Write as _;
101 match n {
102 DockNode::Leaf(p) => {
103 let _ = write!(s, "L{}", p.get());
104 }
105 DockNode::Tabs { tabs, active } => {
106 let _ = write!(s, "T[{active}]{{");
107 for (i, t) in tabs.iter().enumerate() {
108 if i > 0 {
109 s.push(',');
110 }
111 let _ = write!(s, "{}", t.get());
112 }
113 s.push('}');
114 }
115 DockNode::Split {
116 axis,
117 frac,
118 first,
119 second,
120 } => {
121 let a = match axis {
122 SplitAxis::Horizontal => 'H',
123 SplitAxis::Vertical => 'V',
124 };
125 let _ = write!(s, "S({a},{frac:.4},");
126 encode_node(first, s);
127 s.push(',');
128 encode_node(second, s);
129 s.push(')');
130 }
131 }
132}
133
134fn encode_float(f: &FloatingPanel, s: &mut String) {
135 use std::fmt::Write as _;
136 let _ = write!(s, "F{}@{},{},{},{}", f.panel.get(), f.x, f.y, f.w, f.h);
137 if let Some(w) = f.window {
138 let _ = write!(s, "#{w}");
139 }
140}
141
142struct Parser<'a> {
143 src: &'a [u8],
144 pos: usize,
145
146 base: usize,
147}
148
149impl Parser<'_> {
150 fn abs(&self) -> usize {
151 self.base + self.pos
152 }
153
154 fn err<T>(&self, what: &'static str) -> Result<T, DecodeError> {
155 Err(DecodeError::Expected {
156 what,
157 pos: self.abs(),
158 })
159 }
160
161 fn peek(&self) -> Option<u8> {
162 self.src.get(self.pos).copied()
163 }
164
165 fn eat(&mut self, b: u8) -> bool {
166 if self.peek() == Some(b) {
167 self.pos += 1;
168 true
169 } else {
170 false
171 }
172 }
173
174 fn expect(&mut self, b: u8, what: &'static str) -> Result<(), DecodeError> {
175 if self.eat(b) { Ok(()) } else { self.err(what) }
176 }
177
178 fn expect_end(&self) -> Result<(), DecodeError> {
179 if self.pos == self.src.len() {
180 Ok(())
181 } else {
182 Err(DecodeError::Trailing { pos: self.abs() })
183 }
184 }
185
186 fn number(&mut self) -> Result<String, DecodeError> {
187 let start = self.pos;
188 let mut seen = false;
189 while let Some(c) = self.peek() {
190 if c.is_ascii_alphanumeric() || c == b'.' || c == b'-' || c == b'+' {
191 self.pos += 1;
192 seen = true;
193 } else {
194 break;
195 }
196 }
197 if seen {
198 Ok(String::from_utf8_lossy(&self.src[start..self.pos]).into_owned())
199 } else {
200 self.err("liczby")
201 }
202 }
203
204 fn u64_num(&mut self) -> Result<u64, DecodeError> {
205 let t = self.number()?;
206 t.parse::<u64>()
207 .map_err(|_| DecodeError::Number { pos: self.abs() })
208 }
209
210 fn f32_num(&mut self) -> Result<f32, DecodeError> {
211 let t = self.number()?;
212 t.parse::<f32>()
213 .map_err(|_| DecodeError::Number { pos: self.abs() })
214 }
215}
216
217fn parse_node(p: &mut Parser<'_>) -> Result<DockNode, DecodeError> {
218 match p.peek() {
219 Some(b'L') => {
220 p.pos += 1;
221 Ok(DockNode::Leaf(PanelId::new(p.u64_num()?)))
222 }
223 Some(b'T') => {
224 p.pos += 1;
225 p.expect(b'[', "`[`")?;
226 let active: usize = p
227 .number()?
228 .parse()
229 .map_err(|_| DecodeError::Number { pos: p.abs() })?;
230 p.expect(b']', "`]`")?;
231 p.expect(b'{', "`{`")?;
232 let mut tabs = Vec::new();
233
234 tabs.push(PanelId::new(p.u64_num()?));
235 while p.eat(b',') {
236 tabs.push(PanelId::new(p.u64_num()?));
237 }
238 p.expect(b'}', "`}`")?;
239 if tabs.is_empty() || active >= tabs.len() {
240 return p.err("active poza zakresem");
241 }
242 Ok(DockNode::Tabs { tabs, active })
243 }
244 Some(b'S') => {
245 p.pos += 1;
246 p.expect(b'(', "`(`")?;
247 let axis = match p.peek() {
248 Some(b'H') => {
249 p.pos += 1;
250 SplitAxis::Horizontal
251 }
252 Some(b'V') => {
253 p.pos += 1;
254 SplitAxis::Vertical
255 }
256 _ => return p.err("H|V axis"),
257 };
258 p.expect(b',', "`,`")?;
259 let frac = p.f32_num()?;
260 if !frac.is_finite() {
261 return p.err("frac must be finished");
262 }
263 p.expect(b',', "`,`")?;
264 let first = parse_node(p)?;
265 p.expect(b',', "`,`")?;
266 let second = parse_node(p)?;
267 p.expect(b')', "`)`")?;
268 Ok(DockNode::Split {
269 axis,
270 frac: DockNode::sanitized_frac(frac),
271 first: Box::new(first),
272 second: Box::new(second),
273 })
274 }
275 _ => p.err("L|T|S node"),
276 }
277}
278
279fn parse_float(item: &str, base: usize) -> Result<FloatingPanel, DecodeError> {
280 let body = item.strip_prefix('F').ok_or(DecodeError::Expected {
281 what: "`F{id}@x,y,w,h`",
282 pos: base,
283 })?;
284 let (id_part, rest) = split_once(body, "@").ok_or(DecodeError::Expected {
285 what: "`@`",
286 pos: base + 1,
287 })?;
288 let id_base = base + 1;
289 let id: u64 = id_part
290 .parse()
291 .map_err(|_| DecodeError::Number { pos: id_base })?;
292 let (geom, win) = match split_once(rest, "#") {
293 Some((g, w)) => (g, Some(w)),
294 None => (rest, None),
295 };
296 let geom_base = id_base + id_part.len() + 1;
297 let nums: Vec<&str> = geom.split(',').collect();
298 if nums.len() != 4 {
299 return Err(DecodeError::Expected {
300 what: "`x,y,w,h`",
301 pos: geom_base,
302 });
303 }
304 let parse = |s: &str| {
305 s.parse::<f32>()
306 .map_err(|_| DecodeError::Number { pos: geom_base })
307 };
308 let (x, y, w, h) = (
309 parse(nums[0])?,
310 parse(nums[1])?,
311 parse(nums[2])?,
312 parse(nums[3])?,
313 );
314 let window = match win {
315 Some(w) => Some(w.parse::<u64>().map_err(|_| DecodeError::Number {
316 pos: geom_base + geom.len() + 1,
317 })?),
318 None => None,
319 };
320 Ok(FloatingPanel {
321 panel: PanelId::new(id),
322 x,
323 y,
324 w,
325 h,
326 window,
327 })
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::drop::DockDrop;
334 use crate::model::DockModel;
335 use crate::ops::DockOp;
336
337 fn sample() -> DockModel {
338 let mut m = DockModel::single(PanelId::new(1));
339 m.apply(DockOp::Insert {
340 panel: PanelId::new(2),
341 target: PanelId::new(1),
342 drop: DockDrop::Right,
343 frac: 0.3,
344 moving: false,
345 });
346 m.apply(DockOp::Insert {
347 panel: PanelId::new(3),
348 target: PanelId::new(2),
349 drop: DockDrop::Tab,
350 frac: 0.5,
351 moving: false,
352 });
353 m.apply(DockOp::Float {
354 panel: PanelId::new(3),
355 x: 40.0,
356 y: 40.0,
357 w: 320.0,
358 h: 240.0,
359 });
360
361 m.apply(DockOp::Insert {
362 panel: PanelId::new(4),
363 target: PanelId::new(2),
364 drop: DockDrop::Tab,
365 frac: 0.5,
366 moving: false,
367 });
368 m
369 }
370
371 #[test]
372 fn roundtrip() {
373 let m = sample();
374 let s = encode(&m);
375 let back = decode(&s).expect("roundtrip");
376 assert_eq!(m, back, "{s}");
377 }
378
379 #[test]
380 fn empty_roundtrip() {
381 let m = DockModel::new();
382 assert_eq!(decode(&encode(&m)).unwrap(), m);
383 }
384
385 #[test]
386 fn bad_inputs_error_not_panic() {
387 assert!(decode("xxx").is_err());
388
389 assert!(decode("dock-v1 root=L1 float=").is_ok());
390 assert!(decode("dock-v1 root=L1").is_err());
391 assert!(decode("dock-v1 root=S(H,0.5,L1) float=").is_err());
392 assert!(decode("dock-v1 root=L1 float=F1@1,2").is_err());
393 assert!(decode("dock-v1 root=T[5]{1,2} float=").is_err());
394 assert!(decode("dock-v1 root=T[0]{1,2 float=").is_err());
395 }
396
397 #[test]
398 fn tabs_inside_split_with_sibling_roundtrip() {
399 let text = "dock-v1 root=S(H,0.72,T[0]{2,3},L5) float=";
400 let back = decode(text).expect("tabs + siblings");
401
402 let again = decode(&encode(&back)).expect("re-encode");
403 assert_eq!(back, again);
404 match back.root {
405 Some(DockNode::Split { .. }) => {}
406 other => panic!("oczekiwano splitu, jest {other:?}"),
407 }
408 }
409
410 #[test]
411 fn window_float_roundtrip() {
412 let mut m = DockModel::single(PanelId::new(1));
413 m.apply(DockOp::Float {
414 panel: PanelId::new(1),
415 x: 0.0,
416 y: 0.0,
417 w: 400.0,
418 h: 300.0,
419 });
420 m.attach_window(PanelId::new(1), Some(7));
421 let back = decode(&encode(&m)).unwrap();
422 assert_eq!(back.floating[0].window, Some(7));
423 }
424}