FemtoVG: Abstract the rest of the FemtoVGRenderer type to be independent from OpenGL

The main visible external change is that FemtoVGRenderer is now a generic type, but in the public Slint API the type alias that uses the OpenGL backend is re-exported under the old name. This looks a little different in rustdoc
but appears to be source compatible.
This commit is contained in:
Simon Hausmann 2025-04-24 13:14:21 +02:00 committed by Simon Hausmann
parent 96e422d43d
commit 98dc4a8657
6 changed files with 355 additions and 215 deletions

View file

@ -417,8 +417,8 @@ pub mod platform {
/// It is only enabled when the `renderer-femtovg` Slint feature is enabled.
#[cfg(all(feature = "renderer-femtovg", not(target_os = "android")))]
pub mod femtovg_renderer {
pub use i_slint_renderer_femtovg::FemtoVGRenderer;
pub use i_slint_renderer_femtovg::OpenGLInterface;
pub use i_slint_renderer_femtovg::opengl::OpenGLInterface;
pub use i_slint_renderer_femtovg::FemtoVGOpenGLRenderer as FemtoVGRenderer;
}
}

View file

@ -19,7 +19,8 @@ use crate::display::{gbmdisplay::GbmDisplay, Presenter, RenderingRotation};
use crate::drmoutput::DrmOutput;
pub struct FemtoVGRendererAdapter {
renderer: i_slint_renderer_femtovg::FemtoVGRenderer,
renderer:
i_slint_renderer_femtovg::FemtoVGRenderer<i_slint_renderer_femtovg::opengl::OpenGLBackend>,
gbm_display: Rc<GbmDisplay>,
}
@ -114,7 +115,7 @@ impl GlContextWrapper {
}
}
unsafe impl i_slint_renderer_femtovg::OpenGLInterface for GlContextWrapper {
unsafe impl i_slint_renderer_femtovg::opengl::OpenGLInterface for GlContextWrapper {
fn ensure_current(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if !self.glutin_context.is_current() {
self.glutin_context.make_current(&self.glutin_surface).map_err(

View file

@ -6,7 +6,9 @@ use std::sync::Arc;
use i_slint_core::renderer::Renderer;
use i_slint_core::{graphics::RequestedGraphicsAPI, platform::PlatformError};
use i_slint_renderer_femtovg::{FemtoVGRenderer, FemtoVGRendererExt};
use i_slint_renderer_femtovg::{
opengl, FemtoVGOpenGLRendererExt, FemtoVGRenderer, FemtoVGRendererExt,
};
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowExtWebSys;
@ -17,16 +19,13 @@ use super::WinitCompatibleRenderer;
mod glcontext;
pub struct GlutinFemtoVGRenderer {
renderer: FemtoVGRenderer,
renderer: FemtoVGRenderer<opengl::OpenGLBackend>,
suspended: Cell<bool>,
}
impl GlutinFemtoVGRenderer {
pub fn new_suspended() -> Box<dyn WinitCompatibleRenderer> {
Box::new(Self {
renderer: FemtoVGRenderer::new_without_context(),
suspended: Cell::new(true),
})
Box::new(Self { renderer: FemtoVGRenderer::new_suspended(), suspended: Cell::new(true) })
}
}
@ -78,7 +77,7 @@ impl super::WinitCompatibleRenderer for GlutinFemtoVGRenderer {
}
fn suspend(&self) -> Result<(), PlatformError> {
self.renderer.clear_opengl_context()
self.renderer.clear_graphics_context()
}
fn is_suspended(&self) -> bool {

View file

@ -19,7 +19,7 @@ pub struct OpenGLContext {
winit_window: Arc<winit::window::Window>,
}
unsafe impl i_slint_renderer_femtovg::OpenGLInterface for OpenGLContext {
unsafe impl i_slint_renderer_femtovg::opengl::OpenGLInterface for OpenGLContext {
fn ensure_current(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if !self.context.is_current() {
self.context.make_current(&self.surface).map_err(|glutin_error| -> PlatformError {

View file

@ -23,6 +23,7 @@ use i_slint_core::platform::PlatformError;
use i_slint_core::renderer::RendererSealed;
use i_slint_core::window::{WindowAdapter, WindowInner};
use i_slint_core::Brush;
use images::OpenGLTextureImporter;
type PhysicalLength = euclid::Length<f32, PhysicalPx>;
type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
@ -35,25 +36,29 @@ use self::itemrenderer::CanvasRc;
mod fonts;
mod images;
mod itemrenderer;
pub mod opengl;
/// 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<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>>;
pub trait WindowSurface<R: femtovg::Renderer> {
fn render_surface(&self) -> &R::Surface;
}
pub trait GraphicsBackend {
type Renderer: femtovg::Renderer + OpenGLTextureImporter;
type WindowSurface: WindowSurface<Self::Renderer>;
fn new_suspended() -> Self;
fn clear_graphics_context(&self);
fn begin_surface_rendering(
&self,
) -> Result<Self::WindowSurface, Box<dyn std::error::Error + Send + Sync>>;
fn submit_commands(&self, commands: <Self::Renderer as femtovg::Renderer>::CommandBuffer);
fn present_surface(
&self,
surface: Self::WindowSurface,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn with_graphics_api<R>(
&self,
callback: impl FnOnce(Option<i_slint_core::api::GraphicsAPI<'_>>) -> R,
) -> Result<R, i_slint_core::platform::PlatformError>;
/// 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.
@ -62,94 +67,26 @@ pub unsafe trait OpenGLInterface {
width: NonZeroU32,
height: NonZeroU32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn swap_buffers(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn resize(
&self,
_width: NonZeroU32,
_height: NonZeroU32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
Err("ensure current called on suspended renderer".to_string().into())
}
fn swap_buffers(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("swap_buffers called on suspended renderer".to_string().into())
}
fn resize(
&self,
_: NonZeroU32,
_: NonZeroU32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn get_proc_address(&self, _: &std::ffi::CStr) -> *const std::ffi::c_void {
panic!("get_proc_address called on suspended renderer")
}
}
/// Use the FemtoVG renderer when implementing a custom Slint platform where you deliver events to
/// Slint and want the scene to be rendered using OpenGL. The rendering is done using the [FemtoVG](https://github.com/femtovg/femtovg)
/// library.
pub struct FemtoVGRenderer {
pub struct FemtoVGRenderer<B: GraphicsBackend> {
maybe_window_adapter: RefCell<Option<Weak<dyn WindowAdapter>>>,
rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
canvas: RefCell<Option<CanvasRc<femtovg::renderer::OpenGl>>>,
graphics_cache: itemrenderer::ItemGraphicsCache<femtovg::renderer::OpenGl>,
texture_cache: RefCell<images::TextureCache<femtovg::renderer::OpenGl>>,
canvas: RefCell<Option<CanvasRc<B::Renderer>>>,
graphics_cache: itemrenderer::ItemGraphicsCache<B::Renderer>,
texture_cache: RefCell<images::TextureCache<B::Renderer>>,
rendering_metrics_collector: RefCell<Option<Rc<RenderingMetricsCollector>>>,
rendering_first_time: Cell<bool>,
// Last field, so that it's dropped last and context exists and is current when destroying the FemtoVG canvas
opengl_context: RefCell<Box<dyn OpenGLInterface>>,
// Last field, so that it's dropped last and for example the OpenGL context exists and is current when destroying the FemtoVG canvas
graphics_backend: B,
#[cfg(target_arch = "wasm32")]
canvas_id: RefCell<String>,
}
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<Self, PlatformError> {
let this = Self::new_without_context();
this.set_opengl_context(
#[cfg(not(target_arch = "wasm32"))]
opengl_context,
#[cfg(target_arch = "wasm32")]
html_canvas,
)?;
Ok(this)
}
impl<B: GraphicsBackend> FemtoVGRenderer<B> {
/// Render the scene using OpenGL.
pub fn render(&self) -> Result<(), i_slint_core::platform::PlatformError> {
self.internal_render_with_post_callback(
@ -167,7 +104,7 @@ impl FemtoVGRenderer {
surface_size: i_slint_core::api::PhysicalSize,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Result<(), i_slint_core::platform::PlatformError> {
self.opengl_context.borrow().ensure_current()?;
let surface = self.graphics_backend.begin_surface_rendering()?;
if self.rendering_first_time.take() {
*self.rendering_metrics_collector.borrow_mut() =
@ -239,7 +176,8 @@ impl FemtoVGRenderer {
// the back buffer, in order to allow the callback to provide its own rendering of the background.
// femtovg's clear_rect() will merely schedule a clear call, so flush right away to make it immediate.
femtovg_canvas.flush();
let commands = femtovg_canvas.flush_to_surface(surface.render_surface());
self.graphics_backend.submit_commands(commands);
femtovg_canvas.set_size(width.get(), height.get(), scale);
drop(femtovg_canvas);
@ -298,7 +236,8 @@ impl FemtoVGRenderer {
collector.measure_frame_rendered(&mut item_renderer);
}
canvas.borrow_mut().flush();
let commands = canvas.borrow_mut().flush_to_surface(surface.render_surface());
self.graphics_backend.submit_commands(commands);
// Delete any images and layer images (and their FBOs) before making the context not current anymore, to
// avoid GPU memory leaks.
@ -312,7 +251,7 @@ impl FemtoVGRenderer {
self.with_graphics_api(|api| callback.notify(RenderingState::AfterRendering, &api))?;
}
self.opengl_context.borrow().swap_buffers()?;
self.graphics_backend.present_surface(surface)?;
Ok(())
}
@ -321,14 +260,7 @@ impl FemtoVGRenderer {
&self,
callback: impl FnOnce(i_slint_core::api::GraphicsAPI<'_>),
) -> Result<(), PlatformError> {
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),
};
callback(api);
Ok(())
self.graphics_backend.with_graphics_api(|api| callback(api.unwrap()))
}
#[cfg(target_arch = "wasm32")]
@ -351,10 +283,15 @@ impl FemtoVGRenderer {
"Renderer must be associated with component before use".to_string().into()
})
}
fn reset_canvas(&self, canvas: CanvasRc<B::Renderer>) {
*self.canvas.borrow_mut() = canvas.into();
self.rendering_first_time.set(true);
}
}
#[doc(hidden)]
impl RendererSealed for FemtoVGRenderer {
impl<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
fn text_size(
&self,
font_request: i_slint_core::graphics::FontRequest,
@ -508,62 +445,63 @@ impl RendererSealed for FemtoVGRenderer {
_items: &mut dyn Iterator<Item = Pin<i_slint_core::items::ItemRef<'_>>>,
) -> Result<(), i_slint_core::platform::PlatformError> {
if !self.graphics_cache.is_empty() {
self.opengl_context.borrow().ensure_current()?;
self.graphics_cache.component_destroyed(component);
self.graphics_backend.with_graphics_api(|_| {
self.graphics_cache.component_destroyed(component);
})?;
}
Ok(())
}
fn set_window_adapter(&self, window_adapter: &Rc<dyn WindowAdapter>) {
*self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter));
if self.opengl_context.borrow().ensure_current().is_ok() {
self.graphics_cache.clear_all();
self.texture_cache.borrow_mut().clear();
}
self.graphics_backend
.with_graphics_api(|_| {
self.graphics_cache.clear_all();
self.texture_cache.borrow_mut().clear();
})
.ok();
}
fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> {
if let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok()) {
self.opengl_context.borrow().resize(width, height)?;
self.graphics_backend.resize(width, height)?;
};
Ok(())
}
/// Returns an image buffer of what was rendered last by reading the previous front buffer (using glReadPixels).
fn take_snapshot(&self) -> Result<SharedPixelBuffer<Rgba8Pixel>, PlatformError> {
self.opengl_context.borrow().ensure_current()?;
let Some(canvas) = self.canvas.borrow().as_ref().cloned() else {
return Err("FemtoVG renderer cannot take screenshot without a window".into());
};
let screenshot = canvas
.borrow_mut()
.screenshot()
.map_err(|e| format!("FemtoVG error reading current back buffer: {e}"))?;
self.graphics_backend.with_graphics_api(|_| {
let Some(canvas) = self.canvas.borrow().as_ref().cloned() else {
return Err("FemtoVG renderer cannot take screenshot without a window".into());
};
let screenshot = canvas
.borrow_mut()
.screenshot()
.map_err(|e| format!("FemtoVG error reading current back buffer: {e}"))?;
use rgb::ComponentBytes;
Ok(SharedPixelBuffer::clone_from_slice(
screenshot.buf().as_bytes(),
screenshot.width() as u32,
screenshot.height() as u32,
))
use rgb::ComponentBytes;
Ok(SharedPixelBuffer::clone_from_slice(
screenshot.buf().as_bytes(),
screenshot.width() as u32,
screenshot.height() as u32,
))
})?
}
}
impl Drop for FemtoVGRenderer {
impl<B: GraphicsBackend> Drop for FemtoVGRenderer<B> {
fn drop(&mut self) {
self.clear_opengl_context().ok();
self.clear_graphics_context().ok();
}
}
/// The purpose of this trait is to add internal API that's accessed from the winit/linuxkms backends, but not
/// public (as the trait isn't re-exported).
#[doc(hidden)]
pub trait FemtoVGRendererExt {
fn new_without_context() -> Self;
fn set_opengl_context(
&self,
#[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>;
fn clear_opengl_context(&self) -> Result<(), i_slint_core::platform::PlatformError>;
fn new_suspended() -> Self;
fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError>;
fn render_transformed_with_post_callback(
&self,
rotation_angle_degrees: f32,
@ -573,13 +511,22 @@ pub trait FemtoVGRendererExt {
) -> Result<(), i_slint_core::platform::PlatformError>;
}
/// The purpose of this trait is to add internal API specific to the OpenGL renderer that's accessed from the winit
/// backend. In this case, the ability to resume a suspended OpenGL renderer by providing a new context.
#[doc(hidden)]
impl FemtoVGRendererExt for FemtoVGRenderer {
pub trait FemtoVGOpenGLRendererExt {
fn set_opengl_context(
&self,
#[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static,
#[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement,
) -> Result<(), i_slint_core::platform::PlatformError>;
}
#[doc(hidden)]
impl<B: GraphicsBackend> FemtoVGRendererExt for FemtoVGRenderer<B> {
/// Creates a new renderer in suspended state without OpenGL. Any attempts at rendering, etc. will produce an error,
/// until [`Self::set_opengl_context()`] was called successfully.
fn new_without_context() -> Self {
let opengl_context = Box::new(SuspendedRenderer {});
fn new_suspended() -> Self {
Self {
maybe_window_adapter: Default::default(),
rendering_notifier: Default::default(),
@ -588,15 +535,15 @@ impl FemtoVGRendererExt for FemtoVGRenderer {
texture_cache: Default::default(),
rendering_metrics_collector: Default::default(),
rendering_first_time: Cell::new(true),
opengl_context: RefCell::new(opengl_context),
graphics_backend: B::new_suspended(),
#[cfg(target_arch = "wasm32")]
canvas_id: Default::default(),
}
}
fn clear_opengl_context(&self) -> Result<(), i_slint_core::platform::PlatformError> {
fn clear_graphics_context(&self) -> Result<(), i_slint_core::platform::PlatformError> {
// Ensure the context is current before the renderer is destroyed
if self.opengl_context.borrow().ensure_current().is_ok() {
self.graphics_backend.with_graphics_api(|_| {
// If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier.
if !self.rendering_first_time.get() {
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
@ -609,7 +556,7 @@ impl FemtoVGRendererExt for FemtoVGRenderer {
self.graphics_cache.clear_all();
self.texture_cache.borrow_mut().clear();
}
})?;
if let Some(canvas) = self.canvas.borrow_mut().take() {
if Rc::strong_count(&canvas) != 1 {
@ -617,69 +564,11 @@ impl FemtoVGRendererExt for FemtoVGRenderer {
}
}
*self.opengl_context.borrow_mut() = Box::new(SuspendedRenderer {});
self.graphics_backend.clear_graphics_context();
Ok(())
}
fn set_opengl_context(
&self,
#[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::<web_sys::CanvasRenderingContext2d>()
.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")
}
};
#[cfg(target_arch = "wasm32")]
{
*self.canvas_id.borrow_mut() = html_canvas.id();
}
let femtovg_canvas = femtovg::Canvas::new_with_text_context(
gl_renderer,
self::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()),
)
.unwrap();
let canvas = Rc::new(RefCell::new(femtovg_canvas));
*self.canvas.borrow_mut() = canvas.into();
*self.opengl_context.borrow_mut() = opengl_context;
self.rendering_first_time.set(true);
Ok(())
}
fn render_transformed_with_post_callback(
&self,
rotation_angle_degrees: f32,
@ -695,3 +584,21 @@ impl FemtoVGRendererExt for FemtoVGRenderer {
)
}
}
impl FemtoVGOpenGLRendererExt for FemtoVGRenderer<opengl::OpenGLBackend> {
fn set_opengl_context(
&self,
#[cfg(not(target_arch = "wasm32"))] opengl_context: impl opengl::OpenGLInterface + 'static,
#[cfg(target_arch = "wasm32")] html_canvas: web_sys::HtmlCanvasElement,
) -> Result<(), i_slint_core::platform::PlatformError> {
self.graphics_backend.set_opengl_context(
self,
#[cfg(not(target_arch = "wasm32"))]
opengl_context,
#[cfg(target_arch = "wasm32")]
html_canvas,
)
}
}
pub type FemtoVGOpenGLRenderer = FemtoVGRenderer<opengl::OpenGLBackend>;

View file

@ -0,0 +1,233 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn swap_buffers(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
fn resize(
&self,
_width: NonZeroU32,
_height: NonZeroU32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
Err("ensure current called on suspended renderer".to_string().into())
}
fn swap_buffers(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("swap_buffers called on suspended renderer".to_string().into())
}
fn resize(
&self,
_: NonZeroU32,
_: NonZeroU32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<Box<dyn OpenGLInterface>>,
}
impl OpenGLBackend {
pub fn set_opengl_context(
&self,
renderer: &FemtoVGRenderer<Self>,
#[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::<web_sys::CanvasRenderingContext2d>()
.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::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()),
)
.unwrap();
*self.opengl_context.borrow_mut() = opengl_context;
let canvas = Rc::new(RefCell::new(femtovg_canvas));
renderer.reset_canvas(canvas);
Ok(())
}
}
pub struct GLWindowSurface {}
impl WindowSurface<femtovg::renderer::OpenGl> for GLWindowSurface {
fn render_surface(&self) -> &<femtovg::renderer::OpenGl as femtovg::Renderer>::Surface {
&()
}
}
impl GraphicsBackend for OpenGLBackend {
type Renderer = femtovg::renderer::OpenGl;
type WindowSurface = GLWindowSurface;
fn new_suspended() -> Self {
Self { opengl_context: RefCell::new(Box::new(SuspendedRenderer {})) }
}
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<GLWindowSurface, Box<dyn std::error::Error + Send + Sync>> {
self.opengl_context.borrow().ensure_current()?;
Ok(GLWindowSurface {})
}
fn submit_commands(&self, _commands: <Self::Renderer as femtovg::Renderer>::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<dyn std::error::Error + Send + Sync>> {
self.opengl_context.borrow().swap_buffers()
}
fn with_graphics_api<R>(
&self,
callback: impl FnOnce(Option<i_slint_core::api::GraphicsAPI<'_>>) -> R,
) -> Result<R, i_slint_core::platform::PlatformError> {
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)))
}
/// 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<dyn std::error::Error + Send + Sync>> {
self.opengl_context.borrow().resize(width, height)
}
}
impl FemtoVGRenderer<OpenGLBackend> {
/// 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<Self, PlatformError> {
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)
}
}