GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/display/serialize.rs (5.9K)
1 use crate::widget::*;
2
3 fn serialize_single_widget(w: &dyn WidgetHost, json: &mut String) {
4 let (x, y, width, height) = w.rect();
5 let label = w.label().or_else(|| w.base().label.clone()).unwrap_or_default();
6 let focused = w.base().focused;
7 let hovered = w.base().hovered;
8 // Concrete value lookup (6bd value shrink — `value` left `WidgetHost`): the
9 // `Input::value` implementors a serialized roster can hold are these five widgets;
10 // everything else always reported the default 0.
11 let a = w.as_any();
12 let value = a
13 .downcast_ref::<Checkbox>()
14 .map(|x| Input::value(x))
15 .or_else(|| a.downcast_ref::<Dropdown>().map(|x| Input::value(x)))
16 .or_else(|| a.downcast_ref::<Slider>().map(|x| Input::value(x)))
17 .or_else(|| a.downcast_ref::<RangeSlider>().map(|x| Input::value(x)))
18 .or_else(|| a.downcast_ref::<Spinbox>().map(|x| Input::value(x)))
19 .unwrap_or(0);
20 let type_name = w.type_name();
21
22 // Escape JSON label
23 let escaped_label = label.replace('\\', "\\\\").replace('"', "\\\"");
24
25 json.push_str(&format!(
26 "{{\"type\":\"{}\",\"label\":\"{}\",\"rect\":[{},{},{},{}],\"focused\":{},\"hovered\":{},\"value\":{}",
27 type_name, escaped_label, x, y, width, height, focused, hovered, value
28 ));
29
30 // Child handling: there is no ctx here, so tree children were never reachable
31 // (the old lookup ran against a fresh empty UiContext). The one child this path
32 // could ever surface is the field-derived one — Paginator's strip
33 // (`Layout::container_children`) — kept via the 6aw concrete downcast.
34 let children: Vec<&(dyn WidgetHost + 'static)> = w
35 .as_any()
36 .downcast_ref::<Paginator>()
37 .map(|p| vec![&p.sidebar_menu as &(dyn WidgetHost + 'static)])
38 .unwrap_or_default();
39 let mut menu_items = Vec::new();
40 let mut is_menu_open = false;
41 let mut is_vertical = false;
42 let mut checked_states = Vec::new();
43 // Concrete capability lookup (Phase 6aw): the MenuController implementors a serialized
44 // roster can hold are Adapted<MenuBar> and Adapted<Paginator> — WidgetHost's discovery
45 // hooks are gone.
46 let mc: Option<&dyn MenuController> = w
47 .as_any()
48 .downcast_ref::<MenuBar>()
49 .map(|m| m as &dyn MenuController)
50 .or_else(|| w.as_any().downcast_ref::<Paginator>().map(|p| p as &dyn MenuController));
51 if let Some(mc) = mc {
52 menu_items = mc.menu_items();
53 is_menu_open = mc.is_menu_open();
54 is_vertical = mc.is_vertical();
55 checked_states = mc.menu_item_checked();
56 }
57
58 if type_name == "Menu" && is_menu_open && !menu_items.is_empty() {
59 json.push_str(",\"children\":[");
60 let mut max_len = 0;
61 for item in &menu_items {
62 max_len = max_len.max(item.len());
63 }
64 let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
65 let dx = if is_vertical { x + width } else { x };
66 let dy = if is_vertical { y } else { y + height };
67
68 for (i, item) in menu_items.iter().enumerate() {
69 if i > 0 {
70 json.push(',');
71 }
72 let item_y = dy + i as f32 * DROPDOWN_ITEM_H;
73 let checked = checked_states.get(i).copied().flatten().unwrap_or(false);
74 let item_escaped = item.replace('\\', "\\\\").replace('"', "\\\"");
75 json.push_str(&format!(
76 "{{\"type\":\"MenuItem\",\"label\":\"{}\",\"rect\":[{},{},{},{}],\"focused\":false,\"hovered\":false,\"value\":{}}}",
77 item_escaped, dx, item_y, dw, DROPDOWN_ITEM_H, if checked { 1 } else { 0 }
78 ));
79 }
80 json.push_str("]}");
81 } else if !children.is_empty() {
82 json.push_str(",\"children\":[");
83 let mut first = true;
84 for child in &children {
85 if !child.visible() {
86 continue;
87 }
88 if !first {
89 json.push(',');
90 }
91 first = false;
92 serialize_single_widget(*child, json);
93 }
94 json.push_str("]}");
95 } else {
96 json.push('}');
97 }
98 }
99
100 /// Serialize the visible widgets' menu state. Takes dyn refs (not boxes): the designer's
101 /// roster is concretely typed since the Phase 6bb retype and lends a per-slot dyn view.
102 pub fn serialize_widgets(widgets: &[&dyn WidgetHost]) -> String {
103 let mut json = String::new();
104 json.push('[');
105 let mut first = true;
106 for w in widgets {
107 if !w.visible() {
108 continue;
109 }
110 if !first {
111 json.push(',');
112 }
113 first = false;
114 serialize_single_widget(&**w, &mut json);
115 }
116 json.push(']');
117 json
118 }
119
120 #[cfg(test)]
121 mod tests {
122 use super::*;
123
124 /// The serialized `value` field must keep matching `Input::value` for every
125 /// value-bearing widget (the concrete lookup replaced the deleted
126 /// `WidgetHost::value` — a new `Input::value` implementor must be added to the
127 /// downcast chain in `serialize_single_widget`).
128 #[test]
129 fn serialized_value_matches_input_value() {
130 let mut cb = Checkbox::new();
131 assert!(cb.set_value_string("true"));
132 let dd = Dropdown::new(vec!["a".into(), "b".into(), "c".into()], 2);
133 let mut sl = Slider::new();
134 assert!(sl.set_value_string("0.7"));
135 let sb = Spinbox::new(7, 0, 10, 1);
136 let btn = Button::new(0.0, 0.0, 10.0, 10.0); // no Input::value — always 0
137
138 for (w, expect) in [
139 (&cb as &dyn WidgetHost, 1),
140 (&dd as &dyn WidgetHost, 2),
141 (&sl as &dyn WidgetHost, 70),
142 (&sb as &dyn WidgetHost, 7),
143 (&btn as &dyn WidgetHost, 0),
144 ] {
145 let json = serialize_widgets(&[w]);
146 assert!(
147 json.contains(&format!("\"value\":{expect}")),
148 "{} serialized without value {expect}: {json}",
149 w.type_name()
150 );
151 }
152 }
153 }