// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use std::{cell::RefCell, num::NonZeroU32, rc::Rc}; use i_slint_core::api::PlatformError; use crate::{FemtoVGRenderer, GraphicsBackend, WindowSurface}; /// This trait describes the interface GPU accelerated renderers in Slint require to render with OpenGL. /// /// It serves the purpose to ensure that the OpenGL context is current before running any OpenGL /// commands, as well as providing access to the OpenGL implementation by function pointers. /// /// # Safety /// /// This trait is unsafe because an implementation of get_proc_address could return dangling /// pointers. In practice an implementation of this trait should just forward to the EGL/WGL/CGL /// C library that implements EGL/CGL/WGL. #[allow(unsafe_code)] pub unsafe trait OpenGLInterface { /// Ensures that the OpenGL context is current when returning from this function. fn ensure_current(&self) -> Result<(), Box>; /// This function is called by the renderers when all OpenGL commands have been issued and /// the back buffer is reading for on-screen presentation. Typically implementations forward /// this to platform specific APIs such as eglSwapBuffers. fn swap_buffers(&self) -> Result<(), Box>; /// This function is called by the renderers when the surface needs to be resized, typically /// in response to the windowing system notifying of a change in the window system. /// For most implementations this is a no-op, with the exception for wayland for example. fn resize( &self, width: NonZeroU32, height: NonZeroU32, ) -> Result<(), Box>; /// Returns the address of the OpenGL function specified by name, or a null pointer if the /// function does not exist. fn get_proc_address(&self, name: &std::ffi::CStr) -> *const std::ffi::c_void; } #[cfg(target_arch = "wasm32")] struct WebGLNeedsNoCurrentContext; #[cfg(target_arch = "wasm32")] unsafe impl OpenGLInterface for WebGLNeedsNoCurrentContext { fn ensure_current(&self) -> Result<(), Box> { Ok(()) } fn swap_buffers(&self) -> Result<(), Box> { Ok(()) } fn resize( &self, _width: NonZeroU32, _height: NonZeroU32, ) -> Result<(), Box> { Ok(()) } fn get_proc_address(&self, _: &std::ffi::CStr) -> *const std::ffi::c_void { unreachable!() } } struct SuspendedRenderer {} unsafe impl OpenGLInterface for SuspendedRenderer { fn ensure_current(&self) -> Result<(), Box> { Err("ensure current called on suspended renderer".to_string().into()) } fn swap_buffers(&self) -> Result<(), Box> { Err("swap_buffers called on suspended renderer".to_string().into()) } fn resize( &self, _: NonZeroU32, _: NonZeroU32, ) -> Result<(), Box> { Ok(()) } fn get_proc_address(&self, _: &std::ffi::CStr) -> *const std::ffi::c_void { panic!("get_proc_address called on suspended renderer") } } pub struct OpenGLBackend { opengl_context: RefCell>, #[cfg(target_family = "wasm")] html_canvas: RefCell>, } impl OpenGLBackend { pub fn set_opengl_context( &self, renderer: &FemtoVGRenderer, #[cfg(not(target_arch = "wasm32"))] opengl_context: impl OpenGLInterface + 'static, #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement, ) -> Result<(), i_slint_core::platform::PlatformError> { #[cfg(target_arch = "wasm32")] let opengl_context = WebGLNeedsNoCurrentContext {}; let opengl_context = Box::new(opengl_context); #[cfg(not(target_arch = "wasm32"))] let gl_renderer = unsafe { femtovg::renderer::OpenGl::new_from_function_cstr(|name| { opengl_context.get_proc_address(name) }) .unwrap() }; #[cfg(target_arch = "wasm32")] let gl_renderer = match femtovg::renderer::OpenGl::new_from_html_canvas(&html_canvas) { Ok(gl_renderer) => gl_renderer, Err(_) => { use wasm_bindgen::JsCast; // I don't believe that there's a way of disabling the 2D canvas. let context_2d = html_canvas .get_context("2d") .unwrap() .unwrap() .dyn_into::() .unwrap(); context_2d.set_font("20px serif"); // We don't know if we're rendering on dark or white background, so choose a "color" in the middle for the text. context_2d.set_fill_style_str("red"); context_2d .fill_text("Slint requires WebGL to be enabled in your browser", 0., 30.) .unwrap(); panic!("Cannot proceed without WebGL - aborting") } }; let femtovg_canvas = femtovg::Canvas::new_with_text_context( gl_renderer, crate::font_cache::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()), ) .unwrap(); *self.opengl_context.borrow_mut() = opengl_context; #[cfg(target_family = "wasm")] { *self.html_canvas.borrow_mut() = Some(html_canvas); } let canvas = Rc::new(RefCell::new(femtovg_canvas)); renderer.reset_canvas(canvas); Ok(()) } } pub struct GLWindowSurface {} impl WindowSurface for GLWindowSurface { fn render_surface(&self) -> &::Surface { &() } } impl GraphicsBackend for OpenGLBackend { type Renderer = femtovg::renderer::OpenGl; type WindowSurface = GLWindowSurface; const NAME: &'static str = "OpenGL"; fn new_suspended() -> Self { Self { opengl_context: RefCell::new(Box::new(SuspendedRenderer {})), #[cfg(target_family = "wasm")] html_canvas: RefCell::new(None), } } fn clear_graphics_context(&self) { *self.opengl_context.borrow_mut() = Box::new(SuspendedRenderer {}); } /// Ensures that the OpenGL context is current when returning from this function. fn begin_surface_rendering( &self, ) -> Result> { self.opengl_context.borrow().ensure_current()?; Ok(GLWindowSurface {}) } fn submit_commands(&self, _commands: ::CommandBuffer) {} /// This function is called by the renderers when all OpenGL commands have been issued and /// the back buffer is reading for on-screen presentation. Typically implementations forward /// this to platform specific APIs such as eglSwapBuffers. fn present_surface( &self, _surface: GLWindowSurface, ) -> Result<(), Box> { self.opengl_context.borrow().swap_buffers() } #[cfg(not(target_family = "wasm"))] fn with_graphics_api( &self, callback: impl FnOnce(Option>) -> R, ) -> Result { use i_slint_core::api::GraphicsAPI; self.opengl_context.borrow().ensure_current()?; let api = GraphicsAPI::NativeOpenGL { get_proc_address: &|name| self.opengl_context.borrow().get_proc_address(name), }; Ok(callback(Some(api))) } #[cfg(target_family = "wasm")] fn with_graphics_api( &self, callback: impl FnOnce(Option>) -> R, ) -> Result { use i_slint_core::api::GraphicsAPI; let id = self.html_canvas.borrow().as_ref().map_or_else(|| String::new(), |canvas| canvas.id()); let api = GraphicsAPI::WebGL { canvas_element_id: &id, context_type: "webgl2" }; Ok(callback(Some(api))) } /// This function is called by the renderers when the surface needs to be resized, typically /// in response to the windowing system notifying of a change in the window system. /// For most implementations this is a no-op, with the exception for wayland for example. fn resize( &self, width: NonZeroU32, height: NonZeroU32, ) -> Result<(), Box> { self.opengl_context.borrow().resize(width, height) } } impl FemtoVGRenderer { /// Creates a new renderer that renders using OpenGL. An implementation of the OpenGLInterface /// trait needs to supplied. pub fn new( #[cfg(not(target_arch = "wasm32"))] opengl_context: impl OpenGLInterface + 'static, #[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement, ) -> Result { use super::FemtoVGRendererExt; let this = Self::new_suspended(); this.graphics_backend.set_opengl_context( &this, #[cfg(not(target_arch = "wasm32"))] opengl_context, #[cfg(target_arch = "wasm32")] html_canvas, )?; Ok(this) } }