Skip to main content

nemo_plugin/
builder.rs

1//! Core builder types and traits
2
3use indexmap::IndexMap;
4use nemo_plugin_api::PluginValue;
5
6/// A builder that can be converted to a PluginValue
7pub trait Builder {
8    /// Convert this builder to a PluginValue
9    fn build(self) -> PluginValue;
10}
11
12/// Base builder for creating UI components and layouts
13pub struct LayoutBuilder {
14    attributes: IndexMap<String, PluginValue>,
15    children: Option<IndexMap<String, PluginValue>>,
16}
17
18impl LayoutBuilder {
19    /// Create a new layout builder with a component type
20    pub fn new(component_type: impl Into<String>) -> Self {
21        let mut attributes = IndexMap::new();
22        attributes.insert(
23            "type".to_string(),
24            PluginValue::String(component_type.into()),
25        );
26
27        Self {
28            attributes,
29            children: None,
30        }
31    }
32
33    /// Set an attribute on this layout
34    pub fn attr(mut self, key: impl Into<String>, value: impl Into<PluginValue>) -> Self {
35        self.attributes.insert(key.into(), value.into());
36        self
37    }
38
39    /// Set multiple attributes at once
40    pub fn attrs(mut self, attrs: &[(&str, PluginValue)]) -> Self {
41        for (k, v) in attrs {
42            self.attributes.insert(k.to_string(), v.clone());
43        }
44        self
45    }
46
47    /// Add a child component with an ID
48    pub fn child(mut self, id: impl Into<String>, child: impl Builder) -> Self {
49        let children = self.children.get_or_insert_with(IndexMap::new);
50        children.insert(id.into(), child.build());
51        self
52    }
53
54    /// Add multiple children at once
55    pub fn children(mut self, children: impl IntoIterator<Item = (String, PluginValue)>) -> Self {
56        let child_map = self.children.get_or_insert_with(IndexMap::new);
57        for (id, value) in children {
58            child_map.insert(id, value);
59        }
60        self
61    }
62
63    /// Set children from an existing map
64    pub fn with_children(mut self, children: IndexMap<String, PluginValue>) -> Self {
65        self.children = Some(children);
66        self
67    }
68}
69
70impl Builder for LayoutBuilder {
71    fn build(mut self) -> PluginValue {
72        if let Some(children) = self.children {
73            self.attributes
74                .insert("component".to_string(), PluginValue::Object(children));
75        }
76        PluginValue::Object(self.attributes)
77    }
78}
79
80/// Extension methods for setting common attributes
81impl LayoutBuilder {
82    /// Set the width
83    pub fn width(self, width: i64) -> Self {
84        self.attr("width", PluginValue::Integer(width))
85    }
86
87    /// Set the height
88    pub fn height(self, height: i64) -> Self {
89        self.attr("height", PluginValue::Integer(height))
90    }
91
92    /// Set the padding
93    pub fn padding(self, padding: i64) -> Self {
94        self.attr("padding", PluginValue::Integer(padding))
95    }
96
97    /// Set the margin
98    pub fn margin(self, margin: i64) -> Self {
99        self.attr("margin", PluginValue::Integer(margin))
100    }
101
102    /// Set the border width
103    pub fn border(self, border: i64) -> Self {
104        self.attr("border", PluginValue::Integer(border))
105    }
106
107    /// Set the border color
108    pub fn border_color(self, color: impl Into<String>) -> Self {
109        self.attr("border_color", PluginValue::String(color.into()))
110    }
111
112    /// Set the background color
113    pub fn bg_color(self, color: impl Into<String>) -> Self {
114        self.attr("bg_color", PluginValue::String(color.into()))
115    }
116
117    /// Set the shadow
118    pub fn shadow(self, shadow: impl Into<String>) -> Self {
119        self.attr("shadow", PluginValue::String(shadow.into()))
120    }
121
122    /// Set a data binding for a property
123    pub fn bind(self, property: impl Into<String>, path: impl Into<String>) -> Self {
124        let bind_key = format!("bind_{}", property.into());
125        self.attr(bind_key, PluginValue::String(path.into()))
126    }
127
128    /// Set an event handler
129    pub fn on(self, event: impl Into<String>, handler: impl Into<String>) -> Self {
130        let event_key = format!("on_{}", event.into());
131        self.attr(event_key, PluginValue::String(handler.into()))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_layout_builder_basic() {
141        let layout = LayoutBuilder::new("button")
142            .attr("label", PluginValue::String("Click".into()))
143            .build();
144
145        match layout {
146            PluginValue::Object(map) => {
147                assert_eq!(map.get("type"), Some(&PluginValue::String("button".into())));
148                assert_eq!(map.get("label"), Some(&PluginValue::String("Click".into())));
149            }
150            _ => panic!("Expected Object"),
151        }
152    }
153
154    #[test]
155    fn test_layout_builder_with_children() {
156        let child = LayoutBuilder::new("label")
157            .attr("text", PluginValue::String("Child".into()))
158            .build();
159
160        let mut children = IndexMap::new();
161        children.insert("child1".to_string(), child);
162
163        let layout = LayoutBuilder::new("panel").with_children(children).build();
164
165        match layout {
166            PluginValue::Object(map) => {
167                assert!(map.contains_key("component"));
168            }
169            _ => panic!("Expected Object"),
170        }
171    }
172
173    #[test]
174    fn test_layout_builder_common_attrs() {
175        let layout = LayoutBuilder::new("panel")
176            .width(100)
177            .height(200)
178            .padding(10)
179            .margin(5)
180            .border(2)
181            .build();
182
183        match layout {
184            PluginValue::Object(map) => {
185                assert_eq!(map.get("width"), Some(&PluginValue::Integer(100)));
186                assert_eq!(map.get("height"), Some(&PluginValue::Integer(200)));
187                assert_eq!(map.get("padding"), Some(&PluginValue::Integer(10)));
188                assert_eq!(map.get("margin"), Some(&PluginValue::Integer(5)));
189                assert_eq!(map.get("border"), Some(&PluginValue::Integer(2)));
190            }
191            _ => panic!("Expected Object"),
192        }
193    }
194
195    #[test]
196    fn test_event_handler() {
197        let layout = LayoutBuilder::new("button")
198            .on("click", "handle_click")
199            .build();
200
201        match layout {
202            PluginValue::Object(map) => {
203                assert_eq!(
204                    map.get("on_click"),
205                    Some(&PluginValue::String("handle_click".into()))
206                );
207            }
208            _ => panic!("Expected Object"),
209        }
210    }
211
212    #[test]
213    fn test_data_binding() {
214        let layout = LayoutBuilder::new("label")
215            .bind("text", "data.message")
216            .build();
217
218        match layout {
219            PluginValue::Object(map) => {
220                assert_eq!(
221                    map.get("bind_text"),
222                    Some(&PluginValue::String("data.message".into()))
223                );
224            }
225            _ => panic!("Expected Object"),
226        }
227    }
228}