Skip to main content

ferroui_designer_support/
design_window_loader.rs

1use crate::remote::PreviewerWindowImpl;
2use ferroui_base::metadata::{from_markup_value, MarkupAssembly};
3use ferroui_base::platform::ASSET_SCHEME;
4use ferroui_base::reactive::IDisposable;
5use ferroui_base::styling::IStyle;
6use ferroui_base::utilities::{Uri, UriKind};
7use ferroui_base::{BoxedValue, FerroLocator, FerroObject, LocatorExtensions, Ref};
8use ferroui_controls::embedding::offscreen::OffscreenTopLevelImplBase;
9use ferroui_controls::platform::PlatformManager;
10use ferroui_controls::templates::IDataTemplate;
11use ferroui_controls::{Control, Design, PreviewTarget, SizeToContent, Window};
12use ferroui_markup_xaml::{
13    IRuntimeXamlLoader, RuntimeXamlLoaderConfiguration, RuntimeXamlLoaderDocument, XamlLoadException,
14};
15use std::path::Path;
16use std::rc::Rc;
17
18/// `using (PlatformManager.DesignerMode())`.
19struct DesignerMode(Rc<dyn IDisposable>);
20
21impl Drop for DesignerMode {
22    fn drop(&mut self) {
23        self.0.dispose();
24    }
25}
26
27/// Loads the text of a XAML document into the window the previewer shows.
28pub struct DesignWindowLoader;
29
30impl DesignWindowLoader {
31    /// `LoadDesignerWindow(xaml, assemblyPath, xamlFileProjectPath)`: with a
32    /// render scaling of one.
33    pub fn load_designer_window(
34        xaml: &str,
35        assembly_path: Option<&str>,
36        xaml_file_project_path: Option<&str>,
37    ) -> Result<Ref<Window>, XamlLoadException> {
38        Self::load_designer_window_with_scaling(xaml, assembly_path, xaml_file_project_path, 1.0)
39    }
40
41    /// `LoadDesignerWindow(xaml, assemblyPath, xamlFileProjectPath, renderScaling)`.
42    ///
43    /// The document is loaded by the run-time XAML loader in design mode.
44    /// What it builds is previewed as `Design.CreatePreviewWithControl`
45    /// decides: a window is the window, a control is the content of a new
46    /// window, a style, a resource dictionary or a data template is hosted
47    /// in the control `Design.PreviewWith` names, and anything else is a
48    /// message. The design-time properties of the document (`Design.Width`,
49    /// `Design.Height`, `Design.DataContext`, `Design.DesignStyle`) are
50    /// applied to the window, which is then shown.
51    ///
52    /// # Panics
53    /// Panics when no run-time XAML loader is registered.
54    pub fn load_designer_window_with_scaling(
55        xaml: &str,
56        assembly_path: Option<&str>,
57        xaml_file_project_path: Option<&str>,
58        render_scaling: f64,
59    ) -> Result<Ref<Window>, XamlLoadException> {
60        let window;
61        {
62            let _designer_mode = DesignerMode(PlatformManager::designer_mode());
63            let loader = FerroLocator::current().get_required_service::<dyn IRuntimeXamlLoader>();
64
65            let mut base_uri = None;
66            if let Some(assembly_path) = assembly_path {
67                let xaml_file_project_path = xaml_file_project_path.unwrap_or("/Fake.xaml");
68                //Fabricate fake Uri
69                let uri = format!(
70                    "{ASSET_SCHEME}://{}{xaml_file_project_path}",
71                    get_file_name_without_extension(assembly_path)
72                );
73                base_uri = Some(Uri::new(&uri, UriKind::Absolute).map_err(|e| {
74                    XamlLoadException::with_inner(format!("Invalid URI: the URI '{uri}' of the document: {e}"), e)
75                })?);
76            }
77
78            // Deviation (DEVIATIONS.md, Designer support): the original
79            // loads the assembly from the path. The application is linked
80            // into the process that previews it, so the assembly is the
81            // registered one of that name, if there is one.
82            let local_asm =
83                assembly_path.and_then(|assembly_path| MarkupAssembly::find(get_file_name_without_extension(assembly_path)));
84            let use_compiled_bindings =
85                local_asm.and_then(|local_asm| local_asm.metadata_value(MarkupAssembly::USE_COMPILED_BINDINGS_BY_DEFAULT));
86
87            let mut configuration = RuntimeXamlLoaderConfiguration::new();
88            configuration.local_assembly = local_asm;
89            configuration.design_mode = true;
90            configuration.use_compiled_bindings_by_default = use_compiled_bindings.is_some_and(parse_bool);
91            let loaded = loader.load(RuntimeXamlLoaderDocument::with_base_uri(base_uri, xaml), configuration)?;
92
93            let control = Design::create_preview_with_control(&preview_target(&loaded))
94                .expect("a preview is a control or a message that says why there is none");
95            window = match control.cast::<Window>() {
96                Some(window) => window,
97                None => {
98                    let window = Window::new();
99                    window.set_content(Some(Control::boxed(control.clone())));
100                    window
101                }
102            };
103
104            // `window.PlatformImpl is OffscreenTopLevelImplBase offscreenImpl`:
105            // the window of the previewer, or another offscreen implementation.
106            if let Some(platform_impl) = window.platform_impl() {
107                let platform_impl = platform_impl.as_any();
108                if let Some(previewer_impl) = platform_impl.downcast_ref::<PreviewerWindowImpl>() {
109                    previewer_impl.set_render_scaling(render_scaling);
110                } else if let Some(offscreen_impl) = platform_impl.downcast_ref::<OffscreenTopLevelImplBase>() {
111                    offscreen_impl.set_render_scaling(render_scaling);
112                }
113            }
114
115            Design::apply_design_mode_properties(&window, &control);
116
117            if !window.is_set(Window::size_to_content_property()) {
118                if window.width().is_nan() {
119                    window.set_size_to_content(window.size_to_content() | SizeToContent::WIDTH);
120                }
121
122                if window.height().is_nan() {
123                    window.set_size_to_content(window.size_to_content() | SizeToContent::HEIGHT);
124                }
125            }
126        }
127        window.show();
128        Ok(window)
129    }
130}
131
132/// `Path.GetFileNameWithoutExtension`.
133fn get_file_name_without_extension(path: &str) -> &str {
134    Path::new(path).file_stem().and_then(|name| name.to_str()).unwrap_or("")
135}
136
137/// `bool.TryParse(text, out var value) && value`.
138fn parse_bool(text: &str) -> bool {
139    text.trim().eq_ignore_ascii_case("true")
140}
141
142/// What `Design.CreatePreviewWithControl(object)` tests its argument for:
143/// the loaded object as a data template, as an object of the object model
144/// (a control, a style, a resource dictionary, the application) or as a
145/// style that is not one.
146fn preview_target(loaded: &BoxedValue) -> PreviewTarget {
147    let value = Some(loaded.clone());
148    if let Some(template) = from_markup_value::<Rc<dyn IDataTemplate>>(&value) {
149        return PreviewTarget::DataTemplate(template);
150    }
151    if let Some(object) = from_markup_value::<Ref<FerroObject>>(&value) {
152        return PreviewTarget::Object(object);
153    }
154    if let Some(style) = from_markup_value::<Rc<dyn IStyle>>(&value) {
155        return PreviewTarget::Style(style);
156    }
157    PreviewTarget::Other
158}
159
160#[cfg(test)]
161mod tests {
162    // Not from upstream: the upstream project tests the loader only through
163    // the previewer process. The documents are loaded by the run-time
164    // loader into windows of the windowing platform of the previewer.
165    use super::*;
166    use ferroui_base::layout::Layoutable;
167    use ferroui_base::StyledElement;
168    use ferroui_controls::testing::{TestServices, UnitTestApplication, UnitTestApplicationScope};
169    use ferroui_controls::{Border, StackPanel, TextBlock, UserControl};
170    use ferroui_markup_xaml_loader::FerroRuntimeXamlLoader;
171    use crate::remote::previewer_windowing_platform::PreviewerWindowingPlatform;
172    use crate::remote::test_connection::TestConnection;
173
174    const XMLNS: &str = "xmlns='https://github.com/ferroui' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' \
175                         xmlns:d='http://schemas.microsoft.com/expression/blend/2008'";
176
177    /// A unit test application with the run-time loader and the windowing
178    /// platform of the previewer: in the designer mode a window is the
179    /// embeddable window of the platform, which the mock platform of the
180    /// test services does not have.
181    fn start() -> UnitTestApplicationScope {
182        ferroui_markup_xaml::register_types();
183        let scope = UnitTestApplication::start(TestServices::styled_window());
184        FerroRuntimeXamlLoader::register();
185        PreviewerWindowingPlatform::reset_for_unit_tests();
186        PreviewerWindowingPlatform::initialize(TestConnection::new());
187        scope
188    }
189
190    fn load(xaml: &str) -> Ref<Window> {
191        DesignWindowLoader::load_designer_window(xaml, None, None).expect("the document loads")
192    }
193
194    fn content_of(window: &Window) -> Ref<Control> {
195        Control::from_boxed(&window.content().expect("the window has content")).expect("the content is a control")
196    }
197
198    #[test]
199    fn a_document_whose_root_is_a_window_is_the_window() {
200        let app = start();
201        let window = load(&format!("<Window {XMLNS} Title='Previewed' Width='300' Height='200'><Border Name='inner'/></Window>"));
202        assert_eq!(Some("Previewed".to_string()), window.title());
203        assert!(content_of(&window).is::<Border>());
204        assert!(window.is_visible());
205        // A window with a size does not size itself to its content.
206        assert_eq!(SizeToContent::MANUAL, window.size_to_content());
207        window.close();
208        app.dispose();
209    }
210
211    #[test]
212    fn a_window_without_a_size_sizes_itself_to_its_content() {
213        let app = start();
214        let window = load(&format!("<Window {XMLNS}><Border Width='40' Height='30'/></Window>"));
215        assert_eq!(SizeToContent::WIDTH_AND_HEIGHT, window.size_to_content());
216        window.close();
217
218        let window = load(&format!("<Window {XMLNS} Width='300'><Border Width='40' Height='30'/></Window>"));
219        assert_eq!(SizeToContent::HEIGHT, window.size_to_content());
220        window.close();
221
222        // A window that states how it sizes itself is left as it is.
223        let window = load(&format!("<Window {XMLNS} SizeToContent='Manual'><Border/></Window>"));
224        assert_eq!(SizeToContent::MANUAL, window.size_to_content());
225        window.close();
226        app.dispose();
227    }
228
229    #[test]
230    fn a_document_whose_root_is_a_control_is_the_content_of_a_window() {
231        let app = start();
232        let window = load(&format!("<UserControl {XMLNS}><TextBlock Text='Hello'/></UserControl>"));
233        let content = content_of(&window);
234        let user_control = content.cast::<UserControl>().expect("the previewed control");
235        let text = Control::from_boxed(&user_control.content().unwrap()).unwrap().cast::<TextBlock>().unwrap();
236        assert_eq!(Some("Hello".to_string()), text.text());
237        assert_eq!(SizeToContent::WIDTH_AND_HEIGHT, window.size_to_content());
238        assert!(window.is_visible());
239        window.close();
240        app.dispose();
241    }
242
243    #[test]
244    fn the_design_size_and_the_design_data_context_of_the_document_are_those_of_the_window() {
245        let app = start();
246        let window = load(&format!(
247            "<UserControl {XMLNS} d:DesignWidth='320' d:DesignHeight='240'><Border/></UserControl>"
248        ));
249        assert_eq!(320.0, window.width());
250        assert_eq!(240.0, window.height());
251        // With both sizes the window does not size itself to its content.
252        assert_eq!(SizeToContent::MANUAL, window.size_to_content());
253        window.close();
254
255        let window = load(&format!(
256            "<UserControl {XMLNS} Design.Width='200' Design.DataContext='design data'><Border/></UserControl>"
257        ));
258        assert_eq!(200.0, window.width());
259        assert!(window.height().is_nan());
260        assert_eq!(SizeToContent::HEIGHT, window.size_to_content());
261        let data_context = window.get_value(StyledElement::data_context_property()).expect("the design data context");
262        assert_eq!(Some(&"design data".to_string()), data_context.downcast_ref::<String>());
263        // The size of the document itself is not the design size.
264        assert!(content_of(&window).get_value(Layoutable::width_property()).is_nan());
265        window.close();
266        app.dispose();
267    }
268
269    #[test]
270    fn a_control_that_names_a_preview_host_is_previewed_as_itself() {
271        let app = start();
272        // `Design.SetPreviewWith(target, control)` records nothing for a
273        // target that is a visual ("not a supported scenario without
274        // templates"), so the control is previewed as any other control.
275        let window = load(&format!(
276            "<UserControl {XMLNS}><Design.PreviewWith><Border Name='host' Padding='20'/></Design.PreviewWith></UserControl>"
277        ));
278        let content = content_of(&window);
279        assert_eq!(None, content.name());
280        assert!(content.is::<UserControl>());
281        window.close();
282        app.dispose();
283    }
284
285    #[test]
286    fn a_style_is_previewed_in_the_control_it_names() {
287        let app = start();
288        let window = load(&format!(
289            "<Style {XMLNS} Selector='Border'><Design.PreviewWith><Border Name='host'/></Design.PreviewWith>\
290             <Setter Property='Width' Value='50'/></Style>"
291        ));
292        let host = content_of(&window);
293        assert_eq!(Some("host".to_string()), host.name());
294        assert_eq!(1, host.styles().count());
295        window.close();
296
297        // Without a host the preview says how to give one.
298        let window = load(&format!("<Style {XMLNS} Selector='Border'><Setter Property='Width' Value='50'/></Style>"));
299        let message = content_of(&window).cast::<StackPanel>().expect("the lines of the message");
300        let first = message.children().get(0).cast::<TextBlock>().unwrap();
301        assert_eq!(Some("Styles can't be previewed without Design.PreviewWith. Add".to_string()), first.text());
302        window.close();
303        app.dispose();
304    }
305
306    #[test]
307    fn a_resource_dictionary_is_previewed_in_the_control_it_names() {
308        let app = start();
309        let window = load(&format!(
310            "<ResourceDictionary {XMLNS}><Design.PreviewWith><Border Name='host'/></Design.PreviewWith>\
311             <SolidColorBrush x:Key='accent' Color='Red'/></ResourceDictionary>"
312        ));
313        let host = content_of(&window);
314        assert_eq!(Some("host".to_string()), host.name());
315        assert_eq!(1, host.resources().merged_dictionaries().count());
316        window.close();
317
318        let window = load(&format!("<ResourceDictionary {XMLNS}><SolidColorBrush x:Key='accent' Color='Red'/></ResourceDictionary>"));
319        let message = content_of(&window).cast::<StackPanel>().expect("the lines of the message");
320        let first = message.children().get(0).cast::<TextBlock>().unwrap();
321        assert_eq!(
322            Some("ResourceDictionaries can't be previewed without Design.PreviewWith. Add".to_string()),
323            first.text()
324        );
325        window.close();
326        app.dispose();
327    }
328
329    #[test]
330    fn a_document_that_does_not_load_is_an_error() {
331        let app = start();
332        let error = DesignWindowLoader::load_designer_window(&format!("<Window {XMLNS}><NoSuchControl/></Window>"), None, None)
333            .expect_err("the type of the element is unknown");
334        assert!(!error.message().is_empty());
335        assert!(DesignWindowLoader::load_designer_window("<Window", None, None).is_err());
336        app.dispose();
337    }
338
339    #[test]
340    fn the_name_of_the_assembly_and_the_path_of_the_document_make_the_base_uri() {
341        assert_eq!("MyApp", get_file_name_without_extension("/build/bin/MyApp.dll"));
342        assert_eq!("my-app", get_file_name_without_extension("target/debug/my-app"));
343        assert_eq!("", get_file_name_without_extension(""));
344        assert!(parse_bool(" True "));
345        assert!(!parse_bool("yes"));
346    }
347}