1use std::collections::{HashMap, HashSet};
15
16use serde_json::{Map, Value};
17
18use crate::{
19 Error,
20 ui::{
21 Node, View,
22 node::{Handler, Trigger},
23 },
24 wire::{Event, Op, TextMode, WireNode, kind},
25};
26
27pub const REGIONS: [&str; 3] = ["main", "dock", "layer"];
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct Tree {
33 id: String,
35 kind: String,
37 props: Map<String, Value>,
39 children: Vec<Self>,
41}
42
43impl Tree {
44 pub fn id(&self) -> &str {
46 &self.id
47 }
48
49 pub fn kind(&self) -> &str {
51 &self.kind
52 }
53
54 pub const fn props(&self) -> &Map<String, Value> {
56 &self.props
57 }
58
59 pub fn children(&self) -> &[Self] {
61 &self.children
62 }
63
64 pub fn wire(&self) -> WireNode {
66 WireNode {
67 id: self.id.clone(),
68 k: self.kind.clone(),
69 p: self.props.clone(),
70 c: self.children.iter().map(Self::wire).collect(),
71 }
72 }
73
74 fn build<M>(id: String, node: Node<M>, routes: &mut Routes<M>) -> Result<Self, Error> {
76 let Node { kind, mut props, children: nodes, handlers } = node;
77 if !handlers.is_empty() {
78 let found = attach(&id, &mut props, handlers);
79 routes.by_id.insert(id.clone(), found);
80 }
81 let mut seen = HashSet::with_capacity(nodes.len());
82 let mut children = Vec::with_capacity(nodes.len());
83 for (i, child) in nodes.into_iter().enumerate() {
84 let key = match child.props.get("key") {
85 Some(Value::String(k)) => k.clone(),
86 Some(Value::Number(n)) => n.to_string(),
87 _ => i.to_string(),
88 };
89 let child_id = format!("{id}.{key}");
90 if !seen.insert(child_id.clone()) {
91 return Err(Error::DuplicateId { path: id, key });
92 }
93 children.push(Self::build(child_id, child, routes)?);
94 }
95 Ok(Self { id, kind, props, children })
96 }
97
98 fn from_json(id: String, node: &Value) -> Result<Self, Error> {
100 let Some(obj) = node.as_object() else {
101 return Err(Error::InvalidView(format!("{id}: a node must be a table, got {node}")));
102 };
103 let Some(kind) = obj.get("k").and_then(Value::as_str) else {
104 return Err(Error::InvalidView(format!("{id}: a node needs a string `k`")));
105 };
106 let props = match obj.get("p") {
107 None | Some(Value::Null) => Map::new(),
108 Some(Value::Object(p)) => p.clone(),
109 Some(other) => {
110 return Err(Error::InvalidView(format!("{id}: `p` must be a table, got {other}")));
111 },
112 };
113 let nodes: &[Value] = match obj.get("c") {
114 None | Some(Value::Null) => &[],
115 Some(Value::Array(c)) => c,
116 Some(Value::Object(c)) if c.is_empty() => &[],
117 Some(other) => {
118 return Err(Error::InvalidView(format!("{id}: `c` must be a list, got {other}")));
119 },
120 };
121 let mut seen = HashSet::with_capacity(nodes.len());
122 let mut children = Vec::with_capacity(nodes.len());
123 for (i, child) in nodes.iter().enumerate() {
124 let key = match child.get("p").and_then(|p| p.get("key")) {
125 Some(Value::String(k)) => k.clone(),
126 Some(Value::Number(n)) => n.to_string(),
127 _ => i.to_string(),
128 };
129 let child_id = format!("{id}.{key}");
130 if !seen.insert(child_id.clone()) {
131 return Err(Error::DuplicateId { path: id, key });
132 }
133 children.push(Self::from_json(child_id, child)?);
134 }
135 Ok(Self { id, kind: kind.to_owned(), props, children })
136 }
137
138 fn has_text(&self) -> bool {
140 kind::has_text(&self.kind)
141 }
142}
143
144enum Route {
146 Act(String),
148 Ev(&'static str),
150}
151
152pub(crate) struct Routes<M> {
154 by_id: HashMap<String, Vec<(Route, Handler<M>)>>,
156}
157
158impl<M> Default for Routes<M> {
159 fn default() -> Self {
160 Self { by_id: HashMap::new() }
161 }
162}
163
164impl<M> Routes<M> {
165 fn at(&self, id: &str, event: &Event, matches: impl Fn(&Route) -> bool) -> Option<M> {
167 self
168 .by_id
169 .get(id)?
170 .iter()
171 .filter(|(r, _)| matches(r))
172 .find_map(|(_, h)| h(event))
173 }
174
175 pub(crate) fn route(&self, event: &Event) -> Option<M> {
178 let id = event.id()?;
179 match event {
180 Event::Action(a) => self.at(id, event, |r| {
181 matches!(r, Route::Act(n) if *n == a.act || n.split_once('=')
182 .is_some_and(|(act, value)| act == a.act && a.value.as_deref() == Some(value)))
183 }),
184 Event::Select(s) | Event::Activate(s) => {
185 let name = event.name();
186 let is = |r: &Route| matches!(r, Route::Ev(e) if *e == name);
187 (s.item != id)
188 .then(|| self.at(&s.item, event, is))
189 .flatten()
190 .or_else(|| self.at(id, event, is))
191 },
192 _ => {
193 let name = event.name();
194 self.at(id, event, |r| matches!(r, Route::Ev(e) if *e == name))
195 },
196 }
197 }
198}
199
200fn attach<M>(
203 id: &str,
204 props: &mut Map<String, Value>,
205 handlers: Vec<(Trigger, Handler<M>)>,
206) -> Vec<(Route, Handler<M>)> {
207 let mut out = Vec::with_capacity(handlers.len());
208 for (trigger, handler) in handlers {
209 let route = match trigger {
210 Trigger::Action(act) => Route::Act(act),
211 Trigger::Event(ev) => Route::Ev(ev),
212 pointer => {
213 let actions = props
214 .entry("actions")
215 .or_insert_with(|| Value::Object(Map::new()));
216 let Value::Object(actions) = actions else {
217 tracing::warn!(id, "`actions` is not an object here; pointer handler ignored");
218 continue;
219 };
220 match pointer {
221 Trigger::Click | Trigger::Dblclick => {
222 let gesture = if pointer == Trigger::Click {
223 "click"
224 } else {
225 "dblclick"
226 };
227 let act = actions
228 .entry(gesture)
229 .or_insert_with(|| Value::String(gesture.into()))
230 .as_str()
231 .unwrap_or(gesture)
232 .to_owned();
233 Route::Act(act)
234 },
235 Trigger::Menu(act) => {
236 let menu = actions
237 .entry("menu")
238 .or_insert_with(|| Value::Array(Vec::new()));
239 if let Value::Array(menu) = menu
240 && !menu.iter().any(|m| m.as_str() == Some(&act))
241 {
242 menu.push(Value::String(act.clone()));
243 }
244 Route::Act(act)
245 },
246 Trigger::Action(_) | Trigger::Event(_) => unreachable!("matched above"),
247 }
248 },
249 };
250 out.push((route, handler));
251 }
252 out
253}
254
255#[derive(Clone, Debug, Default, PartialEq)]
257pub struct Doc {
258 regions: [Option<Tree>; 3],
260}
261
262impl Doc {
263 pub fn new() -> Self {
265 Self::default()
266 }
267
268 pub fn from_view<M>(view: View<M>) -> Result<Self, Error> {
273 Self::build(view).map(|(doc, _)| doc)
274 }
275
276 pub fn from_json(value: &Value) -> Result<Self, Error> {
289 let mut regions = [None, None, None];
290 match value {
291 Value::Null => {},
292 Value::Object(obj) => {
293 if let Some(extra) = obj.keys().find(|k| !REGIONS.contains(&k.as_str())) {
294 return Err(Error::InvalidView(format!(
295 "view: unknown region {extra:?} (main, dock or layer)"
296 )));
297 }
298 for (slot, name) in regions.iter_mut().zip(REGIONS) {
299 if let Some(node) = obj.get(name).filter(|n| !n.is_null()) {
300 *slot = Some(Tree::from_json(name.to_owned(), node)?);
301 }
302 }
303 },
304 other => {
305 return Err(Error::InvalidView(format!(
306 "view: expected {{main?, dock?, layer?}}, got {other}"
307 )));
308 },
309 }
310 Ok(Self { regions })
311 }
312
313 pub(crate) fn build<M>(view: View<M>) -> Result<(Self, Routes<M>), Error> {
315 let mut routes = Routes::default();
316 let mut regions = [None, None, None];
317 let View { main, dock, layer } = view;
318 for ((slot, name), node) in regions.iter_mut().zip(REGIONS).zip([main, dock, layer]) {
319 if let Some(node) = node {
320 *slot = Some(Tree::build(name.to_owned(), node, &mut routes)?);
321 }
322 }
323 Ok((Self { regions }, routes))
324 }
325
326 pub(crate) fn retain_main(&mut self) {
328 self.regions[1] = None;
329 self.regions[2] = None;
330 }
331
332 pub const fn regions(&self) -> &[Option<Tree>; 3] {
334 &self.regions
335 }
336
337 pub fn ops(&self, next: &Self, surface: &str) -> Vec<Op> {
339 let mut ops = Vec::new();
340 for (old, new) in self.regions.iter().zip(&next.regions) {
341 match (old, new) {
342 (None, None) => {},
343 (Some(old), None) => ops.push(Op::Del { id: old.id.clone() }),
344 (None, Some(new)) => ops.push(add(new, surface, None)),
345 (Some(old), Some(new)) if old.kind != new.kind => {
346 ops.push(Op::Del { id: old.id.clone() });
347 ops.push(add(new, surface, None));
348 },
349 (Some(old), Some(new)) => diff(old, new, &mut ops),
350 }
351 }
352 ops
353 }
354}
355
356fn add(tree: &Tree, parent: &str, before: Option<&str>) -> Op {
358 Op::Add { parent: parent.to_owned(), before: before.map(str::to_owned), node: tree.wire() }
359}
360
361fn diff(old: &Tree, new: &Tree, ops: &mut Vec<Op>) {
363 let text = new.has_text();
364 let mut set = Map::new();
365 for (key, value) in &new.props {
366 if text && key == "text" {
367 continue;
368 }
369 if old.props.get(key) != Some(value) {
370 set.insert(key.clone(), value.clone());
371 }
372 }
373 for key in old.props.keys() {
374 let primary = text && key == "text";
375 if !primary && !new.props.contains_key(key) {
376 set.insert(key.clone(), Value::Null);
377 }
378 }
379 if !set.is_empty() {
380 ops.push(Op::Set { id: new.id.clone(), props: set });
381 }
382 if text {
383 let before = old.props.get("text");
384 let after = new.props.get("text");
385 if before != after {
386 match (before.and_then(Value::as_str), after.and_then(Value::as_str)) {
387 (Some(a), Some(b)) if !a.is_empty() && b.starts_with(a) => ops.push(Op::Text {
388 id: new.id.clone(),
389 mode: TextMode::Append,
390 text: b[a.len()..].to_owned(),
391 }),
392 (_, Some(b)) => ops.push(Op::Text {
393 id: new.id.clone(),
394 mode: TextMode::Replace,
395 text: b.to_owned(),
396 }),
397 (_, None) => {
398 let mut props = Map::new();
399 props.insert("text".into(), Value::Null);
400 ops.push(Op::Set { id: new.id.clone(), props });
401 },
402 }
403 }
404 }
405 children(old, new, ops);
406}
407
408fn children(old: &Tree, new: &Tree, ops: &mut Vec<Op>) {
412 let before: HashMap<&str, (usize, &Tree)> = old
413 .children
414 .iter()
415 .enumerate()
416 .map(|(i, c)| (c.id.as_str(), (i, c)))
417 .collect();
418 let kept: HashSet<&str> = new.children.iter().map(|c| c.id.as_str()).collect();
419 for child in &old.children {
420 if !kept.contains(child.id.as_str()) {
421 ops.push(Op::Del { id: child.id.clone() });
422 }
423 }
424 let same: Vec<(usize, usize)> = new
427 .children
428 .iter()
429 .enumerate()
430 .filter_map(|(at, c)| {
431 before
432 .get(c.id.as_str())
433 .filter(|(_, o)| o.kind == c.kind)
434 .map(|(i, _)| (at, *i))
435 })
436 .collect();
437 let stay: HashSet<usize> = rising(&same.iter().map(|(_, i)| *i).collect::<Vec<_>>())
438 .into_iter()
439 .map(|k| same[k].0)
440 .collect();
441 let mut next: Option<&str> = None;
442 for (at, child) in new.children.iter().enumerate().rev() {
443 match before.get(child.id.as_str()) {
444 None => ops.push(add(child, &new.id, next)),
445 Some((_, o)) if o.kind != child.kind => {
446 ops.push(Op::Del { id: child.id.clone() });
447 ops.push(add(child, &new.id, next));
448 },
449 Some((_, o)) => {
450 if !stay.contains(&at) {
451 ops.push(Op::Move {
452 id: child.id.clone(),
453 parent: new.id.clone(),
454 before: next.map(str::to_owned),
455 });
456 }
457 diff(o, child, ops);
458 },
459 }
460 next = Some(child.id.as_str());
461 }
462}
463
464fn rising(seq: &[usize]) -> Vec<usize> {
466 let mut tails: Vec<usize> = Vec::new();
468 let mut prev: Vec<Option<usize>> = vec![None; seq.len()];
469 for (i, &v) in seq.iter().enumerate() {
470 let at = tails.partition_point(|&t| seq[t] < v);
471 prev[i] = at.checked_sub(1).map(|p| tails[p]);
472 if at == tails.len() {
473 tails.push(i);
474 } else {
475 tails[at] = i;
476 }
477 }
478 let mut out = Vec::with_capacity(tails.len());
479 let mut cur = tails.last().copied();
480 while let Some(i) = cur {
481 out.push(i);
482 cur = prev[i];
483 }
484 out.reverse();
485 out
486}
487
488#[cfg(test)]
489mod tests {
490 use serde_json::json;
491
492 use super::*;
493 use crate::ui;
494
495 #[test]
496 fn json_empty_luau_tables_are_empty_children() {
497 let bare = Doc::from_json(&json!({"main": {"k": "col"}})).unwrap();
498 for children in [Value::Null, json!({}), json!([])] {
499 let doc = Doc::from_json(&json!({
500 "main": {"k": "col", "p": null, "c": children},
501 "dock": null, "layer": null,
502 }))
503 .unwrap();
504 assert_eq!(doc, bare);
505 assert!(doc.ops(&bare, "p").is_empty());
506 }
507 let nested = Doc::from_json(&json!({
508 "main": {"k": "col", "c": [{"k": "col", "p": {}, "c": {}}]},
509 }))
510 .unwrap();
511 assert!(
512 nested.regions()[0].as_ref().unwrap().children()[0]
513 .children()
514 .is_empty()
515 );
516 assert_eq!(Doc::from_json(&Value::Null).unwrap(), Doc::new());
517 }
518
519 #[test]
520 fn json_rejects_unknown_regions_even_when_null() {
521 for node in [Value::Null, json!({"k": "col"})] {
522 let error = Doc::from_json(&json!({"side": node})).unwrap_err();
523 assert_eq!(error.to_string(), "view: unknown region \"side\" (main, dock or layer)");
524 }
525 for value in [json!(1), json!([]), json!("main")] {
526 assert!(matches!(Doc::from_json(&value), Err(Error::InvalidView(_))));
527 }
528 }
529
530 #[test]
531 fn json_rejects_malformed_nodes_with_paths() {
532 for (node, message) in [
533 (json!(1), "a node must be a table, got 1"),
534 (json!([]), "a node must be a table, got []"),
535 (json!({}), "a node needs a string `k`"),
536 (json!({"k": false}), "a node needs a string `k`"),
537 (json!({"k": "col", "p": []}), "`p` must be a table, got []"),
538 (json!({"k": "col", "p": 1}), "`p` must be a table, got 1"),
539 (json!({"k": "col", "c": {"bad": 1}}), "`c` must be a list, got {\"bad\":1}"),
540 (json!({"k": "col", "c": false}), "`c` must be a list, got false"),
541 (json!({"k": "col", "c": "bad"}), "`c` must be a list, got \"bad\""),
542 ] {
543 let error = Doc::from_json(&json!({"main": node})).unwrap_err();
544 assert_eq!(error.to_string(), format!("main: {message}"));
545 let error = Doc::from_json(&json!({
546 "main": {"k": "col", "c": [{"k": "col", "p": {"key": "nested"}, "c": [node]}]},
547 }))
548 .unwrap_err();
549 assert_eq!(error.to_string(), format!("main.nested.0: {message}"));
550 }
551 }
552
553 #[test]
554 fn duplicate_child_errors_name_parent_and_key() {
555 for (keys, path, key) in [
556 ([json!("a"), json!("a")], "main", "a"),
557 ([json!(7), json!("7")], "main", "7"),
558 ([json!("a.b"), json!("a.b")], "main", "a.b"),
559 ] {
560 let children = keys.map(|key| json!({"k": "text", "p": {"key": key}}));
561 let node = json!({"k": "col", "c": children});
562 let error = Doc::from_json(&json!({"main": node})).unwrap_err();
563 assert_eq!(error.to_string(), format!("{path}: duplicate child key {key:?}"));
564 assert!(matches!(error, Error::DuplicateId { path: p, key: k } if p == path && k == key));
565 let error = Doc::from_json(&json!({
566 "main": {"k": "col", "c": [node]},
567 }))
568 .unwrap_err();
569 assert_eq!(error.to_string(), format!("main.0: duplicate child key {key:?}"));
570 }
571 let view = View::<()>::new().main([ui::text("a").key("x"), ui::text("b").key("x")]);
572 assert_eq!(Doc::from_view(view).unwrap_err().to_string(), "main: duplicate child key \"x\"");
573 }
574
575 #[test]
576 fn integer_keys_derive_decimal_ids() {
577 let doc = Doc::from_json(&json!({
578 "main": {"k": "col", "c": [
579 {"k": "text", "p": {"key": -42}},
580 {"k": "text", "p": {"key": u64::MAX}},
581 ]},
582 }))
583 .unwrap();
584 let children = doc.regions()[0].as_ref().unwrap().children();
585 assert_eq!(children[0].id(), "main.-42");
586 assert_eq!(children[1].id(), "main.18446744073709551615");
587 }
588
589 #[test]
590 fn pointer_actions_match_both_name_and_value() {
591 let (doc, routes) = Doc::build(View::from(
592 ui::text("open")
593 .prop("actions", json!({"click": "open=one=two", "dblclick": "open=other"}))
594 .on_click(1)
595 .on_dblclick(2)
596 .on_action("fallback", 3),
597 ))
598 .unwrap();
599 assert_eq!(
600 doc.regions()[0].as_ref().unwrap().children()[0].props()["actions"],
601 json!({"click": "open=one=two", "dblclick": "open=other"})
602 );
603 for (act, value, expected) in [
604 ("open", Some("one=two"), Some(1)),
605 ("open", Some("other"), Some(2)),
606 ("open", Some("wrong"), None),
607 ("open", None, None),
608 ("wrong", Some("one=two"), None),
609 ("fallback", Some("anything"), Some(3)),
610 ] {
611 let mut raw = json!({"ev": "action", "sf": "s1", "id": "main.0", "act": act});
612 if let Some(value) = value {
613 raw["value"] = json!(value);
614 }
615 let event = Event::decode(raw.as_object().unwrap().clone());
616 assert_eq!(routes.route(&event), expected);
617 }
618 }
619
620 #[test]
621 fn retaining_main_drops_only_transient_regions() {
622 let mut doc = Doc::from_view(
623 View::<()>::new()
624 .main([ui::text("kept")])
625 .dock([ui::text("draft")])
626 .layer([ui::text("dialog")]),
627 )
628 .unwrap();
629 let main = doc.regions()[0].clone();
630 doc.retain_main();
631 assert_eq!(doc.regions(), &[main, None, None]);
632 let next = Doc::from_view(View::<()>::new().main([ui::text("kept")])).unwrap();
633 assert!(doc.ops(&next, "s1").is_empty());
634 }
635}