docs/development/custom-renderer.md
Note for AI coding assistants (agents): When to load this document: Working on
internal/renderers/, adding rendering backends, fixing drawing bugs, or implementing custom graphics output. For general build commands and project structure, see/AGENTS.md.
This document covers how to implement a custom renderer for Slint. This is intended for developers extending Slint's rendering capabilities or debugging existing renderers.
Slint includes three built-in renderers:
internal/renderers/software/) - Pure Rust CPU-based renderinginternal/renderers/femtovg/) - OpenGL ES 2.0 via FemtoVG libraryinternal/renderers/skia/) - GPU-accelerated via Skia libraryinternal/core/renderer.rs)The fundamental trait all renderers must implement. Uses the sealed trait pattern—RendererSealed is internal, while Renderer is the public re-export that external code uses.
Key methods:
| Method | Purpose |
|---|---|
text_size() | Measure text dimensions with optional wrapping |
font_metrics() | Query font ascent, descent, line height |
text_input_byte_offset_for_position() | Hit-testing for text input cursor placement |
text_input_cursor_rect_for_byte_offset() | Get cursor rectangle for a byte offset |
set_window_adapter() / window_adapter() | Associate renderer with a window |
free_graphics_resources() | Cleanup when components are destroyed |
mark_dirty_region() | Manual dirty region marking for partial rendering |
register_font_from_memory() / register_font_from_path() | Custom font registration |
set_rendering_notifier() | Lifecycle callbacks (BeforeRendering, AfterRendering, etc.) |
resize() | Handle window resize events |
take_snapshot() | Capture rendered frame to pixel buffer |
internal/core/item_rendering.rs)The drawing interface for all UI elements. Each renderer provides its own implementation.
Drawing methods:
draw_rectangle() - Solid/gradient rectanglesdraw_border_rectangle() - Rectangles with borders and border-radiusdraw_image() - Images with fit, alignment, tiling optionsdraw_text() - Text with colors, alignment, wrappingdraw_text_input() - Text input fields with selection/cursordraw_path() - Custom vector pathsdraw_box_shadow() - Shadow effectsClipping and transformations:
combine_clip() - Set clip region (supports rounded corners)get_current_clip() - Query current clip boundstranslate() / rotation() / scale() - 2D transformationsapply_opacity() - Alpha blendingState management:
save_state() / restore_state() - State stack for nested renderingfilter_item() - Early-out clipping testscale_factor() - DPI scaling factorFemtoVG abstracts over graphics APIs using generics:
pub struct FemtoVGRenderer<B: GraphicsBackend> { ... }
pub trait GraphicsBackend {
type Renderer: femtovg::Renderer + TextureImporter;
type WindowSurface: WindowSurface<Self::Renderer>;
fn new_suspended() -> Self;
fn begin_surface_rendering(&self) -> Result<Self::WindowSurface, ...>;
fn submit_commands(&self, commands: ...);
fn present_surface(&self, surface: Self::WindowSurface) -> Result<(), ...>;
fn resize(&self, width: NonZeroU32, height: NonZeroU32) -> Result<(), ...>;
}
Skia uses trait objects for dynamic surface selection:
pub trait Surface {
fn new(
shared_context: &SkiaSharedContext,
window_handle: Arc<dyn HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Self, PlatformError>;
fn name(&self) -> &'static str;
fn render(&self, window: &Window, size: PhysicalWindowSize,
render_callback: &dyn Fn(&Canvas, ...), ...) -> Result<(), ...>;
fn resize_event(&self, size: PhysicalWindowSize) -> Result<(), ...>;
fn use_partial_rendering(&self) -> bool { false }
}
Available surface implementations: OpenGLSurface, MetalSurface, VulkanSurface, D3DSurface, SoftwareSurface
The software renderer builds a scene graph then rasterizes:
pub struct SoftwareRenderer { ... }
impl SoftwareRenderer {
pub fn render(&self, buffer: &mut [impl TargetPixel], pixel_stride: usize);
pub fn render_by_line(&self, line_callback: impl FnMut(&mut [impl TargetPixel]));
}
Supports memory-constrained devices via line-by-line rendering.
internal/backends/winit/)For winit-based applications, renderers implement:
pub trait WinitCompatibleRenderer: std::any::Any {
fn render(&self, window: &Window) -> Result<(), PlatformError>;
fn as_core_renderer(&self) -> &dyn Renderer;
fn suspend(&self) -> Result<(), PlatformError>;
fn resume(&self, event_loop: &ActiveEventLoop,
attrs: WindowAttributes) -> Result<Arc<winit::window::Window>, ...>;
}
| Type | Location | Purpose |
|---|---|---|
ItemCache<T> | internal/core/ | Per-item graphics caching with automatic invalidation |
DirtyRegion | internal/core/ | Partial rendering dirty tracking |
RenderingNotifier | internal/core/ | Lifecycle event callbacks |
CachedRenderingData | internal/core/ | Per-item cached rendering state |
BorderRadius | internal/core/ | Rounded corner support |
Brush | internal/core/ | Color and gradient fills |
SharedPixelBuffer | internal/core/ | Pixel buffer for snapshots |
To implement a custom renderer:
RendererSealed - Text measurement, font handling, window associationItemRenderer - Drawing all UI element typesWindowAdapter - Register renderer and handle window eventsRenderingNotifier - For BeforeRendering/AfterRendering hooksItemCacheRenderers are enabled via Cargo features in api/rs/slint/Cargo.toml:
renderer-femtovg = ["i-slint-backend-selector/renderer-femtovg"]
renderer-skia = ["i-slint-backend-selector/renderer-skia"]
renderer-software = ["i-slint-backend-selector/renderer-software"]
The selector (internal/backends/selector/lib.rs) chooses renderer at runtime:
SLINT_BACKEND environment variable (e.g., winit-skia, winit-femtovg)To add a new renderer:
internal/backends/selector/Cargo.tomltry_create_renderer() in internal/backends/selector/lib.rsinternal/backends/winit/)SLINT_BACKEND=winit-software cargo run # Force software renderer
SLINT_BACKEND=winit-skia cargo run # Force Skia renderer
Renderers integrate with the platform through WindowAdapter:
Platform (winit/qt/linuxkms)
└── WindowAdapter
├── window() -> Window (Slint window abstraction)
└── renderer() -> &dyn Renderer
└── render() called by event loop on redraw
Render lifecycle:
WindowAdapter::renderer().render()ItemRenderer methodsKey integration points:
internal/backends/winit/winitwindowadapter.rs - Winit integrationinternal/core/window.rs - Platform-agnostic window logicinternal/core/api.rs - Public Window API# Run screenshot comparison tests
cargo test -p test-driver-screenshots
# Generate new reference screenshots (run when intentionally changing rendering)
SLINT_CREATE_SCREENSHOTS=1 cargo test -p test-driver-screenshots
Use the headless testing backend for automated tests:
SLINT_BACKEND=testing cargo test
The testing backend (internal/backends/testing/) provides:
# Run gallery to visually inspect rendering
cargo run -p gallery
# View specific .slint file with hot reload
cargo run --bin slint-viewer -- path/to/file.slint
internal/renderers/
├── femtovg/
│ ├── lib.rs # FemtoVGRenderer, GraphicsBackend trait
│ ├── itemrenderer.rs # GLItemRenderer (ItemRenderer impl)
│ ├── opengl.rs # OpenGL backend
│ └── wgpu.rs # WebGPU backend
├── skia/
│ ├── lib.rs # SkiaRenderer, Surface trait
│ ├── itemrenderer.rs # SkiaItemRenderer (ItemRenderer impl)
│ ├── opengl_surface.rs
│ ├── metal_surface.rs
│ ├── vulkan_surface.rs
│ └── software_surface.rs
└── software/
├── lib.rs # SoftwareRenderer, scene building
├── scene.rs # Scene graph structures
└── draw_functions.rs
The software renderer is the simplest to study as it has no external dependencies:
internal/renderers/software/lib.rsItemRenderer: builds a scene graph from draw callsrender() method rasterizes the scene to a pixel bufferFor GPU rendering patterns, study internal/renderers/skia/itemrenderer.rs which shows: