git.lucas.co / cce-ui
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 }