use crate::compat::HashMap;
use core::fmt;
use crate::app::{
ButtonHandle, CheckBoxHandle, ComboBoxHandle, FrameHandle, GridWidgetHandle, LabelHandle,
LineEditHandle, ListBoxHandle, ListViewHandle, PanelHandle, ProgressBarHandle,
RadioButtonHandle, ScrollAreaHandle, ScrollBarHandle, SliderHandle, SpinBoxHandle,
TabWidgetHandle, TextEditHandle, WidgetHandle, WindowHandle,
};
use crate::core::ObjectId;
pub struct BoundJsonLayout {
name_map: HashMap<String, ObjectId>,
root: Option<ObjectId>,
parent_of: HashMap<ObjectId, ObjectId>,
children_of: HashMap<ObjectId, Vec<ObjectId>>,
kind_of: HashMap<ObjectId, String>,
key_of: HashMap<ObjectId, String>,
}
impl fmt::Debug for BoundJsonLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BoundJsonLayout")
.field("len", &self.len())
.field("has_root", &self.root.is_some())
.field("nodes", &self.parent_of.len())
.finish()
}
}
impl BoundJsonLayout {
pub fn new() -> Self {
Self {
name_map: HashMap::new(),
root: None,
parent_of: HashMap::new(),
children_of: HashMap::new(),
kind_of: HashMap::new(),
key_of: HashMap::new(),
}
}
pub fn register(&mut self, name: impl Into<String>, id: ObjectId) {
self.name_map.insert(name.into(), id);
}
pub fn register_node(
&mut self,
id: ObjectId,
widget_type: impl Into<String>,
key: impl Into<String>,
parent: Option<ObjectId>,
) {
if id == 0 {
return;
}
if let Some(old_parent) = self.parent_of.remove(&id) {
self.unlink_child(old_parent, id);
}
self.kind_of.insert(id, widget_type.into());
let key = key.into();
if key.is_empty() {
self.key_of.remove(&id);
} else {
self.key_of.insert(id, key);
}
self.children_of.entry(id).or_default();
match parent {
Some(p) if p != 0 => {
self.parent_of.insert(id, p);
let siblings = self.children_of.entry(p).or_default();
if !siblings.contains(&id) {
siblings.push(id);
}
}
_ => {
if self.root.is_none() {
self.root = Some(id);
}
}
}
}
fn unregister_name(&mut self, id: ObjectId) {
self.name_map.retain(|_, &mut v| v != id);
}
fn unlink_child(&mut self, parent: ObjectId, id: ObjectId) {
if let Some(siblings) = self.children_of.get_mut(&parent) {
siblings.retain(|&x| x != id);
}
}
fn forget_node(&mut self, id: ObjectId) {
if let Some(parent) = self.parent_of.remove(&id) {
self.unlink_child(parent, id);
}
self.children_of.remove(&id);
self.kind_of.remove(&id);
self.key_of.remove(&id);
if self.root == Some(id) {
self.root = None;
}
self.unregister_name(id);
}
pub fn move_child_to(&mut self, parent: ObjectId, child: ObjectId, index: usize) {
if child == 0 || child == parent {
return;
}
let siblings = self.children_of.entry(parent).or_default();
siblings.retain(|&x| x != child);
let at = index.min(siblings.len());
siblings.insert(at, child);
self.parent_of.insert(child, parent);
self.children_of.entry(child).or_default();
}
pub fn detach(&mut self, id: ObjectId) -> Vec<ObjectId> {
let mut removed: Vec<ObjectId> = Vec::new();
let mut stack = vec![id];
while let Some(current) = stack.pop() {
if !removed.contains(¤t) {
removed.push(current);
}
if let Some(kids) = self.children_of.get(¤t) {
stack.extend(kids.iter().copied());
}
}
removed.retain(|node| self.kind_of.contains_key(node) || self.root == Some(*node));
for node in &removed {
self.forget_node(*node);
}
removed
}
pub fn clear_structure(&mut self) -> Vec<ObjectId> {
let mut removed: Vec<ObjectId> = self.kind_of.keys().copied().collect();
for root in self.root.iter() {
if !removed.contains(root) {
removed.push(*root);
}
}
removed.sort_unstable();
self.root = None;
self.parent_of.clear();
self.children_of.clear();
self.kind_of.clear();
self.key_of.clear();
removed
}
pub fn detach_all_except(&mut self, keep: ObjectId) -> Vec<ObjectId> {
let mut removed: Vec<ObjectId> =
self.kind_of.keys().copied().filter(|&k| k != keep).collect();
for node in &removed {
self.forget_node(*node);
}
self.children_of.insert(keep, Vec::new());
removed.sort_unstable();
removed
}
pub fn root(&self) -> Option<ObjectId> {
self.root
}
pub fn parent(&self, id: ObjectId) -> Option<ObjectId> {
self.parent_of.get(&id).copied()
}
pub fn children(&self, id: ObjectId) -> &[ObjectId] {
self.children_of.get(&id).map_or(&[], Vec::as_slice)
}
pub fn sibling_index(&self, id: ObjectId) -> Option<usize> {
let parent = self.parent(id)?;
self.children(parent).iter().position(|&x| x == id)
}
pub fn widget_name(&self, id: ObjectId) -> Option<&str> {
self.kind_of.get(&id).map(String::as_str)
}
pub fn node_key(&self, id: ObjectId) -> Option<&str> {
self.key_of.get(&id).map(String::as_str)
}
pub fn child_by_key(&self, parent: Option<ObjectId>, key: &str) -> Option<ObjectId> {
let candidates: &[ObjectId] = match parent {
Some(p) => self.children(p),
None => self.root.as_slice(),
};
candidates.iter().copied().find(|&id| self.node_key(id) == Some(key))
}
pub fn node_count(&self) -> usize {
self.kind_of.len()
}
pub fn depth_of(&self, id: ObjectId) -> Option<usize> {
if !self.kind_of.contains_key(&id) && self.root != Some(id) {
return None;
}
let mut depth = 0usize;
let mut current = id;
while let Some(parent) = self.parent(current) {
depth += 1;
current = parent;
if depth > 4096 {
return Some(depth);
}
}
Some(depth)
}
pub fn walk(&self) -> Vec<(ObjectId, usize, usize)> {
let mut out = Vec::new();
if let Some(root) = self.root {
self.walk_into(root, 0, 0, &mut out);
}
out
}
fn walk_into(
&self,
id: ObjectId,
depth: usize,
index: usize,
out: &mut Vec<(ObjectId, usize, usize)>,
) {
out.push((id, depth, index));
for (i, child) in self.children(id).iter().enumerate() {
self.walk_into(*child, depth + 1, i, out);
}
}
pub fn id(&self, name: &str) -> Option<ObjectId> {
self.name_map.get(name).copied()
}
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.name_map.keys().map(String::as_str)
}
pub fn len(&self) -> usize {
self.name_map.len()
}
pub fn is_empty(&self) -> bool {
self.name_map.is_empty()
}
pub fn widget_by_name<T: WidgetHandle>(&self, name: &str) -> Result<T, String> {
let raw_id = self.id(name).ok_or_else(|| {
format!(
"layout has no widget named '{name}'; available ids are {:?}",
self.ids().collect::<Vec<_>>()
)
})?;
Ok(T::from_raw(raw_id))
}
pub fn button(&self, name: &str) -> Result<ButtonHandle, String> {
self.widget_by_name(name)
}
pub fn label(&self, name: &str) -> Result<LabelHandle, String> {
self.widget_by_name(name)
}
pub fn line_edit(&self, name: &str) -> Result<LineEditHandle, String> {
self.widget_by_name(name)
}
pub fn checkbox(&self, name: &str) -> Result<CheckBoxHandle, String> {
self.widget_by_name(name)
}
pub fn radio_button(&self, name: &str) -> Result<RadioButtonHandle, String> {
self.widget_by_name(name)
}
pub fn combo_box(&self, name: &str) -> Result<ComboBoxHandle, String> {
self.widget_by_name(name)
}
pub fn list_box(&self, name: &str) -> Result<ListBoxHandle, String> {
self.widget_by_name(name)
}
pub fn slider(&self, name: &str) -> Result<SliderHandle, String> {
self.widget_by_name(name)
}
pub fn progress_bar(&self, name: &str) -> Result<ProgressBarHandle, String> {
self.widget_by_name(name)
}
pub fn panel(&self, name: &str) -> Result<PanelHandle, String> {
self.widget_by_name(name)
}
pub fn spin_box(&self, name: &str) -> Result<SpinBoxHandle, String> {
self.widget_by_name(name)
}
pub fn list_view(&self, name: &str) -> Result<ListViewHandle, String> {
self.widget_by_name(name)
}
pub fn scroll_area(&self, name: &str) -> Result<ScrollAreaHandle, String> {
self.widget_by_name(name)
}
pub fn text_edit(&self, name: &str) -> Result<TextEditHandle, String> {
self.widget_by_name(name)
}
pub fn scroll_bar(&self, name: &str) -> Result<ScrollBarHandle, String> {
self.widget_by_name(name)
}
pub fn tab_widget(&self, name: &str) -> Result<TabWidgetHandle, String> {
self.widget_by_name(name)
}
pub fn grid_widget(&self, name: &str) -> Result<GridWidgetHandle, String> {
self.widget_by_name(name)
}
pub fn frame(&self, name: &str) -> Result<FrameHandle, String> {
self.widget_by_name(name)
}
pub fn window(&self, name: &str) -> Result<WindowHandle, String> {
self.widget_by_name(name)
}
}
crate::impl_default_via_new!(BoundJsonLayout);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_creates_empty_layout() {
let layout = BoundJsonLayout::new();
assert_eq!(layout.len(), 0);
assert!(layout.is_empty());
}
#[test]
fn register_and_retrieve_id() {
let mut layout = BoundJsonLayout::new();
let id = 42;
layout.register("my_widget", id);
assert_eq!(layout.len(), 1);
assert!(!layout.is_empty());
assert_eq!(layout.id("my_widget"), Some(id));
}
#[test]
fn id_returns_none_for_unknown() {
let layout = BoundJsonLayout::new();
assert_eq!(layout.id("nonexistent"), None);
}
#[test]
fn register_multiple_widgets() {
let mut layout = BoundJsonLayout::new();
layout.register("btn1", 1);
layout.register("btn2", 2);
layout.register("label1", 3);
assert_eq!(layout.len(), 3);
}
#[test]
fn duplicate_name_overwrites() {
let mut layout = BoundJsonLayout::new();
let id1 = 10;
let id2 = 20;
layout.register("dup", id1);
layout.register("dup", id2);
assert_eq!(layout.len(), 1);
assert_eq!(layout.id("dup"), Some(id2));
}
#[test]
fn widget_by_name_not_found_error() {
let layout = BoundJsonLayout::new();
let result = layout.widget_by_name::<LabelHandle>("missing");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("missing"), "{err}");
assert!(err.contains("available ids"), "{err}");
}
#[test]
fn convenience_handle_methods() {
let mut layout = BoundJsonLayout::new();
let id = 1;
layout.register("my_btn", id);
layout.register("my_label", id);
layout.register("my_edit", id);
assert!(layout.button("my_btn").is_ok());
assert!(layout.label("my_label").is_ok());
assert!(layout.line_edit("my_edit").is_ok());
}
#[test]
fn convenience_handles_return_err_for_missing() {
let layout = BoundJsonLayout::new();
assert!(layout.button("no_such").is_err());
assert!(layout.checkbox("no_such").is_err());
assert!(layout.combo_box("no_such").is_err());
assert!(layout.slider("no_such").is_err());
assert!(layout.progress_bar("no_such").is_err());
assert!(layout.panel("no_such").is_err());
assert!(layout.spin_box("no_such").is_err());
assert!(layout.scroll_area("no_such").is_err());
assert!(layout.tab_widget("no_such").is_err());
assert!(layout.grid_widget("no_such").is_err());
assert!(layout.frame("no_such").is_err());
assert!(layout.window("no_such").is_err());
}
#[test]
fn default_is_empty() {
let layout = BoundJsonLayout::default();
assert!(layout.is_empty());
assert_eq!(layout.len(), 0);
}
#[test]
fn register_string_and_str() {
let mut layout = BoundJsonLayout::new();
let id = 99;
layout.register("from_str".to_string(), id);
layout.register("from_ref", id);
assert_eq!(layout.id("from_str"), Some(id));
assert_eq!(layout.id("from_ref"), Some(id));
}
}