Skip to main content

ferroui_browser/
browser_app_builder.rs

1use crate::browser_runtime_platform::BrowserRuntimePlatformServices;
2use crate::browser_single_view_lifetime::BrowserSingleViewLifetime;
3use crate::ferro_view::FerroView;
4use crate::interop::dom_helper;
5use crate::windowing_platform::BrowserWindowingPlatform;
6use ferroui_base::{FerroLocator, LocatorExtensions};
7use ferroui_controls::application_lifetimes::IApplicationLifetime;
8use ferroui_controls::AppBuilder;
9use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
10use ferroui_skia::SkiaApplicationExtensions;
11use std::rc::Rc;
12
13/// How a view is rendered.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15#[repr(i32)]
16pub enum BrowserRenderingMode {
17    /// Rendered in memory and copied to a 2D canvas.
18    Software2D = 1,
19    /// Rendered with WebGL 1.
20    WebGL1,
21    /// Rendered with WebGL 2.
22    WebGL2,
23}
24
25impl BrowserRenderingMode {
26    /// The mode with the given name, ignoring case.
27    pub fn parse(name: &str) -> Option<BrowserRenderingMode> {
28        [BrowserRenderingMode::Software2D, BrowserRenderingMode::WebGL1, BrowserRenderingMode::WebGL2]
29            .into_iter()
30            .find(|mode| mode.name().eq_ignore_ascii_case(name))
31    }
32
33    /// The name of the mode.
34    pub fn name(self) -> &'static str {
35        match self {
36            BrowserRenderingMode::Software2D => "Software2D",
37            BrowserRenderingMode::WebGL1 => "WebGL1",
38            BrowserRenderingMode::WebGL2 => "WebGL2",
39        }
40    }
41}
42
43/// The render backend a module draws with.
44///
45/// Not in the original, which has one backend. The rendering modes
46/// ([`BrowserRenderingMode`]) keep their meaning with either: they say what
47/// the canvas of a view is (the canvas of a WebGL context, a 2D canvas),
48/// and the backend draws to it with what it has for that kind of canvas.
49///
50/// | mode | Skia | Vello |
51/// |---|---|---|
52/// | `WebGL2` | Ganesh | the hybrid mode (`vello_gpu` over the WebGL2 context) |
53/// | `WebGL1` | Ganesh | not drawn: the mode is taken out of the list |
54/// | `Software2D` | Skia raster | the CPU mode (`vello_cpu`) |
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
56pub enum BrowserRenderer {
57    /// The Skia backend.
58    #[default]
59    Skia,
60    /// The Vello backend. The module has to be built with the feature
61    /// `vello` of this crate.
62    Vello,
63}
64
65impl BrowserRenderer {
66    /// The backend with the given name, ignoring case.
67    pub fn parse(name: &str) -> Option<BrowserRenderer> {
68        [BrowserRenderer::Skia, BrowserRenderer::Vello].into_iter().find(|renderer| renderer.name().eq_ignore_ascii_case(name))
69    }
70
71    /// The name of the backend.
72    pub fn name(self) -> &'static str {
73        match self {
74            BrowserRenderer::Skia => "Skia",
75            BrowserRenderer::Vello => "Vello",
76        }
77    }
78
79    /// Whether the module was built with the backend.
80    pub fn is_available(self) -> bool {
81        match self {
82            BrowserRenderer::Skia => true,
83            BrowserRenderer::Vello => cfg!(feature = "vello"),
84        }
85    }
86
87    /// The rendering modes of `modes` the backend draws, in their order.
88    /// The Vello backend has no renderer for WebGL 1; a list that asks for
89    /// nothing else is drawn in memory.
90    pub fn rendering_modes(self, modes: &[BrowserRenderingMode]) -> Vec<BrowserRenderingMode> {
91        match self {
92            BrowserRenderer::Skia => modes.to_vec(),
93            BrowserRenderer::Vello => {
94                let drawn: Vec<BrowserRenderingMode> =
95                    modes.iter().copied().filter(|mode| *mode != BrowserRenderingMode::WebGL1).collect();
96                match drawn.is_empty() && !modes.is_empty() {
97                    true => vec![BrowserRenderingMode::Software2D],
98                    false => drawn,
99                }
100            }
101        }
102    }
103}
104
105/// Options of the browser backend.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct BrowserPlatformOptions {
108    /// The rendering modes with fallbacks. The first element has the
109    /// highest priority.
110    pub rendering_mode: Vec<BrowserRenderingMode>,
111
112    /// The render backend. Skia by default.
113    ///
114    /// Not in the original. The Vello backend needs a module built with the
115    /// feature `vello` of this crate; the application fails to start
116    /// without it, with a message that names the feature.
117    pub renderer: BrowserRenderer,
118
119    /// Defines if the service worker used by FerroUI should be registered.
120    /// If registered, service worker can work as a save file picker fallback
121    /// on the browsers that don't support native implementation. For more
122    /// details, see
123    /// https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
124    ///
125    /// Unstable: this property might not work reliably.
126    pub register_ferro_service_worker: bool,
127
128    /// If [`register_ferro_service_worker`](Self::register_ferro_service_worker)
129    /// is enabled, it is possible to redefine scope for the worker. By
130    /// default the scope is the directory of the worker script, which is the
131    /// root of the site (the documentation of the original says the domain
132    /// root, which differs when the site is served from a subpath).
133    pub ferro_service_worker_scope: Option<String>,
134
135    /// The file dialogs use the `native-file-system-adapter` polyfill. If
136    /// the native implementation is available, by default it is used. This
137    /// property forces the polyfill to be always used. For more details, see
138    /// https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
139    pub prefer_file_dialog_polyfill: bool,
140
141    /// Whether a module built with threads renders on a render thread: the
142    /// compositor and the render backend then run in a worker that owns the
143    /// canvases of the views, and the thread of the page keeps the user
144    /// interface. `true` by default. With `false` such a module renders on
145    /// the thread of the page, as a module built without threads always
146    /// does; the option has no effect there.
147    ///
148    /// Not in the original, where a build with threads always renders on
149    /// its render worker.
150    pub render_thread: bool,
151}
152
153impl Default for BrowserPlatformOptions {
154    fn default() -> Self {
155        Self {
156            rendering_mode: vec![
157                BrowserRenderingMode::WebGL2,
158                BrowserRenderingMode::WebGL1,
159                BrowserRenderingMode::Software2D,
160            ],
161            renderer: BrowserRenderer::Skia,
162            register_ferro_service_worker: false,
163            ferro_service_worker_scope: None,
164            prefer_file_dialog_polyfill: false,
165            render_thread: true,
166        }
167    }
168}
169
170/// Starts an application in a web page.
171pub trait BrowserAppBuilder {
172    /// Configures browser backend and creates a single view lifetime from
173    /// the passed `main_div_id` parameter.
174    ///
175    /// `main_div_id` is the ID of the html element where the content should
176    /// be rendered. The function returns once the application is set up;
177    /// the page drives it from then on.
178    fn start_browser_app(&self, main_div_id: &str, options: Option<BrowserPlatformOptions>);
179
180    /// Configures browser backend.
181    ///
182    /// This method doesn't create any views to be rendered. To do so create
183    /// a [`FerroView`] object. Alternatively, you can call
184    /// [`start_browser_app`](Self::start_browser_app) instead.
185    fn setup_browser_app(&self, options: Option<BrowserPlatformOptions>);
186
187    /// Selects the subsystems of the browser: its runtime platform and
188    /// windowing subsystem, Skia for rendering and HarfBuzz for text
189    /// shaping.
190    fn use_browser(&self) -> AppBuilder;
191}
192
193fn pre_setup_browser(builder: &AppBuilder, options: Option<BrowserPlatformOptions>) -> AppBuilder {
194    let options = match options {
195        Some(options) => Rc::new(options),
196        None => FerroLocator::current().get_service::<BrowserPlatformOptions>().unwrap_or_default(),
197    };
198
199    let renderer = options.renderer;
200    FerroLocator::current_mutable().bind_to_self(options);
201
202    BrowserWindowingPlatform::set_global_this(dom_helper::get_global_this());
203
204    let builder = if builder.windowing_subsystem_initializer().is_none() {
205        builder.use_browser()
206    } else {
207        builder.clone()
208    };
209
210    match renderer {
211        BrowserRenderer::Skia => builder,
212        BrowserRenderer::Vello => use_vello_renderer(&builder),
213    }
214}
215
216/// The builder with the Vello backend in the place of the render backend it
217/// had: the last one chosen draws.
218///
219/// The modes of the backend are the hybrid mode and then the CPU mode: a
220/// canvas with a WebGL2 context is drawn by the first, a 2D canvas and
221/// everything that ends in memory (bitmaps, layers) by the second. Options
222/// the application registered are kept.
223#[cfg(feature = "vello")]
224fn use_vello_renderer(builder: &AppBuilder) -> AppBuilder {
225    use ferroui_vello::{VelloApplicationExtensions, VelloOptions, VelloRenderingMode};
226
227    if FerroLocator::current().get_service::<VelloOptions>().is_none() {
228        FerroLocator::current_mutable().bind_to_self(Rc::new(VelloOptions {
229            rendering_modes: [Some(VelloRenderingMode::Hybrid), Some(VelloRenderingMode::Cpu), None],
230            ..VelloOptions::default()
231        }));
232    }
233
234    builder.use_vello()
235}
236
237/// # Panics
238/// Always: the module has no Vello backend.
239#[cfg(not(feature = "vello"))]
240fn use_vello_renderer(_builder: &AppBuilder) -> AppBuilder {
241    panic!(
242        "BrowserPlatformOptions::renderer asks for the Vello backend, and the module was built without the feature \
243         `vello` of ferroui-browser (scripts/build-browser.sh <application> --features vello; \
244         docs/porting/vello-backend.md, section 12)"
245    );
246}
247
248impl BrowserAppBuilder for AppBuilder {
249    fn start_browser_app(&self, main_div_id: &str, options: Option<BrowserPlatformOptions>) {
250        let builder = pre_setup_browser(self, options);
251
252        let lifetime = BrowserSingleViewLifetime::new();
253        builder.after_application_setup({
254            let lifetime = lifetime.clone();
255            let main_div_id = main_div_id.to_string();
256            move |_| {
257                lifetime.set_view(Some(FerroView::new(&main_div_id)));
258            }
259        });
260
261        let lifetime: Rc<dyn IApplicationLifetime> = lifetime;
262        builder.setup_with_lifetime(lifetime);
263    }
264
265    fn setup_browser_app(&self, options: Option<BrowserPlatformOptions>) {
266        let builder = pre_setup_browser(self, options);
267
268        let lifetime: Rc<dyn IApplicationLifetime> = BrowserSingleViewLifetime::new();
269        builder.setup_with_lifetime(lifetime);
270    }
271
272    fn use_browser(&self) -> AppBuilder {
273        BrowserRuntimePlatformServices::use_browser_runtime_platform_subsystem(self)
274            .use_windowing_subsystem(BrowserWindowingPlatform::register, "")
275            .use_skia()
276            .use_harfbuzz()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn the_modes_have_the_values_the_script_side_uses() {
286        assert_eq!(1, BrowserRenderingMode::Software2D as i32);
287        assert_eq!(2, BrowserRenderingMode::WebGL1 as i32);
288        assert_eq!(3, BrowserRenderingMode::WebGL2 as i32);
289    }
290
291    #[test]
292    fn the_default_modes_fall_back_from_webgl2_to_software() {
293        assert_eq!(
294            vec![BrowserRenderingMode::WebGL2, BrowserRenderingMode::WebGL1, BrowserRenderingMode::Software2D],
295            BrowserPlatformOptions::default().rendering_mode
296        );
297    }
298
299    #[test]
300    fn a_module_with_threads_renders_on_a_render_thread_unless_told_not_to() {
301        assert!(BrowserPlatformOptions::default().render_thread);
302    }
303
304    #[test]
305    fn skia_is_the_default_renderer_and_draws_every_mode() {
306        let options = BrowserPlatformOptions::default();
307
308        assert_eq!(BrowserRenderer::Skia, options.renderer);
309        assert!(BrowserRenderer::Skia.is_available());
310        assert_eq!(options.rendering_mode, BrowserRenderer::Skia.rendering_modes(&options.rendering_mode));
311    }
312
313    #[test]
314    fn renderers_are_parsed_by_name_ignoring_case() {
315        assert_eq!(Some(BrowserRenderer::Vello), BrowserRenderer::parse("vello"));
316        assert_eq!(Some(BrowserRenderer::Skia), BrowserRenderer::parse("SKIA"));
317        assert_eq!(None, BrowserRenderer::parse("Direct2D"));
318        assert_eq!(None, BrowserRenderer::parse(""));
319        assert_eq!(cfg!(feature = "vello"), BrowserRenderer::Vello.is_available());
320    }
321
322    #[test]
323    fn the_vello_renderer_draws_no_webgl1_canvas() {
324        use BrowserRenderingMode::{Software2D, WebGL1, WebGL2};
325
326        // The default order: the hybrid mode, then the CPU mode.
327        assert_eq!(vec![WebGL2, Software2D], BrowserRenderer::Vello.rendering_modes(&[WebGL2, WebGL1, Software2D]));
328        assert_eq!(vec![Software2D], BrowserRenderer::Vello.rendering_modes(&[Software2D]));
329        assert_eq!(vec![WebGL2], BrowserRenderer::Vello.rendering_modes(&[WebGL2]));
330        // WebGL 1 alone: in memory, which is what the script side falls
331        // back to for a list it cannot serve.
332        assert_eq!(vec![Software2D], BrowserRenderer::Vello.rendering_modes(&[WebGL1]));
333        assert!(BrowserRenderer::Vello.rendering_modes(&[]).is_empty());
334    }
335
336    #[test]
337    fn modes_are_parsed_by_name_ignoring_case() {
338        assert_eq!(Some(BrowserRenderingMode::WebGL2), BrowserRenderingMode::parse("WebGL2"));
339        assert_eq!(Some(BrowserRenderingMode::WebGL1), BrowserRenderingMode::parse("webgl1"));
340        assert_eq!(Some(BrowserRenderingMode::Software2D), BrowserRenderingMode::parse("SOFTWARE2D"));
341        assert_eq!(None, BrowserRenderingMode::parse("WebGPU"));
342        assert_eq!(None, BrowserRenderingMode::parse(""));
343    }
344}