1use crate::design_window_loader::DesignWindowLoader;
2use crate::remote::file_watcher_transport::{FileWatcherTransport, ITransportWithEnforcedMethod};
3use crate::remote::html_transport::HtmlWebSocketTransport;
4use crate::remote::previewer_windowing_platform::PreviewerWindowingPlatform;
5use ferroui_base::threading::{CancellationToken, Dispatcher, DispatcherPriority};
6use ferroui_base::utilities::{Uri, UriKind};
7use ferroui_base::Ref;
8use ferroui_controls::{AppBuilder, Design, Window};
9use ferroui_markup_xaml::XamlLoadException;
10use ferroui_remote_protocol::designer::{
11 ExceptionDetails, StartDesignerSessionMessage, UpdateXamlMessage, UpdateXamlResultMessage,
12};
13use ferroui_remote_protocol::viewport::{
14 ClientRenderInfoMessage, ClientSupportedPixelFormatsMessage, ClientViewportAllocatedMessage,
15};
16use ferroui_remote_protocol::{
17 exception_handler, message_handler, BsonTcpTransport, Guid, IFerroRemoteTransportConnection, Message,
18 TcpTransportBase,
19};
20use std::cell::{Cell, RefCell};
21use std::hash::{BuildHasher, Hasher};
22use std::net::IpAddr;
23use std::panic::{catch_unwind, AssertUnwindSafe};
24use std::sync::Arc;
25use xamlx::XamlError;
26
27ferroui_base::ferro_thread_local! {
28 static SUPPORTED_PIXEL_FORMATS: RefCell<Option<Message>> = const { RefCell::new(None) };
31 static VIEWPORT_ALLOCATED_MESSAGE: RefCell<Option<Message>> = const { RefCell::new(None) };
32 static RENDER_INFO_MESSAGE: RefCell<Option<Message>> = const { RefCell::new(None) };
33 static LAST_RENDER_SCALING: Cell<f64> = const { Cell::new(1.0) };
34 static TRANSPORT: RefCell<Option<Arc<dyn IFerroRemoteTransportConnection>>> = const { RefCell::new(None) };
35 static CURRENT_WINDOW: RefCell<Option<Ref<Window>>> = const { RefCell::new(None) };
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub struct CommandLineArgs {
41 pub app_path: Option<String>,
42 pub transport: Option<Uri>,
43 pub html_method_listen_uri: Option<Uri>,
44 pub method: String,
45 pub session_id: String,
46}
47
48impl CommandLineArgs {
49 fn new() -> CommandLineArgs {
50 CommandLineArgs {
51 app_path: None,
52 transport: None,
53 html_method_listen_uri: None,
54 method: methods::FERRO_REMOTE.to_string(),
55 session_id: new_guid().to_string(),
56 }
57 }
58}
59
60pub mod methods {
62 pub const FERRO_REMOTE: &str = "ferroui-remote";
63 pub const WIN32: &str = "win32";
64 pub const HTML: &str = "html";
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct UsageError;
71
72pub(crate) fn new_guid() -> Guid {
75 let mut bytes = [0u8; 16];
76 for chunk in bytes.chunks_mut(8) {
77 let random = std::collections::hash_map::RandomState::new().build_hasher().finish();
78 chunk.copy_from_slice(&random.to_le_bytes());
79 }
80 bytes[7] = (bytes[7] & 0x0F) | 0x40;
81 bytes[8] = (bytes[8] & 0x3F) | 0x80;
82 Guid::from_byte_array(&bytes).expect("sixteen bytes are an identifier")
83}
84
85fn host_and_port(uri: &Uri) -> Option<(String, u16)> {
88 host_and_port_or(uri, None)
89}
90
91pub(crate) fn host_and_port_or(uri: &Uri, default_port: Option<u16>) -> Option<(String, u16)> {
94 let text = uri.absolute_uri();
95 let rest = &text[text.find("://")? + 3..];
96 let authority = &rest[..rest.find(['/', '?', '#']).unwrap_or(rest.len())];
97 let authority = authority.rsplit('@').next()?;
98 let (host, port) = match authority.strip_prefix('[') {
99 Some(bracketed) => {
101 let (host, port) = bracketed.split_once(']')?;
102 (host, port.strip_prefix(':'))
103 }
104 None => match authority.rsplit_once(':') {
105 Some((host, port)) => (host, Some(port)),
106 None => (authority, None),
107 },
108 };
109 let port = match port {
110 Some(port) => port.parse().ok()?,
111 None => default_port?,
112 };
113 Some((host.to_string(), port))
114}
115
116fn die(error: Option<&str>) -> ! {
117 if let Some(error) = error {
118 eprintln!("{error}");
119 }
120 std::process::exit(1)
121}
122
123fn log(message: &str) {
124 println!("{message}");
125}
126
127fn usage() -> String {
129 [
130 "Usage: --transport transport_spec --session-id sid --method method app",
131 "",
132 "--transport: transport used for communication with the IDE",
133 " 'tcp-bson' (e. g. 'tcp-bson://127.0.0.1:30243/') - TCP-based transport with BSON serialization of messages defined in FerroUI.Remote.Protocol",
134 " 'file' (e. g. 'file://C://my/file.xaml' - pseudo-transport that triggers XAML updates on file changes, useful as a standalone previewer tool, always uses http preview method",
135 "",
136 "--session-id: session id to be sent to IDE process",
137 "",
138 "--method: the way the XAML is displayed",
139 " 'ferroui-remote' - binary image is sent via transport connection in FrameMessage",
140 " 'win32' - XAML is displayed in win32 window (handle could be obtained from UpdateXamlResultMessage), IDE is responsible to use user32!SetParent",
141 " 'html' - Previewer starts an HTML server and displays XAML previewer as a web page",
142 "",
143 "--html-url - endpoint for HTML method to listen on, e. g. http://127.0.0.1:8081",
144 "",
145 "Example: --transport tcp-bson://127.0.0.1:30243/ --session-id 123 --method ferroui-remote MyApp.exe",
146 ]
147 .join("\n")
148}
149
150fn print_usage() -> ! {
151 eprintln!("{}", usage());
152 die(None)
153}
154
155pub struct RemoteDesignerEntryPoint;
158
159impl RemoteDesignerEntryPoint {
160 pub fn parse_command_line_args(args: &[String]) -> Result<CommandLineArgs, UsageError> {
163 #[derive(Clone, Copy)]
164 enum Next {
165 Transport,
166 Method,
167 HtmlUrl,
168 SessionId,
169 }
170
171 let mut rv = CommandLineArgs::new();
172 let mut next: Option<Next> = None;
173 for arg in args {
174 if let Some(option) = next.take() {
175 match option {
176 Next::Transport => rv.transport = Some(Uri::new(arg, UriKind::Absolute).map_err(|_| UsageError)?),
177 Next::Method => rv.method = arg.clone(),
178 Next::HtmlUrl => {
179 rv.html_method_listen_uri = Some(Uri::new(arg, UriKind::Absolute).map_err(|_| UsageError)?)
180 }
181 Next::SessionId => rv.session_id = arg.clone(),
182 }
183 } else if arg == "--transport" {
184 next = Some(Next::Transport);
185 } else if arg == "--method" {
186 next = Some(Next::Method);
187 } else if arg == "--html-url" {
188 next = Some(Next::HtmlUrl);
189 } else if arg == "--session-id" {
190 next = Some(Next::SessionId);
191 } else if rv.app_path.is_none() {
192 rv.app_path = Some(arg.clone());
193 } else {
194 return Err(UsageError);
195 }
196 }
197 if rv.app_path.is_none() || rv.transport.is_none() {
198 return Err(UsageError);
199 }
200
201 if next.is_some() {
202 return Err(UsageError);
203 }
204 Ok(rv)
205 }
206
207 fn create_transport(
211 args: &CommandLineArgs,
212 ) -> Result<(Arc<dyn IFerroRemoteTransportConnection>, Option<&'static str>), String> {
213 let Some(transport) = &args.transport else {
214 print_usage()
215 };
216 if transport.scheme() == "tcp-bson" {
217 let (host, port) =
218 host_and_port(transport).ok_or_else(|| format!("Invalid URI: no host and port in '{}'", transport.original_string()))?;
219 let address: IpAddr = host.parse().map_err(|_| "An invalid IP address was specified.".to_string())?;
220 let connection = BsonTcpTransport::empty().connect(address, port).map_err(|e| e.to_string())?;
221 return Ok((connection, None));
222 }
223
224 if transport.scheme() == "file" {
225 let connection = FileWatcherTransport::new(transport, args.app_path.clone());
226 let method = connection.previewer_method();
227 return Ok((connection, Some(method)));
228 }
229 print_usage()
230 }
231
232 fn configure_app(
236 transport: Arc<dyn IFerroRemoteTransportConnection>,
237 args: &CommandLineArgs,
238 builder: &AppBuilder,
239 ) -> Result<Arc<dyn IFerroRemoteTransportConnection>, String> {
240 let mut transport = transport;
241 let builder = builder.use_standard_runtime_platform_subsystem();
242 if args.method == methods::FERRO_REMOTE {
243 let transport = transport.clone();
244 drop(builder.use_windowing_subsystem(move || PreviewerWindowingPlatform::initialize(transport.clone()), ""));
245 }
246 if args.method == methods::HTML {
247 transport = Self::html_transport(transport, args.html_method_listen_uri.as_ref())?;
248 let transport = transport.clone();
249 drop(builder.use_windowing_subsystem(move || PreviewerWindowingPlatform::initialize(transport.clone()), ""));
250 }
251
252 if args.method == methods::WIN32 {
253 #[cfg(windows)]
260 {
261 use ferroui_base::LocatorExtensions;
262 drop(builder.use_windowing_subsystem(
263 || {
264 let options = ferroui_base::FerroLocator::current()
265 .get_service::<ferroui_win32::Win32PlatformOptions>();
266 ferroui_win32::Win32Platform::initialize(options.as_deref().cloned().unwrap_or_default());
267 },
268 "Win32",
269 ));
270 }
271 #[cfg(not(windows))]
272 return Err("The 'win32' method is not available: the Win32 platform runs on Windows only.".to_string());
273 }
274 drop(builder.setup_without_starting());
275 Ok(transport)
276 }
277
278 fn html_transport(
285 transport: Arc<dyn IFerroRemoteTransportConnection>,
286 html_method_listen_uri: Option<&Uri>,
287 ) -> Result<Arc<dyn IFerroRemoteTransportConnection>, String> {
288 let listen_uri = match html_method_listen_uri {
289 Some(listen_uri) => listen_uri.clone(),
290 None => Uri::new("http://localhost:5000", UriKind::Absolute).map_err(|_| "Invalid URI".to_string())?,
291 };
292 let transport = HtmlWebSocketTransport::new(transport, &listen_uri).map_err(|e| e.to_string())?;
293 Ok(transport)
294 }
295
296 pub fn main(cmdline: &[String], build_app: impl FnOnce() -> AppBuilder) -> ! {
309 let mut args = match Self::parse_command_line_args(cmdline) {
310 Ok(args) => args,
311 Err(UsageError) => print_usage(),
312 };
313 let (transport, enforced_method) = match Self::create_transport(&args) {
314 Ok(transport) => transport,
315 Err(error) => die(Some(&error)),
316 };
317 if let Some(enforced_method) = enforced_method {
318 args.method = enforced_method.to_string();
319 }
320 log("Initializing application in design mode");
321 Design::set_is_design_mode(true);
322 log("Obtaining AppBuilder instance from the application");
323 let app_builder = build_app();
324 let transport = match Self::configure_app(transport, &args, &app_builder) {
325 Ok(transport) => transport,
326 Err(error) => die(Some(&error)),
327 };
328 Self::attach_transport(transport.clone());
329 transport.on_exception(exception_handler(|_, e| {
330 die(Some(&e.to_string()));
331 }));
332 transport.start();
333 log("Sending StartDesignerSessionMessage");
334 transport.send(Arc::new(StartDesignerSessionMessage { session_id: Some(args.session_id.clone()) }));
335
336 Dispatcher::ui_thread().main_loop(&CancellationToken::none());
337 std::process::exit(0)
338 }
339
340 pub(crate) fn attach_transport(transport: Arc<dyn IFerroRemoteTransportConnection>) {
343 TRANSPORT.with(|slot| *slot.borrow_mut() = Some(transport.clone()));
344 let dispatcher = Dispatcher::ui_thread();
347 transport.on_message(message_handler(move |_, obj| {
348 let arg = obj.clone();
349 dispatcher.post(move || Self::on_transport_message(&arg), DispatcherPriority::DEFAULT);
350 }));
351 }
352
353 fn rebuild_pre_flight() {
354 let messages = [&SUPPORTED_PIXEL_FORMATS, &VIEWPORT_ALLOCATED_MESSAGE, &RENDER_INFO_MESSAGE]
357 .into_iter()
358 .filter_map(|slot| slot.with(|slot| slot.borrow().clone()))
359 .collect();
360 PreviewerWindowingPlatform::set_pre_flight_messages(messages);
361 }
362
363 fn on_transport_message(arg: &Message) {
365 if arg.is::<ClientSupportedPixelFormatsMessage>() {
366 SUPPORTED_PIXEL_FORMATS.with(|slot| *slot.borrow_mut() = Some(arg.clone()));
367 Self::rebuild_pre_flight();
368 }
369 if arg.is::<ClientRenderInfoMessage>() {
370 RENDER_INFO_MESSAGE.with(|slot| *slot.borrow_mut() = Some(arg.clone()));
371 Self::rebuild_pre_flight();
372 }
373 if arg.is::<ClientViewportAllocatedMessage>() {
374 VIEWPORT_ALLOCATED_MESSAGE.with(|slot| *slot.borrow_mut() = Some(arg.clone()));
375 Self::rebuild_pre_flight();
376 }
377 if let Some(xaml) = arg.downcast_ref::<UpdateXamlMessage>() {
378 let current_window = CURRENT_WINDOW.with(|slot| slot.borrow_mut().take());
379 if let Some(current_window) = current_window {
380 LAST_RENDER_SCALING.set(current_window.render_scaling());
381
382 let _ = catch_unwind(AssertUnwindSafe(|| current_window.close()));
384 }
385 let Some(transport) = TRANSPORT.with(|slot| slot.borrow().clone()) else {
386 return;
387 };
388 let loaded = catch_unwind(AssertUnwindSafe(|| {
391 DesignWindowLoader::load_designer_window_with_scaling(
392 xaml.xaml.as_deref().unwrap_or(""),
393 xaml.assembly_path.as_deref(),
394 xaml.xaml_file_project_path.as_deref(),
395 LAST_RENDER_SCALING.get(),
396 )
397 }));
398 let result = match loaded {
399 Ok(Ok(window)) => {
400 let handle = window.platform_impl().and_then(|platform_impl| platform_impl.handle());
401 CURRENT_WINDOW.with(|slot| *slot.borrow_mut() = Some(window));
402 UpdateXamlResultMessage {
403 handle: handle.map(|handle| handle.handle().to_string()),
404 ..UpdateXamlResultMessage::default()
405 }
406 }
407 Ok(Err(e)) => UpdateXamlResultMessage {
408 error: Some(e.to_string()),
409 exception: Some(exception_details(&e)),
410 ..UpdateXamlResultMessage::default()
411 },
412 Err(payload) => {
413 let message = if let Some(text) = payload.downcast_ref::<&str>() {
414 (*text).to_string()
415 } else if let Some(text) = payload.downcast_ref::<String>() {
416 text.clone()
417 } else {
418 "The document could not be previewed.".to_string()
419 };
420 UpdateXamlResultMessage {
421 error: Some(message.clone()),
422 exception: Some(ExceptionDetails {
423 exception_type: Some("Panic".to_string()),
424 message: Some(message),
425 line_number: None,
426 line_position: None,
427 }),
428 ..UpdateXamlResultMessage::default()
429 }
430 }
431 };
432 transport.send(Arc::new(result));
433 }
434 }
435
436 #[cfg(test)]
439 pub(crate) fn reset_for_unit_tests() {
440 SUPPORTED_PIXEL_FORMATS.with(|slot| *slot.borrow_mut() = None);
441 VIEWPORT_ALLOCATED_MESSAGE.with(|slot| *slot.borrow_mut() = None);
442 RENDER_INFO_MESSAGE.with(|slot| *slot.borrow_mut() = None);
443 LAST_RENDER_SCALING.set(1.0);
444 TRANSPORT.with(|slot| *slot.borrow_mut() = None);
445 CURRENT_WINDOW.with(|slot| *slot.borrow_mut() = None);
446 }
447}
448
449fn exception_details(e: &XamlLoadException) -> ExceptionDetails {
453 if let Some(inner) = e.inner_exception().and_then(|inner| inner.downcast_ref::<XamlError>()) {
454 let line_info = inner.is_xml_exception();
455 return ExceptionDetails {
456 exception_type: Some(inner.type_name().to_string()),
457 message: Some(inner.message()),
458 line_number: inner.line_number().filter(|_| line_info),
459 line_position: inner.line_position().filter(|_| line_info),
460 };
461 }
462 ExceptionDetails {
463 exception_type: Some("XamlLoadException".to_string()),
464 message: Some(e.message().to_string()),
465 line_number: None,
466 line_position: None,
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
476 use crate::remote::test_connection::TestConnection;
477 use ferroui_controls::testing::{TestServices, UnitTestApplication, UnitTestApplicationScope};
478 use ferroui_markup_xaml_loader::FerroRuntimeXamlLoader;
479 use ferroui_remote_protocol::viewport::{PixelFormat, RequestViewportResizeMessage};
480 use std::rc::Rc;
481
482 fn args(values: &[&str]) -> Vec<String> {
483 values.iter().map(|value| value.to_string()).collect()
484 }
485
486 #[test]
487 fn the_arguments_of_the_ide_are_parsed() {
488 let parsed = RemoteDesignerEntryPoint::parse_command_line_args(&args(&[
489 "--transport",
490 "tcp-bson://127.0.0.1:30243/",
491 "--session-id",
492 "123",
493 "--method",
494 "win32",
495 "--html-url",
496 "http://127.0.0.1:8081",
497 "MyApp.exe",
498 ]))
499 .unwrap();
500 assert_eq!(Some("MyApp.exe".to_string()), parsed.app_path);
501 assert_eq!("tcp-bson", parsed.transport.as_ref().unwrap().scheme());
502 assert_eq!(Some(("127.0.0.1".to_string(), 30243)), host_and_port(parsed.transport.as_ref().unwrap()));
503 assert_eq!("123", parsed.session_id);
504 assert_eq!("win32", parsed.method);
505 assert_eq!(
506 Some(("127.0.0.1".to_string(), 8081)),
507 host_and_port(parsed.html_method_listen_uri.as_ref().unwrap())
508 );
509 }
510
511 #[test]
512 fn the_method_and_the_session_have_defaults() {
513 let parsed =
514 RemoteDesignerEntryPoint::parse_command_line_args(&args(&["app", "--transport", "file:///tmp/a.xaml"])).unwrap();
515 assert_eq!(methods::FERRO_REMOTE, parsed.method);
516 assert!(parsed.html_method_listen_uri.is_none());
517 let session = Guid::parse(&parsed.session_id).expect("an identifier");
519 let other =
520 RemoteDesignerEntryPoint::parse_command_line_args(&args(&["app", "--transport", "file:///tmp/a.xaml"])).unwrap();
521 assert_ne!(session.to_string(), other.session_id);
522 assert_eq!("file", parsed.transport.unwrap().scheme());
523 }
524
525 #[test]
526 fn arguments_that_are_not_those_of_the_previewer_are_a_usage_error() {
527 let parse = |values: &[&str]| RemoteDesignerEntryPoint::parse_command_line_args(&args(values));
528 assert_eq!(Err(UsageError), parse(&[]));
530 assert_eq!(Err(UsageError), parse(&["--transport", "tcp-bson://127.0.0.1:1/"]));
531 assert_eq!(Err(UsageError), parse(&["app"]));
532 assert_eq!(Err(UsageError), parse(&["app", "other", "--transport", "tcp-bson://127.0.0.1:1/"]));
534 assert_eq!(Err(UsageError), parse(&["app", "--transport", "not a uri"]));
536 assert_eq!(Err(UsageError), parse(&["app", "--transport", "tcp-bson://127.0.0.1:1/", "--html-url", "x"]));
537 assert_eq!(Err(UsageError), parse(&["app", "--transport", "tcp-bson://127.0.0.1:1/", "--method"]));
539 let parsed = parse(&["app", "--transport", "tcp-bson://127.0.0.1:1/", "--session-id", "--method"]).unwrap();
541 assert_eq!("--method", parsed.session_id);
542 }
543
544 #[test]
545 fn the_host_and_the_port_of_a_transport_are_read_from_its_uri() {
546 let uri = |text: &str| Uri::new(text, UriKind::Absolute).unwrap();
547 assert_eq!(Some(("localhost".to_string(), 5000)), host_and_port(&uri("http://localhost:5000")));
548 assert_eq!(Some(("::1".to_string(), 30243)), host_and_port(&uri("tcp-bson://[::1]:30243/")));
549 assert_eq!(None, host_and_port(&uri("tcp-bson://127.0.0.1/")));
550 assert_eq!(Some(("127.0.0.1".to_string(), 80)), host_and_port_or(&uri("http://127.0.0.1"), Some(80)));
552 assert_eq!(Some(("::1".to_string(), 80)), host_and_port_or(&uri("http://[::1]/"), Some(80)));
553 assert_eq!(Some(("127.0.0.1".to_string(), 8081)), host_and_port_or(&uri("http://127.0.0.1:8081"), Some(80)));
554 }
555
556 #[test]
557 fn the_html_method_wraps_the_connection_in_the_html_transport() {
558 use ferroui_remote_protocol::viewport::FrameMessage;
559 use ferroui_remote_protocol::HtmlTransportStartedMessage;
560
561 let uri = |text: &str| Uri::new(text, UriKind::Absolute).unwrap();
562 let connection = TestConnection::new();
563 let transport = RemoteDesignerEntryPoint::html_transport(connection.clone(), Some(&uri("http://127.0.0.1:0")))
564 .expect("the loopback interface can be listened on");
565 assert_eq!(1, connection.sent_of::<HtmlTransportStartedMessage>().len());
568 transport.send(Arc::new(FrameMessage::default()));
569 assert!(connection.sent_of::<FrameMessage>().is_empty());
570 transport.dispose();
571
572 let connection = TestConnection::new();
575 assert_eq!(
576 Some("An invalid IP address was specified.".to_string()),
577 RemoteDesignerEntryPoint::html_transport(connection.clone(), None).err()
578 );
579 assert!(RemoteDesignerEntryPoint::html_transport(connection.clone(), Some(&uri("https://127.0.0.1:1"))).is_err());
580 assert!(connection.sent().is_empty());
581 }
582
583 #[test]
584 fn the_usage_names_the_options() {
585 let usage = usage();
586 for option in ["--transport", "--session-id", "--method", "--html-url", "tcp-bson", "ferroui-remote"] {
587 assert!(usage.contains(option), "{option} is not in the usage");
588 }
589 }
590
591 #[test]
592 fn a_new_identifier_is_of_version_four() {
593 let text = new_guid().to_string();
594 assert_eq!(36, text.len());
595 assert_eq!(Some('4'), text.chars().nth(14));
596 }
597
598 struct Session {
603 connection: Arc<TestConnection>,
604 app: UnitTestApplicationScope,
605 }
606
607 impl Session {
608 fn start() -> Session {
609 ferroui_markup_xaml::register_types();
610 let app = UnitTestApplication::start(TestServices::styled_window());
611 FerroRuntimeXamlLoader::register();
612 RemoteDesignerEntryPoint::reset_for_unit_tests();
613 PreviewerWindowingPlatform::reset_for_unit_tests();
614 let connection = TestConnection::new();
615 PreviewerWindowingPlatform::initialize(connection.clone());
616 RemoteDesignerEntryPoint::attach_transport(connection.clone());
617 Session { connection, app }
618 }
619
620 fn receive(&self, message: Message) {
622 self.connection.raise_message(message);
623 Dispatcher::ui_thread().run_jobs(None);
624 }
625
626 fn update_xaml(&self, xaml: &str) -> UpdateXamlResultMessage {
627 let before = self.connection.sent_of::<UpdateXamlResultMessage>().len();
628 self.receive(Arc::new(UpdateXamlMessage { xaml: Some(xaml.to_string()), ..UpdateXamlMessage::default() }));
629 let results = self.connection.sent_of::<UpdateXamlResultMessage>();
630 assert_eq!(before + 1, results.len(), "an update is answered with one result");
631 results.last().unwrap().clone()
632 }
633
634 fn end(self) {
635 let window = CURRENT_WINDOW.with(|slot| slot.borrow_mut().take());
636 if let Some(window) = window {
637 window.close();
638 }
639 RemoteDesignerEntryPoint::reset_for_unit_tests();
640 PreviewerWindowingPlatform::reset_for_unit_tests();
641 self.app.dispose();
642 }
643 }
644
645 const XMLNS: &str = "xmlns='https://github.com/ferroui' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'";
646
647 #[test]
648 fn an_update_of_the_xaml_is_answered_with_a_result_and_the_window_of_the_document() {
649 let session = Session::start();
650 let result = session.update_xaml(&format!("<Window {XMLNS} Width='120' Height='80'><Border/></Window>"));
651 assert_eq!(None, result.error);
652 assert_eq!(None, result.exception);
653 assert_eq!(None, result.handle);
655
656 let window = CURRENT_WINDOW.with(|slot| slot.borrow().clone()).expect("the window of the document");
659 let platform_impl = PreviewerWindowingPlatform::last_window().expect("the window of the previewer");
660 assert!(window.platform_impl().is_some_and(|window_impl| {
661 std::ptr::addr_eq(Rc::as_ptr(&window_impl), Rc::as_ptr(&platform_impl))
662 }));
663 let resizes = session.connection.sent_of::<RequestViewportResizeMessage>();
664 assert_eq!(Some(&RequestViewportResizeMessage { width: 120.0, height: 80.0 }), resizes.last());
665
666 assert!(!platform_impl.base().is_disposed());
671 let result = session.update_xaml(&format!("<UserControl {XMLNS}><Border Width='30' Height='20'/></UserControl>"));
672 assert_eq!(None, result.error);
673 assert!(platform_impl.base().is_disposed());
674 let next = CURRENT_WINDOW.with(|slot| slot.borrow().clone()).unwrap();
675 assert!(next != window);
676 let next_impl = PreviewerWindowingPlatform::last_window().unwrap();
677 assert!(!Rc::ptr_eq(&next_impl, &platform_impl));
678 assert!(!next_impl.base().is_disposed());
679 session.end();
680 }
681
682 #[test]
683 fn an_update_with_invalid_xaml_is_answered_with_the_details_of_the_error() {
684 let session = Session::start();
685 let result = session.update_xaml(&format!("<Window {XMLNS}>\n <NoSuchControl/>\n</Window>"));
686 let error = result.error.expect("the error of the document");
687 assert!(!error.is_empty());
688 assert_eq!(None, result.handle);
689 let details = result.exception.expect("the details of the error");
690 assert!(details.exception_type.is_some_and(|name| !name.is_empty()));
691 assert!(details.message.is_some_and(|message| !message.is_empty()));
692 assert_eq!(Some(2), details.line_number);
694 assert!(details.line_position.is_some());
695 assert!(CURRENT_WINDOW.with(|slot| slot.borrow().is_none()));
696
697 let result = session.update_xaml("<Window");
699 assert!(result.error.is_some());
700 assert!(result.exception.is_some());
701
702 let result = session.update_xaml(&format!("<Border {XMLNS}/>"));
704 assert_eq!(None, result.error);
705 session.end();
706 }
707
708 #[test]
709 fn the_messages_of_the_client_are_kept_for_the_windows_that_follow() {
710 let session = Session::start();
711 assert!(PreviewerWindowingPlatform::pre_flight_messages().is_empty());
712 session.receive(Arc::new(ClientSupportedPixelFormatsMessage { formats: Some(vec![PixelFormat::Rgba8888]) }));
713 session.receive(Arc::new(ClientViewportAllocatedMessage { width: 1024.0, height: 768.0, dpi_x: 192.0, dpi_y: 192.0 }));
714 let messages = PreviewerWindowingPlatform::pre_flight_messages();
715 assert_eq!(2, messages.len());
716 assert!(messages[0].is::<ClientSupportedPixelFormatsMessage>());
717 assert!(messages[1].is::<ClientViewportAllocatedMessage>());
718 session.receive(Arc::new(ClientRenderInfoMessage { dpi_x: 192.0, dpi_y: 192.0 }));
719 assert_eq!(3, PreviewerWindowingPlatform::pre_flight_messages().len());
720
721 let result = session.update_xaml(&format!("<Window {XMLNS} Width='100' Height='50'><Border/></Window>"));
725 assert_eq!(None, result.error);
726 Dispatcher::ui_thread().run_jobs(None);
727 let platform_impl = PreviewerWindowingPlatform::last_window().unwrap();
728 assert_eq!(2.0, platform_impl.base().render_scaling());
729 assert_eq!(ferroui_base::Size::new(100.0, 50.0), platform_impl.base().client_size());
730 let resizes = session.connection.sent_of::<RequestViewportResizeMessage>();
731 assert!(resizes.contains(&RequestViewportResizeMessage { width: 200.0, height: 100.0 })
732 || resizes.contains(&RequestViewportResizeMessage { width: 100.0, height: 50.0 }));
733 session.end();
734 }
735}