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