Skip to main content

ferroui_designer_support/remote/
remote_designer_entry_point.rs

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    // The static fields of the original. They belong to the UI thread: the
29    // handler of the connection posts every message to it.
30    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/// The arguments of the previewer.
39#[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
60/// `RemoteDesignerEntryPoint.Methods`: the ways the XAML is displayed.
61pub 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/// The arguments are not the ones of the previewer: what `PrintUsage`
68/// answers in the original.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct UsageError;
71
72/// `Guid.NewGuid()`: a random identifier of version four. The random bits
73/// are the keys the standard library seeds its hash maps with.
74pub(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
85/// `Uri.Host` and `Uri.Port` of an absolute URI with an authority that
86/// names its port.
87fn host_and_port(uri: &Uri) -> Option<(String, u16)> {
88    host_and_port_or(uri, None)
89}
90
91/// `Uri.Host` and `Uri.Port` of an absolute URI with an authority;
92/// `default_port` is the port of the scheme, for a URI that names none.
93pub(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        // `[::1]:30243`
100        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
127/// The text `PrintUsage` prints.
128fn 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
155/// The previewer: the entry point a host binary calls, and the handling of
156/// the messages of the IDE.
157pub struct RemoteDesignerEntryPoint;
158
159impl RemoteDesignerEntryPoint {
160    /// `ParseCommandLineArgs`. Where the original prints the usage and
161    /// exits, this returns the error; [`main`](Self::main) prints and exits.
162    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    /// `CreateTransport`: the connection the transport of the arguments
208    /// names and, for a transport that enforces one
209    /// (`ITransportWithEnforcedMethod`), the method of the previewer.
210    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    /// `AppInitializer.ConfigureApp`: gives the builder of the application
233    /// the runtime platform and the windowing subsystem of the method, and
234    /// sets the application up without starting it.
235    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            // `GetInitializer("FerroUI.Win32")`: the original loads the
254            // assembly of the Windows platform and calls its parameterless
255            // `Win32Platform.Initialize()`, which takes the options the
256            // service locator has. The platform is linked on Windows only:
257            // elsewhere the method fails as the initializer of the original
258            // does where the platform cannot start.
259            #[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    /// `new HtmlWebSocketTransport(transport, args.HtmlMethodListenUri ??
279    /// new Uri("http://localhost:5000"))`: the connection of the `html`
280    /// method around the connection to the IDE. What the constructor throws
281    /// in the original is the error, which ends the previewer: so does the
282    /// default, whose host is a name and not an IP address, in the original
283    /// too (`IPAddress.Parse`), so that the method needs `--html-url`.
284    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    /// `Main`: the entry point of the previewer. It does not return: the
297    /// previewer runs until the IDE ends the process, and exits with the
298    /// usage when the arguments are not valid.
299    ///
300    /// Deviation (DEVIATIONS.md, Designer support): the original loads the
301    /// assembly of the application from the path in the arguments and finds
302    /// the method that builds its application builder by reflection. An
303    /// application of the port is linked into its binary, so the binary of
304    /// the application is the host of its own previewer: it calls this
305    /// function with its arguments and the function that builds its
306    /// application builder. The run-time XAML loader is registered by the
307    /// host, as the host application of the original does.
308    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    /// `s_transport = transport; transport.OnMessage += OnTransportMessage`.
341    /// Called on the UI thread, whose dispatcher the handler posts to.
342    pub(crate) fn attach_transport(transport: Arc<dyn IFerroRemoteTransportConnection>) {
343        TRANSPORT.with(|slot| *slot.borrow_mut() = Some(transport.clone()));
344        // `OnTransportMessage`: runs on the reader thread of the connection
345        // and posts the message, which is plain data, to the UI thread.
346        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        // The original lists the three messages, the ones that have not
355        // arrived as null; a window ignores a null message.
356        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    /// The part of `OnTransportMessage` that runs on the UI thread.
364    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                //Ignore
383                let _ = catch_unwind(AssertUnwindSafe(|| current_window.close()));
384            }
385            let Some(transport) = TRANSPORT.with(|slot| slot.borrow().clone()) else {
386                return;
387            };
388            // `catch (Exception e)`: an error of the document, and whatever
389            // else fails while the window is built and shown (a panic here).
390            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    /// Forgets the session. For tests, which run one after another on
437    /// threads that are reused.
438    #[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
449/// `new ExceptionDetails(e)`: the name of the class of the error, its
450/// message and, for an error of the document (an `XmlException` or a class
451/// that derives from it), its line and position.
452fn 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    // Not from upstream: the upstream project tests the entry point by
473    // starting the previewer process. The argument parser and the message
474    // handling are tested here in the process of the test.
475    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        // The session is a new identifier, different every time.
518        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        // No application, no transport.
529        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        // A second application.
533        assert_eq!(Err(UsageError), parse(&["app", "other", "--transport", "tcp-bson://127.0.0.1:1/"]));
534        // A transport that is not an absolute URI.
535        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        // An option without its value.
538        assert_eq!(Err(UsageError), parse(&["app", "--transport", "tcp-bson://127.0.0.1:1/", "--method"]));
539        // The value of an option is taken as it is, whatever it looks like.
540        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        // The port of the scheme for a URI that names none.
551        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        // The IDE is told where the page is, and the frames of a window no
566        // longer go to it.
567        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        // The default of the original names a host, which is no address,
573        // and a URI of another scheme is not one of the method.
574        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    /// A session of the previewer in the test: a unit test application with
599    /// the run-time loader, the windowing platform of the previewer over a
600    /// connection the test holds the other end of, and the handler of the
601    /// entry point on that connection.
602    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        /// A message of the IDE, with the jobs it posts to the UI thread.
621        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        // The window of the previewer has no handle of the platform.
654        assert_eq!(None, result.handle);
655
656        // The window is the embeddable window of the previewer, which asks
657        // the client to resize its viewport to the size of the window.
658        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        // The next update closes the window and shows another one. Closing
667        // disposes the implementation of the window; an offscreen
668        // implementation does not report that it closed, so the window is
669        // not told (and stays visible), as in the original.
670        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        // The unknown element is on the second line of the document.
693        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        // Text that is not XML.
698        let result = session.update_xaml("<Window");
699        assert!(result.error.is_some());
700        assert!(result.exception.is_some());
701
702        // A valid document after an invalid one is previewed.
703        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        // A window created now is given them: it takes the scaling of the
722        // client and not the size of its viewport, and asks for a viewport
723        // of its own size in pixels.
724        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}