ferroui_designer_support/
design_window_loader.rs1use 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
18struct DesignerMode(Rc<dyn IDisposable>);
20
21impl Drop for DesignerMode {
22 fn drop(&mut self) {
23 self.0.dispose();
24 }
25}
26
27pub struct DesignWindowLoader;
29
30impl DesignWindowLoader {
31 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 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 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 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 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
132fn get_file_name_without_extension(path: &str) -> &str {
134 Path::new(path).file_stem().and_then(|name| name.to_str()).unwrap_or("")
135}
136
137fn parse_bool(text: &str) -> bool {
139 text.trim().eq_ignore_ascii_case("true")
140}
141
142fn 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 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 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 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 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 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 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 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 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}