Skip to main content

dampen_iced/
lib.rs

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