dampen_iced/
lib.rs

1//! Dampen Iced - Iced Backend Implementation
2
3pub mod builder;
4pub mod convert;
5pub mod style_mapping;
6pub mod system_theme;
7pub mod theme_adapter;
8
9// Re-export system theme subscription for production use
10pub use system_theme::watch_system_theme;
11
12use dampen_core::{AttributeValue, Backend, EventKind, InterpolatedPart, WidgetKind, WidgetNode};
13use iced::widget::{button, column, row, text};
14use iced::{Element, Renderer, Theme};
15
16/// Standard message type for handler-based applications
17#[derive(Clone, Debug, PartialEq)]
18pub enum HandlerMessage {
19    /// Simple handler with optional payload
20    Handler(String, Option<String>),
21}
22
23// Re-export builder
24pub use builder::DampenWidgetBuilder;
25
26/// Iced backend implementation
27pub struct IcedBackend {
28    message_handler: Box<dyn Fn(String, Option<String>) -> Box<dyn CloneableMessage> + 'static>,
29}
30
31impl IcedBackend {
32    /// Create a new Iced backend with a message handler
33    pub fn new<F>(handler: F) -> Self
34    where
35        F: Fn(String, Option<String>) -> Box<dyn CloneableMessage> + 'static,
36    {
37        Self {
38            message_handler: Box::new(handler),
39        }
40    }
41}
42
43/// Trait for messages that can be cloned
44pub trait CloneableMessage: std::fmt::Debug + Send + Sync + 'static {
45    fn clone_box(&self) -> Box<dyn CloneableMessage>;
46}
47
48impl<T> CloneableMessage for T
49where
50    T: Clone + std::fmt::Debug + Send + Sync + 'static,
51{
52    fn clone_box(&self) -> Box<dyn CloneableMessage> {
53        Box::new(self.clone())
54    }
55}
56
57impl Clone for Box<dyn CloneableMessage> {
58    fn clone(&self) -> Self {
59        self.clone_box()
60    }
61}
62
63impl Backend for IcedBackend {
64    type Widget<'a> = Element<'a, Box<dyn CloneableMessage>, Theme, Renderer>;
65    type Message = Box<dyn CloneableMessage>;
66
67    fn text<'a>(&self, content: &str) -> Self::Widget<'a> {
68        text(content.to_string()).into()
69    }
70
71    fn button<'a>(
72        &self,
73        label: Self::Widget<'a>,
74        on_press: Option<Self::Message>,
75    ) -> Self::Widget<'a> {
76        let btn = button(label);
77        if let Some(msg) = on_press {
78            btn.on_press(msg).into()
79        } else {
80            btn.into()
81        }
82    }
83
84    fn column<'a>(&self, children: Vec<Self::Widget<'a>>) -> Self::Widget<'a> {
85        column(children).into()
86    }
87
88    fn row<'a>(&self, children: Vec<Self::Widget<'a>>) -> Self::Widget<'a> {
89        row(children).into()
90    }
91
92    fn container<'a>(&self, content: Self::Widget<'a>) -> Self::Widget<'a> {
93        // Iced 0.14 doesn't have a simple container() helper, use column as placeholder
94        // In a full implementation, you'd use iced::widget::container with proper imports
95        column(vec![content]).into()
96    }
97
98    fn scrollable<'a>(&self, content: Self::Widget<'a>) -> Self::Widget<'a> {
99        // Placeholder - Iced 0.14 has scrollable but needs feature flags
100        column(vec![content]).into()
101    }
102
103    fn stack<'a>(&self, children: Vec<Self::Widget<'a>>) -> Self::Widget<'a> {
104        // Stack is not in Iced 0.14 core - use column as placeholder
105        column(children).into()
106    }
107
108    fn text_input<'a>(
109        &self,
110        _placeholder: &str,
111        _value: &str,
112        _on_input: Option<Self::Message>,
113    ) -> Self::Widget<'a> {
114        // Placeholder - text_input needs proper message handling
115        text("[text_input]").into()
116    }
117
118    fn checkbox<'a>(
119        &self,
120        _label: &str,
121        _is_checked: bool,
122        _on_toggle: Option<Self::Message>,
123    ) -> Self::Widget<'a> {
124        // Placeholder - checkbox needs proper message handling
125        text("[checkbox]").into()
126    }
127
128    fn slider<'a>(
129        &self,
130        _min: f32,
131        _max: f32,
132        _value: f32,
133        _on_change: Option<Self::Message>,
134    ) -> Self::Widget<'a> {
135        // Placeholder - slider needs proper message handling
136        text("[slider]").into()
137    }
138
139    fn pick_list<'a>(
140        &self,
141        _options: Vec<&str>,
142        _selected: Option<&str>,
143        _on_select: Option<Self::Message>,
144    ) -> Self::Widget<'a> {
145        // Placeholder - pick_list needs proper message handling
146        text("[pick_list]").into()
147    }
148
149    fn toggler<'a>(
150        &self,
151        _label: &str,
152        _is_active: bool,
153        _on_toggle: Option<Self::Message>,
154    ) -> Self::Widget<'a> {
155        // Placeholder - toggler needs proper message handling
156        text("[toggler]").into()
157    }
158
159    fn image<'a>(&self, _path: &str) -> Self::Widget<'a> {
160        // Placeholder - image needs feature flag
161        text("[image]").into()
162    }
163
164    fn svg<'a>(&self, _path: &str) -> Self::Widget<'a> {
165        // Placeholder - SVG not in core Iced
166        text("[svg]").into()
167    }
168
169    fn space<'a>(&self) -> Self::Widget<'a> {
170        // Placeholder - space needs proper implementation
171        text("").into()
172    }
173
174    fn rule<'a>(&self) -> Self::Widget<'a> {
175        // Placeholder - rule needs proper implementation
176        text("─").into()
177    }
178
179    fn radio<'a>(
180        &self,
181        _label: &str,
182        _value: &str,
183        _selected: Option<&str>,
184        _on_select: Option<Self::Message>,
185    ) -> Self::Widget<'a> {
186        // Placeholder - radio is fully implemented in DampenWidgetBuilder
187        // This legacy method is kept for backwards compatibility
188        text("[radio]").into()
189    }
190}
191
192/// Render a widget node to an Iced element
193///
194/// Note: This is a simplified version. In a full implementation, this would receive
195/// a model and evaluate bindings. For now, it handles static values.
196/// Render a widget tree with layout and style support
197pub fn render_with_layout<'a>(
198    node: &WidgetNode,
199    backend: &IcedBackend,
200) -> Element<'a, Box<dyn CloneableMessage>, Theme, Renderer> {
201    use crate::style_mapping::{map_layout_constraints, map_style_properties};
202    use iced::widget::container;
203
204    // First render the base widget
205    let widget = render(node, backend);
206
207    // Apply layout constraints
208    let layout = node.layout.as_ref().map(map_layout_constraints);
209
210    // Apply style properties
211    let style = node.style.as_ref().map(map_style_properties);
212
213    // Wrap widget in container with layout and style
214    if layout.is_some() || style.is_some() {
215        let mut container = container(widget);
216
217        if let Some(layout) = layout {
218            container = container
219                .width(layout.width)
220                .height(layout.height)
221                .padding(layout.padding);
222
223            // Apply align_items (vertical alignment of children)
224            if let Some(align) = layout.align_items {
225                container = container.align_y(align);
226            }
227
228            // Apply direct alignment (align_x, align_y)
229            if let Some(align_x) = layout.align_x {
230                container = container.align_x(align_x);
231            }
232            if let Some(align_y) = layout.align_y {
233                container = container.align_y(align_y);
234            }
235        }
236
237        if let Some(style) = style {
238            container = container.style(move |_theme| style);
239        }
240
241        container.into()
242    } else {
243        widget
244    }
245}
246
247pub fn render<'a>(
248    node: &WidgetNode,
249    backend: &IcedBackend,
250) -> Element<'a, Box<dyn CloneableMessage>, Theme, Renderer> {
251    match node.kind {
252        WidgetKind::Text => {
253            // Get the value attribute
254            let value = match node.attributes.get("value") {
255                Some(AttributeValue::Static(v)) => v.clone(),
256                Some(AttributeValue::Binding(_)) => {
257                    // Binding would be evaluated with model
258                    "[binding]".to_string()
259                }
260                Some(AttributeValue::Interpolated(parts)) => {
261                    // Interpolated would be evaluated with model
262                    format_interpolated(parts)
263                }
264                None => String::new(),
265            };
266            backend.text(&value)
267        }
268        WidgetKind::Button => {
269            // Get label
270            let label_text = match node.attributes.get("label") {
271                Some(AttributeValue::Static(l)) => l.clone(),
272                Some(AttributeValue::Binding(_)) => "[binding]".to_string(),
273                Some(AttributeValue::Interpolated(parts)) => format_interpolated(parts),
274                None => String::new(),
275            };
276            let label = backend.text(&label_text);
277
278            // Find click handler
279            let on_press = node
280                .events
281                .iter()
282                .find(|e| e.event == EventKind::Click)
283                .map(|e| {
284                    let handler_name = e.handler.clone();
285                    (backend.message_handler)(handler_name, None)
286                });
287
288            backend.button(label, on_press)
289        }
290        WidgetKind::Column => {
291            let children: Vec<_> = node
292                .children
293                .iter()
294                .map(|child| render(child, backend))
295                .collect();
296            backend.column(children)
297        }
298        WidgetKind::Row => {
299            let children: Vec<_> = node
300                .children
301                .iter()
302                .map(|child| render(child, backend))
303                .collect();
304            backend.row(children)
305        }
306        WidgetKind::Container => {
307            let children: Vec<_> = node
308                .children
309                .iter()
310                .map(|child| render(child, backend))
311                .collect();
312            if let Some(first_child) = children.into_iter().next() {
313                backend.container(first_child)
314            } else {
315                backend.container(backend.text(""))
316            }
317        }
318        WidgetKind::Scrollable => {
319            let children: Vec<_> = node
320                .children
321                .iter()
322                .map(|child| render(child, backend))
323                .collect();
324            if let Some(first_child) = children.into_iter().next() {
325                backend.scrollable(first_child)
326            } else {
327                backend.scrollable(backend.text(""))
328            }
329        }
330        WidgetKind::Stack => {
331            let children: Vec<_> = node
332                .children
333                .iter()
334                .map(|child| render(child, backend))
335                .collect();
336            backend.stack(children)
337        }
338        WidgetKind::TextInput => {
339            let placeholder = match node.attributes.get("placeholder") {
340                Some(AttributeValue::Static(v)) => v.clone(),
341                _ => String::new(),
342            };
343            let value = match node.attributes.get("value") {
344                Some(AttributeValue::Static(v)) => v.clone(),
345                Some(AttributeValue::Binding(_)) => "[binding]".to_string(),
346                Some(AttributeValue::Interpolated(parts)) => format_interpolated(parts),
347                None => String::new(),
348            };
349            // Find input handler
350            let on_input = node
351                .events
352                .iter()
353                .find(|e| e.event == EventKind::Input)
354                .map(|e| {
355                    let handler_name = e.handler.clone();
356                    (backend.message_handler)(handler_name, None)
357                });
358            backend.text_input(&placeholder, &value, on_input)
359        }
360        WidgetKind::Checkbox => {
361            let label = match node.attributes.get("label") {
362                Some(AttributeValue::Static(l)) => l.clone(),
363                _ => String::new(),
364            };
365            let is_checked = match node.attributes.get("checked") {
366                Some(AttributeValue::Static(v)) => v == "true" || v == "1",
367                _ => false,
368            };
369            // Find toggle handler
370            let on_toggle = node
371                .events
372                .iter()
373                .find(|e| e.event == EventKind::Toggle)
374                .map(|e| {
375                    let handler_name = e.handler.clone();
376                    (backend.message_handler)(handler_name, None)
377                });
378            backend.checkbox(&label, is_checked, on_toggle)
379        }
380        WidgetKind::Slider => {
381            let min = match node.attributes.get("min") {
382                Some(AttributeValue::Static(v)) => v.parse::<f32>().unwrap_or(0.0),
383                _ => 0.0,
384            };
385            let max = match node.attributes.get("max") {
386                Some(AttributeValue::Static(v)) => v.parse::<f32>().unwrap_or(100.0),
387                _ => 100.0,
388            };
389            let value = match node.attributes.get("value") {
390                Some(AttributeValue::Static(v)) => v.parse::<f32>().unwrap_or(50.0),
391                _ => 50.0,
392            };
393            // Find change handler
394            let on_change = node
395                .events
396                .iter()
397                .find(|e| e.event == EventKind::Change)
398                .map(|e| {
399                    let handler_name = e.handler.clone();
400                    (backend.message_handler)(handler_name, None)
401                });
402            backend.slider(min, max, value, on_change)
403        }
404        WidgetKind::PickList => {
405            let options_str = match node.attributes.get("options") {
406                Some(AttributeValue::Static(v)) => v.clone(),
407                _ => String::new(),
408            };
409            let options: Vec<&str> = options_str.split(',').collect();
410            let selected = match node.attributes.get("selected") {
411                Some(AttributeValue::Static(v)) => Some(v.as_str()),
412                _ => None,
413            };
414            // Find select handler
415            let on_select = node
416                .events
417                .iter()
418                .find(|e| e.event == EventKind::Select)
419                .map(|e| {
420                    let handler_name = e.handler.clone();
421                    (backend.message_handler)(handler_name, None)
422                });
423            backend.pick_list(options, selected, on_select)
424        }
425        WidgetKind::Toggler => {
426            let label = match node.attributes.get("label") {
427                Some(AttributeValue::Static(l)) => l.clone(),
428                _ => String::new(),
429            };
430            let is_active = match node.attributes.get("active") {
431                Some(AttributeValue::Static(v)) => v == "true" || v == "1",
432                _ => false,
433            };
434            // Find toggle handler
435            let on_toggle = node
436                .events
437                .iter()
438                .find(|e| e.event == EventKind::Toggle)
439                .map(|e| {
440                    let handler_name = e.handler.clone();
441                    (backend.message_handler)(handler_name, None)
442                });
443            backend.toggler(&label, is_active, on_toggle)
444        }
445        WidgetKind::Image => {
446            let path = match node.attributes.get("src") {
447                Some(AttributeValue::Static(v)) => v.clone(),
448                _ => String::new(),
449            };
450            backend.image(&path)
451        }
452        WidgetKind::Svg => {
453            let path = match node.attributes.get("src") {
454                Some(AttributeValue::Static(v)) => v.clone(),
455                _ => String::new(),
456            };
457            backend.svg(&path)
458        }
459        WidgetKind::Space => backend.space(),
460        WidgetKind::Rule => backend.rule(),
461        WidgetKind::Radio => {
462            let label = match node.attributes.get("label") {
463                Some(AttributeValue::Static(l)) => l.clone(),
464                _ => String::new(),
465            };
466            let value = match node.attributes.get("value") {
467                Some(AttributeValue::Static(v)) => v.clone(),
468                _ => String::new(),
469            };
470            backend.radio(&label, &value, None, None)
471        }
472        WidgetKind::Custom(_) => {
473            // For custom widgets, return empty
474            backend.column(Vec::new())
475        }
476        WidgetKind::ComboBox => backend.column(Vec::new()),
477        WidgetKind::ProgressBar => backend.column(Vec::new()),
478        WidgetKind::Tooltip => backend.column(Vec::new()),
479        WidgetKind::Grid => backend.column(Vec::new()),
480        WidgetKind::Canvas => backend.column(Vec::new()),
481        WidgetKind::Float => backend.column(Vec::new()),
482        WidgetKind::For => backend.column(Vec::new()), // For loop requires model context, not supported in this legacy function
483    }
484}
485
486/// Helper to format interpolated parts (without model evaluation)
487fn format_interpolated(parts: &[InterpolatedPart]) -> String {
488    let mut result = String::new();
489    for part in parts {
490        match part {
491            InterpolatedPart::Literal(literal) => result.push_str(literal),
492            InterpolatedPart::Binding(_) => result.push_str("[binding]"),
493        }
494    }
495    result
496}