Skip to main content

ferroui_browser/
ferro_view.rs

1use crate::browser_top_level_impl::BrowserTopLevelImpl;
2use crate::interop::{dom_helper, input_helper, JsObject};
3use crate::windowing_platform::BrowserWindowingPlatform;
4use ferroui_base::input::{FocusChangedEventArgs, InputElement};
5use ferroui_base::interactivity::Interactive;
6use ferroui_base::Ref;
7use ferroui_controls::embedding::EmbeddableControlRoot;
8use ferroui_controls::platform::ITopLevelImpl;
9use ferroui_controls::{Control, TopLevel};
10use std::rc::Rc;
11
12/// Shows content in an element of the web page.
13pub struct FerroView {
14    top_level: Ref<EmbeddableControlRoot>,
15}
16
17impl FerroView {
18    /// Creates a view over the element with the given id.
19    ///
20    /// `div_id` is the ID of the html element where the content should be
21    /// rendered.
22    ///
23    /// # Panics
24    /// Panics when the document has no such element.
25    pub fn new(div_id: &str) -> Rc<Self> {
26        match dom_helper::get_element_by_id(div_id, &BrowserWindowingPlatform::global_this()) {
27            Some(host) => Self::with_host(host),
28            None => panic!("Element with id '{div_id}' was not found in the html document."),
29        }
30    }
31
32    /// Creates a view over an element.
33    ///
34    /// `host` is the object holding a div element where the content should
35    /// be rendered.
36    ///
37    /// # Panics
38    /// Panics when `host` is null or undefined.
39    pub fn with_host(host: JsObject) -> Rc<Self> {
40        if host.is_null() || host.is_undefined() {
41            panic!("Value cannot be null. (Parameter 'host')");
42        }
43
44        let host_content = match dom_helper::create_ferro_host(&host) {
45            Some(host_content) => host_content,
46            None => panic!("The host of the view wasn't initialized."),
47        };
48
49        let native_controls_container = match host_content.native_host() {
50            Some(native_host) => native_host,
51            None => panic!("NativeHost cannot be null"),
52        };
53        let input_element = match host_content.input_element() {
54            Some(input_element) => input_element,
55            None => panic!("InputElement cannot be null"),
56        };
57
58        let browser_top_level = BrowserTopLevelImpl::new(host.clone(), native_controls_container, input_element);
59        let surface = browser_top_level.surface();
60        let top_level_impl: Rc<dyn ITopLevelImpl> = browser_top_level;
61        let top_level = EmbeddableControlRoot::with_impl(top_level_impl);
62
63        top_level.prepare();
64        top_level.add_handler(InputElement::got_focus_event(), {
65            let host = host.clone();
66            move |_: &Interactive, _: &FocusChangedEventArgs| input_helper::focus_element(&host)
67        });
68        top_level.renderer().start(); // TODO: use Start+StopRenderer() instead.
69        // Differs from upstream, which closes the splash screen at the first
70        // animation frame of the top-level, on the thread that draws the
71        // view. That animation frame comes before the first frame is drawn,
72        // and with a render thread it is not the thread of the page that
73        // draws: the first frame of the worker may be seconds away, during
74        // which the page would show an empty canvas that input cannot hit.
75        // The splash screen is closed when the first frame has been drawn
76        // to the canvas of the view, whichever thread drew it.
77        if let Some(surface) = surface {
78            surface.shared().on_first_frame(Box::new(move || {
79                // Try to get local splash-screen of the specific host.
80                // If couldn't find - get global one by ID for compatibility.
81                let splash = dom_helper::get_elements_by_class_name("ferroui-splash", &host).or_else(|| {
82                    dom_helper::get_element_by_id("ferroui-splash", &BrowserWindowingPlatform::global_this())
83                });
84                if let Some(splash) = splash {
85                    dom_helper::add_css_class(&splash, "splash-close");
86                }
87            }));
88        }
89
90        Rc::new(Self { top_level })
91    }
92
93    /// The content of the view.
94    ///
95    /// # Panics
96    /// Panics when the content of the top-level was set to something that
97    /// is not a control.
98    pub fn content(&self) -> Option<Ref<Control>> {
99        let content = self.top_level.content()?;
100        match Control::from_boxed(&content) {
101            Some(control) => Some(control),
102            None => panic!("Unable to cast the content of the view to a control."),
103        }
104    }
105
106    /// Sets the content of the view.
107    pub fn set_content(&self, value: Option<Ref<Control>>) {
108        self.top_level.set_content(value.map(Control::boxed));
109    }
110
111    pub(crate) fn top_level(&self) -> Ref<TopLevel> {
112        self.top_level.clone().upcast()
113    }
114
115    /// Closes the view: its top-level is disposed, which releases the
116    /// surface of the canvas (on the thread that renders, the render target
117    /// of the canvas and what drew to it) and stops the renderer of the
118    /// view. The elements of the view stay in the page. The view cannot be
119    /// used afterwards.
120    ///
121    /// Not from upstream, whose view has no way to be closed (the top-level
122    /// under it is disposable there as here). A page that shows and removes
123    /// views would otherwise keep the canvas and the graphics context of
124    /// every view it ever had.
125    pub fn dispose(&self) {
126        self.top_level.dispose();
127    }
128}