use crate::browser_runtime_platform::BrowserRuntimePlatformServices;
use crate::browser_single_view_lifetime::BrowserSingleViewLifetime;
use crate::ferro_view::FerroView;
use crate::interop::dom_helper;
use crate::windowing_platform::BrowserWindowingPlatform;
use ferroui_base::{FerroLocator, LocatorExtensions};
use ferroui_controls::application_lifetimes::IApplicationLifetime;
use ferroui_controls::AppBuilder;
use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
use ferroui_skia::SkiaApplicationExtensions;
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum BrowserRenderingMode {
Software2D = 1,
WebGL1,
WebGL2,
}
impl BrowserRenderingMode {
pub fn parse(name: &str) -> Option<BrowserRenderingMode> {
[BrowserRenderingMode::Software2D, BrowserRenderingMode::WebGL1, BrowserRenderingMode::WebGL2]
.into_iter()
.find(|mode| mode.name().eq_ignore_ascii_case(name))
}
pub fn name(self) -> &'static str {
match self {
BrowserRenderingMode::Software2D => "Software2D",
BrowserRenderingMode::WebGL1 => "WebGL1",
BrowserRenderingMode::WebGL2 => "WebGL2",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum BrowserRenderer {
#[default]
Skia,
Vello,
}
impl BrowserRenderer {
pub fn parse(name: &str) -> Option<BrowserRenderer> {
[BrowserRenderer::Skia, BrowserRenderer::Vello].into_iter().find(|renderer| renderer.name().eq_ignore_ascii_case(name))
}
pub fn name(self) -> &'static str {
match self {
BrowserRenderer::Skia => "Skia",
BrowserRenderer::Vello => "Vello",
}
}
pub fn is_available(self) -> bool {
match self {
BrowserRenderer::Skia => true,
BrowserRenderer::Vello => cfg!(feature = "vello"),
}
}
pub fn rendering_modes(self, modes: &[BrowserRenderingMode]) -> Vec<BrowserRenderingMode> {
match self {
BrowserRenderer::Skia => modes.to_vec(),
BrowserRenderer::Vello => {
let drawn: Vec<BrowserRenderingMode> =
modes.iter().copied().filter(|mode| *mode != BrowserRenderingMode::WebGL1).collect();
match drawn.is_empty() && !modes.is_empty() {
true => vec![BrowserRenderingMode::Software2D],
false => drawn,
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BrowserPlatformOptions {
pub rendering_mode: Vec<BrowserRenderingMode>,
pub renderer: BrowserRenderer,
pub register_ferro_service_worker: bool,
pub ferro_service_worker_scope: Option<String>,
pub prefer_file_dialog_polyfill: bool,
pub render_thread: bool,
}
impl Default for BrowserPlatformOptions {
fn default() -> Self {
Self {
rendering_mode: vec![
BrowserRenderingMode::WebGL2,
BrowserRenderingMode::WebGL1,
BrowserRenderingMode::Software2D,
],
renderer: BrowserRenderer::Skia,
register_ferro_service_worker: false,
ferro_service_worker_scope: None,
prefer_file_dialog_polyfill: false,
render_thread: true,
}
}
}
pub trait BrowserAppBuilder {
fn start_browser_app(&self, main_div_id: &str, options: Option<BrowserPlatformOptions>);
fn setup_browser_app(&self, options: Option<BrowserPlatformOptions>);
fn use_browser(&self) -> AppBuilder;
}
fn pre_setup_browser(builder: &AppBuilder, options: Option<BrowserPlatformOptions>) -> AppBuilder {
let options = match options {
Some(options) => Rc::new(options),
None => FerroLocator::current().get_service::<BrowserPlatformOptions>().unwrap_or_default(),
};
let renderer = options.renderer;
FerroLocator::current_mutable().bind_to_self(options);
BrowserWindowingPlatform::set_global_this(dom_helper::get_global_this());
let builder = if builder.windowing_subsystem_initializer().is_none() {
builder.use_browser()
} else {
builder.clone()
};
match renderer {
BrowserRenderer::Skia => builder,
BrowserRenderer::Vello => use_vello_renderer(&builder),
}
}
#[cfg(feature = "vello")]
fn use_vello_renderer(builder: &AppBuilder) -> AppBuilder {
use ferroui_vello::{VelloApplicationExtensions, VelloOptions, VelloRenderingMode};
if FerroLocator::current().get_service::<VelloOptions>().is_none() {
FerroLocator::current_mutable().bind_to_self(Rc::new(VelloOptions {
rendering_modes: [Some(VelloRenderingMode::Hybrid), Some(VelloRenderingMode::Cpu), None],
..VelloOptions::default()
}));
}
builder.use_vello()
}
#[cfg(not(feature = "vello"))]
fn use_vello_renderer(_builder: &AppBuilder) -> AppBuilder {
panic!(
"BrowserPlatformOptions::renderer asks for the Vello backend, and the module was built without the feature \
`vello` of ferroui-browser (scripts/build-browser.sh <application> --features vello; \
docs/porting/vello-backend.md, section 12)"
);
}
impl BrowserAppBuilder for AppBuilder {
fn start_browser_app(&self, main_div_id: &str, options: Option<BrowserPlatformOptions>) {
let builder = pre_setup_browser(self, options);
let lifetime = BrowserSingleViewLifetime::new();
builder.after_application_setup({
let lifetime = lifetime.clone();
let main_div_id = main_div_id.to_string();
move |_| {
lifetime.set_view(Some(FerroView::new(&main_div_id)));
}
});
let lifetime: Rc<dyn IApplicationLifetime> = lifetime;
builder.setup_with_lifetime(lifetime);
}
fn setup_browser_app(&self, options: Option<BrowserPlatformOptions>) {
let builder = pre_setup_browser(self, options);
let lifetime: Rc<dyn IApplicationLifetime> = BrowserSingleViewLifetime::new();
builder.setup_with_lifetime(lifetime);
}
fn use_browser(&self) -> AppBuilder {
BrowserRuntimePlatformServices::use_browser_runtime_platform_subsystem(self)
.use_windowing_subsystem(BrowserWindowingPlatform::register, "")
.use_skia()
.use_harfbuzz()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_modes_have_the_values_the_script_side_uses() {
assert_eq!(1, BrowserRenderingMode::Software2D as i32);
assert_eq!(2, BrowserRenderingMode::WebGL1 as i32);
assert_eq!(3, BrowserRenderingMode::WebGL2 as i32);
}
#[test]
fn the_default_modes_fall_back_from_webgl2_to_software() {
assert_eq!(
vec![BrowserRenderingMode::WebGL2, BrowserRenderingMode::WebGL1, BrowserRenderingMode::Software2D],
BrowserPlatformOptions::default().rendering_mode
);
}
#[test]
fn a_module_with_threads_renders_on_a_render_thread_unless_told_not_to() {
assert!(BrowserPlatformOptions::default().render_thread);
}
#[test]
fn skia_is_the_default_renderer_and_draws_every_mode() {
let options = BrowserPlatformOptions::default();
assert_eq!(BrowserRenderer::Skia, options.renderer);
assert!(BrowserRenderer::Skia.is_available());
assert_eq!(options.rendering_mode, BrowserRenderer::Skia.rendering_modes(&options.rendering_mode));
}
#[test]
fn renderers_are_parsed_by_name_ignoring_case() {
assert_eq!(Some(BrowserRenderer::Vello), BrowserRenderer::parse("vello"));
assert_eq!(Some(BrowserRenderer::Skia), BrowserRenderer::parse("SKIA"));
assert_eq!(None, BrowserRenderer::parse("Direct2D"));
assert_eq!(None, BrowserRenderer::parse(""));
assert_eq!(cfg!(feature = "vello"), BrowserRenderer::Vello.is_available());
}
#[test]
fn the_vello_renderer_draws_no_webgl1_canvas() {
use BrowserRenderingMode::{Software2D, WebGL1, WebGL2};
assert_eq!(vec![WebGL2, Software2D], BrowserRenderer::Vello.rendering_modes(&[WebGL2, WebGL1, Software2D]));
assert_eq!(vec![Software2D], BrowserRenderer::Vello.rendering_modes(&[Software2D]));
assert_eq!(vec![WebGL2], BrowserRenderer::Vello.rendering_modes(&[WebGL2]));
assert_eq!(vec![Software2D], BrowserRenderer::Vello.rendering_modes(&[WebGL1]));
assert!(BrowserRenderer::Vello.rendering_modes(&[]).is_empty());
}
#[test]
fn modes_are_parsed_by_name_ignoring_case() {
assert_eq!(Some(BrowserRenderingMode::WebGL2), BrowserRenderingMode::parse("WebGL2"));
assert_eq!(Some(BrowserRenderingMode::WebGL1), BrowserRenderingMode::parse("webgl1"));
assert_eq!(Some(BrowserRenderingMode::Software2D), BrowserRenderingMode::parse("SOFTWARE2D"));
assert_eq!(None, BrowserRenderingMode::parse("WebGPU"));
assert_eq!(None, BrowserRenderingMode::parse(""));
}
}