Skip to main content

ferroui_controls/
design.rs

1use crate::templates::{FuncTemplate, IDataTemplate, TemplateRef};
2use crate::Control;
3use ferroui_base::data::BindingPriority;
4use ferroui_base::layout::Layoutable;
5use ferroui_base::styling::IStyle;
6use ferroui_base::{
7    ferro_property, AttachedProperty, BoxedValue, FerroObject, FerroObjectExtensions, FerroProperty, Ref, StyledElement, Visual,
8};
9use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11use std::rc::Rc;
12
13/// A template that creates the control a design-time preview is hosted in.
14pub type PreviewTemplate = TemplateRef<Option<Ref<Control>>>;
15
16/// The object a design-time value is recorded for. The entry keeps the
17/// object alive, so its address identifies it for as long as the entry
18/// exists.
19#[allow(dead_code)]
20enum Target {
21    Object(Ref<FerroObject>),
22    DataTemplate(Rc<dyn IDataTemplate>),
23    Style(Rc<dyn IStyle>),
24}
25
26fn object_key(target: &FerroObject) -> usize {
27    target as *const FerroObject as usize
28}
29
30fn data_template_key(target: &Rc<dyn IDataTemplate>) -> usize {
31    Rc::as_ptr(target) as *const () as usize
32}
33
34/// A style that is an object has the key of that object.
35fn style_key(target: &Rc<dyn IStyle>) -> usize {
36    match target.as_object() {
37        Some(object) => object_key(object),
38        None => Rc::as_ptr(target) as *const () as usize,
39    }
40}
41
42ferroui_base::ferro_thread_local! {
43    static IS_DESIGN_MODE: Cell<bool> = const { Cell::new(false) };
44    static PREVIEW_WITH: RefCell<HashMap<usize, (Target, Option<PreviewTemplate>)>> = RefCell::new(HashMap::new());
45    static TEMPLATE_DATA_CONTEXT: RefCell<HashMap<usize, (Rc<dyn IDataTemplate>, Option<BoxedValue>)>> =
46        RefCell::new(HashMap::new());
47}
48
49fn set_preview(key: usize, target: Target, template: Option<PreviewTemplate>) {
50    let previous = PREVIEW_WITH.with(|map| map.borrow_mut().insert(key, (target, template)));
51    drop(previous);
52}
53
54fn get_preview(key: usize) -> Option<Ref<Control>> {
55    let template = PREVIEW_WITH.with(|map| map.borrow().get(&key).and_then(|(_, template)| template.clone()));
56    template.and_then(|template| template.build_typed())
57}
58
59fn control_template(control: Ref<Control>) -> PreviewTemplate {
60    FuncTemplate::new(move || Some(control.clone()))
61}
62
63fn build_template(template: PreviewTemplate) -> PreviewTemplate {
64    FuncTemplate::new(move || template.build_typed())
65}
66
67/// Helper class for design-time property support.
68pub struct Design;
69
70ferroui_base::ferro_static_type!(Design);
71
72ferroui_base::ferro_properties! { impl Design, also [
73    Design::height_property,
74    Design::width_property,
75    Design::data_context_property,
76    Design::preview_with_property,
77    Design::design_style_property,
78] {} }
79
80impl Design {
81    /// Whether the application is running in design mode.
82    ///
83    /// This property is set by the previewer when it hosts the application;
84    /// it is false in a running application.
85    pub fn is_design_mode() -> bool {
86        IS_DESIGN_MODE.get()
87    }
88
89    /// Sets whether the application is running in design mode. For design
90    /// hosts.
91    #[doc(hidden)]
92    pub fn set_is_design_mode(value: bool) {
93        IS_DESIGN_MODE.set(value)
94    }
95
96    ferro_property!(for Design;
97        /// Defines the `Height` attached property: a height for the control
98        /// that is applied at design time only.
99        pub fn height_property() -> AttachedProperty<f64> {
100            FerroProperty::register_attached::<Design, Control, _>("Height", 0.0)
101        }
102    );
103
104    /// Sets the design-time height for a control.
105    pub fn set_height(control: &Control, value: f64) {
106        control.set_value(Self::height_property(), value)
107    }
108
109    /// Gets the design-time height for a control.
110    pub fn get_height(control: &Control) -> f64 {
111        control.get_value(Self::height_property())
112    }
113
114    ferro_property!(for Design;
115        /// Defines the `Width` attached property: a width for the control
116        /// that is applied at design time only.
117        pub fn width_property() -> AttachedProperty<f64> {
118            FerroProperty::register_attached::<Design, Control, _>("Width", 0.0)
119        }
120    );
121
122    /// Sets the design-time width for a control.
123    pub fn set_width(control: &Control, value: f64) {
124        control.set_value(Self::width_property(), value)
125    }
126
127    /// Gets the design-time width for a control.
128    pub fn get_width(control: &Control) -> f64 {
129        control.get_value(Self::width_property())
130    }
131
132    ferro_property!(for Design;
133        /// Defines the `DataContext` attached property: a data context for
134        /// the control that is applied at design time only.
135        pub fn data_context_property() -> AttachedProperty<Option<BoxedValue>> {
136            FerroProperty::register_attached::<Design, Control, _>("DataContext", None)
137        }
138    );
139
140    /// Sets the design-time data context for a control.
141    pub fn set_data_context(control: &Control, value: Option<BoxedValue>) {
142        control.set_value(Self::data_context_property(), value)
143    }
144
145    /// Gets the design-time data context for a control.
146    pub fn get_data_context(control: &Control) -> Option<BoxedValue> {
147        control.get_value(Self::data_context_property())
148    }
149
150    /// Sets the design-time data context for a data template.
151    pub fn set_data_context_for_data_template(control: &Rc<dyn IDataTemplate>, value: Option<BoxedValue>) {
152        let previous = TEMPLATE_DATA_CONTEXT
153            .with(|map| map.borrow_mut().insert(data_template_key(control), (control.clone(), value)));
154        drop(previous);
155    }
156
157    /// Gets the design-time data context for a data template.
158    pub fn get_data_context_for_data_template(control: &Rc<dyn IDataTemplate>) -> Option<BoxedValue> {
159        TEMPLATE_DATA_CONTEXT
160            .with(|map| map.borrow().get(&data_template_key(control)).and_then(|(_, value)| value.clone()))
161    }
162
163    ferro_property!(for Design;
164        /// Defines the `PreviewWith` attached property: a control that
165        /// hosts the object in the previewer.
166        pub fn preview_with_property() -> AttachedProperty<Option<Ref<Control>>> {
167            FerroProperty::register_attached::<Design, FerroObject, _>("PreviewWith", None)
168        }
169    );
170
171    /// Sets a preview template for the specified object at design time.
172    ///
173    /// This method allows you to specify a substitute control template to
174    /// be used to preview the given object in the previewer.
175    pub fn set_preview_with_template(target: &Ref<FerroObject>, template: Option<PreviewTemplate>) {
176        set_preview(object_key(target), Target::Object(target.clone()), template)
177    }
178
179    /// Sets a preview control for the specified object at design time.
180    ///
181    /// This method allows you to specify a substitute control to be used to
182    /// preview the given object in the previewer. A visual cannot be
183    /// previewed with a control: it needs a template.
184    pub fn set_preview_with(target: &Ref<FerroObject>, control: Option<Ref<Control>>) {
185        let template = match control {
186            // Not a supported scenario without templates; causes stack overflows.
187            Some(_) if target.is::<Visual>() => None,
188            Some(control) => Some(control_template(control)),
189            None => None,
190        };
191        set_preview(object_key(target), Target::Object(target.clone()), template)
192    }
193
194    /// Sets a preview template for the specified data template at design
195    /// time.
196    pub fn set_preview_with_template_for_data_template(
197        target: &Rc<dyn IDataTemplate>,
198        template: Option<PreviewTemplate>,
199    ) {
200        set_preview(data_template_key(target), Target::DataTemplate(target.clone()), template.map(build_template))
201    }
202
203    /// Sets a preview control for the specified data template at design
204    /// time.
205    pub fn set_preview_with_for_data_template(target: &Rc<dyn IDataTemplate>, control: Option<Ref<Control>>) {
206        set_preview(data_template_key(target), Target::DataTemplate(target.clone()), control.map(control_template))
207    }
208
209    /// Sets a preview template for the specified style at design time.
210    pub fn set_preview_with_template_for_style(target: &Rc<dyn IStyle>, template: Option<PreviewTemplate>) {
211        set_preview(style_key(target), Target::Style(target.clone()), template.map(build_template))
212    }
213
214    /// Sets a preview control for the specified style at design time.
215    pub fn set_preview_with_for_style(target: &Rc<dyn IStyle>, control: Option<Ref<Control>>) {
216        set_preview(style_key(target), Target::Style(target.clone()), control.map(control_template))
217    }
218
219    /// Gets the preview control for the specified object at design time,
220    /// if one is set.
221    pub fn get_preview_with(target: &FerroObject) -> Option<Ref<Control>> {
222        get_preview(object_key(target))
223    }
224
225    /// Gets the preview control for the specified data template at design
226    /// time, if one is set.
227    pub fn get_preview_with_for_data_template(target: &Rc<dyn IDataTemplate>) -> Option<Ref<Control>> {
228        get_preview(data_template_key(target))
229    }
230
231    /// Gets the preview control for the specified style at design time, if
232    /// one is set.
233    pub fn get_preview_with_for_style(target: &Rc<dyn IStyle>) -> Option<Ref<Control>> {
234        get_preview(style_key(target))
235    }
236
237    ferro_property!(for Design;
238        /// Defines the `DesignStyle` attached property for design-time use.
239        ///
240        /// This property allows you to apply a style to a control only at
241        /// design-time, enabling custom visualizations or highlighting in
242        /// the designer without affecting the runtime appearance.
243        pub fn design_style_property() -> AttachedProperty<Option<DesignStyleValue>> {
244            FerroProperty::register_attached::<Design, Control, _>("DesignStyle", None)
245        }
246    );
247
248    /// Sets the design-time style for a control.
249    pub fn set_design_style(control: &Control, value: Rc<dyn IStyle>) {
250        control.set_value(Self::design_style_property(), Some(DesignStyleValue(value)))
251    }
252
253    /// Gets the design-time style for a control.
254    pub fn get_design_style(control: &Control) -> Option<Rc<dyn IStyle>> {
255        control.get_value(Self::design_style_property()).map(|value| value.0)
256    }
257
258    /// Applies the design-time properties set on `source` to `target`.
259    pub fn apply_design_mode_properties(target: &Control, source: &Control) {
260        let source_object: &FerroObject = source;
261        if source.is_set(Self::width_property()) {
262            target.bind(
263                Layoutable::width_property(),
264                FerroObjectExtensions::get_observable(source_object, &**Self::width_property()),
265                BindingPriority::LocalValue,
266            );
267        }
268        if source.is_set(Self::height_property()) {
269            target.bind(
270                Layoutable::height_property(),
271                FerroObjectExtensions::get_observable(source_object, &**Self::height_property()),
272                BindingPriority::LocalValue,
273            );
274        }
275        if source.is_set(Self::data_context_property()) {
276            target.bind(
277                StyledElement::data_context_property(),
278                FerroObjectExtensions::get_observable(source_object, &**Self::data_context_property()),
279                BindingPriority::LocalValue,
280            );
281        }
282        if source.is_set(Self::design_style_property()) {
283            if let Some(style) = Self::get_design_style(source) {
284                target.styles().add(style);
285            }
286        }
287    }
288
289    /// Creates the control the previewer shows for `target`: the object
290    /// itself when it is a control, otherwise the control it is previewed
291    /// with, set up to host it, or a message that says why it cannot be
292    /// previewed.
293    #[doc(hidden)]
294    pub fn create_preview_with_control(target: &PreviewTarget) -> Option<Ref<Control>> {
295        let target = match target {
296            PreviewTarget::Style(style) => return Some(Self::create_preview_for_style(style)),
297            PreviewTarget::DataTemplate(template) => return Some(Self::create_preview_for_data_template(template)),
298            PreviewTarget::Other => return message_line("This file cannot be previewed in design view"),
299            PreviewTarget::Object(target) => target,
300        };
301
302        // The styles that are objects.
303        if let Some(style) = target.cast::<ferroui_base::styling::StyleBase>() {
304            return Some(Self::create_preview_for_style(&style.into()));
305        }
306        if let Some(styles) = target.cast::<ferroui_base::styling::Styles>() {
307            return Some(Self::create_preview_for_style(&styles.into()));
308        }
309
310        if let Some(resources) = target.cast::<ferroui_base::controls::ResourceDictionary>() {
311            if let Some(substitute) = Self::get_preview_with(target) {
312                substitute.resources().merged_dictionaries().add(resources.into());
313                return Some(substitute);
314            }
315
316            return Some(message(&[
317                "ResourceDictionaries can't be previewed without Design.PreviewWith. Add",
318                "<Design.PreviewWith>",
319                "    <Border Padding=\"20\"><!-- YOUR CONTROL FOR PREVIEW HERE --></Border>",
320                "</Design.PreviewWith>",
321                "in your resource dictionary",
322            ]));
323        }
324
325        if target.is::<crate::Application>() {
326            return message_line("This file cannot be previewed in design view");
327        }
328
329        if !target.is::<crate::Window>() {
330            if let Some(preview_with) = Self::get_preview_with(target) {
331                return Some(preview_with);
332            }
333        }
334
335        match target.cast::<Control>() {
336            Some(control) => Some(control),
337            None => message_line("This file cannot be previewed in design view"),
338        }
339    }
340
341    fn create_preview_for_style(style: &Rc<dyn IStyle>) -> Ref<Control> {
342        if let Some(substitute) = Self::get_preview_with_for_style(style) {
343            substitute.styles().add(style.clone());
344            return substitute;
345        }
346
347        message(&[
348            "Styles can't be previewed without Design.PreviewWith. Add",
349            "<Design.PreviewWith>",
350            "    <Border Padding=\"20\"><!-- YOUR CONTROL FOR PREVIEW HERE --></Border>",
351            "</Design.PreviewWith>",
352            "before setters in your first Style",
353        ])
354    }
355
356    fn create_preview_for_data_template(template: &Rc<dyn IDataTemplate>) -> Ref<Control> {
357        let substitute =
358            Self::get_preview_with_for_data_template(template).and_then(|control| control.cast::<crate::ContentControl>());
359        if let Some(substitute) = substitute {
360            substitute.set_content_template(Some(template.clone()));
361            if !substitute.is_set(Self::data_context_property())
362                && substitute.is_set(StyledElement::data_context_property())
363            {
364                let data_context = substitute.get_value(StyledElement::data_context_property());
365                substitute.set_data_context(data_context);
366            }
367            return substitute.upcast();
368        }
369
370        if let Some(data_context) = Self::get_data_context_for_data_template(template) {
371            let substitute = crate::ContentControl::new();
372            substitute.set_content_template(Some(template.clone()));
373            substitute.set_data_context(Some(data_context.clone()));
374            substitute.set_content(Some(data_context));
375            return substitute.upcast();
376        }
377
378        message(&[
379            "IDataTemplate can't be previewed without Design.PreviewWith.",
380            "Provide ContentControl with your design data as Content. Previewer will set ContentTemplate from this file.",
381            "<Design.PreviewWith>",
382            "    <ContentControl Content=\"{x:Static YOUR_DATA_OBJECT_HERE}\" />",
383            "</Design.PreviewWith>",
384        ])
385    }
386}
387
388/// What a design-time preview is created for; see
389/// [`Design::create_preview_with_control`].
390pub enum PreviewTarget {
391    /// A style that is not an object.
392    Style(Rc<dyn IStyle>),
393    /// A data template.
394    DataTemplate(Rc<dyn IDataTemplate>),
395    /// An object: a control, a style, a resource dictionary, the
396    /// application, ...
397    Object(Ref<FerroObject>),
398    /// Anything else.
399    Other,
400}
401
402/// A line of text of a preview message.
403fn message_line(text: &str) -> Option<Ref<Control>> {
404    let line = crate::TextBlock::new();
405    line.set_text(Some(text));
406    Some(line.upcast())
407}
408
409/// A preview message: its lines stacked.
410fn message(lines: &[&str]) -> Ref<Control> {
411    let panel = crate::StackPanel::new();
412    for line in lines {
413        if let Some(line) = message_line(line) {
414            panel.children().add(line);
415        }
416    }
417    panel.upcast()
418}
419
420/// The value of the `DesignStyle` property: a style, compared by identity.
421#[derive(Clone)]
422pub struct DesignStyleValue(pub Rc<dyn IStyle>);
423
424impl PartialEq for DesignStyleValue {
425    fn eq(&self, other: &Self) -> bool {
426        ferroui_base::styling::style_ptr_eq(&self.0, &other.0)
427    }
428}