Introduction to Femtovg
Femtovg is a 2D graphics library written in Rust. This means you can draw shapes and stuff with it!
For a quick demo, clone the repository and run the demo example (you'll need to have Rust installed):
git clone https://github.com/femtovg/femtovg.git
cd femtovg
cargo run --example demo

You can run other examples too. Try cargo run --example text or cargo run --example breakout!
Getting Started
Let's start with a simple program – we'll create a window and paint a simple shape on it!
If you don't have one already, make a new crate named hello_femtovg:
cargo new hello_femtovg
Unless you have much big brain, it is highly recommended that you learn Rust first, then come back here.
Setting Up
Femtovg uses OpenGL to talk to the GPU. We'll need to give Femtovg an OpenGL context – an object that stores a bunch of stuff needed to draw things. Then, we can create a Canvas to draw things on!
Creating an OpenGL Context
If you're new to graphics, maybe this part will feel a bit overwhelming. Don't worry, we'll wrap all the weird code in a function and never worry about it again.
So, how do we get this OpenGL context? We'll use the winit library to create a window and the glutin library to create an OpenGL context for rendering to that window. glutin-winit connects the two, and raw-window-handle lets glutin find the window winit made:
[dependencies]
femtovg = "0.28"
winit = "0.30"
glutin = "0.32"
glutin-winit = "0.5"
raw-window-handle = "0.6"
The first thing we need to do is create an Event Loop. winit hands it an application – a type implementing ApplicationHandler – and calls it back as things happen. Windows can only be created once the application has been resumed, so that is where we'll set everything up:
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
struct App {
context: Option<PossiblyCurrentContext>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.context.is_none() {
self.context = Some(create_window(event_loop));
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: winit::window::WindowId, event: WindowEvent) {}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
let mut app = App { context: None };
event_loop.run_app(&mut app).unwrap();
}
Let's configure a window. We can specify many settings here, but let's just set the size and title:
use winit::window::Window;
use winit::dpi::PhysicalSize;
let window_attrs = Window::default_attributes()
.with_inner_size(PhysicalSize::new(1000., 600.))
.with_title("Femtovg");
Next we specify a configuration for that window. Usually windows may have many different properties. Think transparency, OpenGL support, bit depth. The following lines find one that is suitable for rendering:
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
context::PossiblyCurrentContext,
display::GetGlDisplay,
prelude::*,
};
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, template, |mut configs| configs.next().unwrap())
.unwrap();
let window = window.unwrap();
let gl_display = gl_config.display();
let raw_window_handle = window.window_handle().unwrap().as_raw();
let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
Now, we can create a surface for rendering and make our OpenGL context current on that surface:
use glutin::surface::{SurfaceAttributesBuilder, WindowSurface};
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
raw_window_handle,
NonZeroU32::new(1000).unwrap(),
NonZeroU32::new(600).unwrap(),
);
let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };
not_current_gl_context.take().unwrap().make_current(&surface).unwrap()
In order for any OpenGL commands to work, a context must be current; all OpenGL commands affect the state of whichever context is current (from OpenGL wiki)
We'll need the current context for the next step, but as promised, we can hide everything else in a function, create_window. Here's the code we have so far:
use std::num::NonZeroU32;
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::Window;
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
context::PossiblyCurrentContext,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
struct App {
context: Option<PossiblyCurrentContext>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.context.is_some() {
return;
}
self.context = Some(create_window(event_loop));
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: winit::window::WindowId, event: WindowEvent) {
if let WindowEvent::CloseRequested = event {
event_loop.exit();
}
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
let mut app = App { context: None };
event_loop.run_app(&mut app).unwrap();
}
fn create_window(event_loop: &ActiveEventLoop) -> PossiblyCurrentContext {
let window_attrs = Window::default_attributes()
.with_inner_size(winit::dpi::PhysicalSize::new(1000., 600.))
.with_title("Femtovg");
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, template, |mut configs| configs.next().unwrap())
.unwrap();
let window = window.unwrap();
let gl_display = gl_config.display();
let raw_window_handle = window.window_handle().unwrap().as_raw();
let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let raw_window_handle = window.window_handle().unwrap().as_raw();
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
raw_window_handle,
NonZeroU32::new(1000).unwrap(),
NonZeroU32::new(600).unwrap(),
);
let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };
not_current_gl_context.take().unwrap().make_current(&surface).unwrap()
}
It compiles, runs, and opens an empty window that closes when you click its close button.
Creating a Canvas
We have an OpenGL context and display now – the Femtovg renderer can use it as output for rendering things. Let's create a renderer from the display we have:
let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) }
.expect("Cannot create renderer");
The renderer is responsible for drawing things, but we can't draw on it directly – instead, we need to create a Canvas object:
let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
Finally, we have what we need to proceed to the next section – canvas has methods like fill_path and fill_text that actually draw stuff.
Rendering
Now that we have a canvas, we can start drawing things! To keep things organized, let's create a render function that will do all the rendering:
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let (context, gl_display, window, surface) = create_window(event_loop);
// [...]
let mut canvas = ..;
render(&context, &surface, &window, &mut canvas);
}
fn render<T: Renderer>(
context: &PossiblyCurrentContext,
surface: &Surface<WindowSurface>,
window: &Window,
canvas: &mut Canvas<T>,
) {}
In render, first let's make sure that the canvas has the right size – it should match the dimensions and DPI of the window:
let size = window.inner_size();
canvas.set_size(size.width, size.height, window.scale_factor() as f32);
Next, let's do some actual drawing. As an example, we'll fill a smol red rectangle:
canvas.clear_rect(30, 30, 30, 30, Color::rgbf(1., 0., 0.));
clear_rectfills a rectangle. The first 2 parameters specify its position, and the next 2 specify the dimensions of the rectangle.
Color::rgbfis one of the functions that lets you create a Color. The three parameters correspond to the Red, Green and Blue values in the range 0..1.
Even if you consider your minimalist abstract masterpiece complete, there's actually some more code we need to write. We have to call canvas.flush() to tell the renderer to execute all drawing commands. Then, we must call swap_buffers to display what we've rendered:
canvas.flush();
surface.swap_buffers(context).expect("Could not swap buffers");
The render function is finished. The canvas, context, surface and window have to outlive resumed, so the app keeps them in an AppState; the event loop keeps the window open until it's closed.
Our program now looks like this:
use std::num::NonZeroU32;
use femtovg::renderer::OpenGl;
use femtovg::{Canvas, Color};
use glutin::surface::Surface;
use glutin::{context::PossiblyCurrentContext, display::Display};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::{dpi::PhysicalSize, window::Window};
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
struct App {
state: Option<AppState>,
}
#[allow(dead_code)]
struct AppState {
context: PossiblyCurrentContext,
surface: Surface<WindowSurface>,
window: Window,
canvas: Canvas<OpenGl>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
let (context, gl_display, window, surface) = create_window(event_loop);
let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) }
.expect("Cannot create renderer");
let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
canvas.set_size(1000, 600, window.scale_factor() as f32);
render(&context, &surface, &window, &mut canvas);
self.state = Some(AppState {
context,
surface,
window,
canvas,
});
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: winit::window::WindowId, event: WindowEvent) {
if let WindowEvent::CloseRequested = event {
event_loop.exit();
}
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
let mut app = App { state: None };
event_loop.run_app(&mut app).unwrap();
}
fn create_window(event_loop: &ActiveEventLoop) -> (PossiblyCurrentContext, Display, Window, Surface<WindowSurface>) {
let window_attrs = Window::default_attributes()
.with_inner_size(PhysicalSize::new(1000., 600.))
.with_title("Femtovg");
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, template, |mut configs| configs.next().unwrap())
.unwrap();
let window = window.unwrap();
let gl_display = gl_config.display();
let raw_window_handle = window.window_handle().unwrap().as_raw();
let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let raw_window_handle = window.window_handle().unwrap().as_raw();
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
raw_window_handle,
NonZeroU32::new(1000).unwrap(),
NonZeroU32::new(600).unwrap(),
);
let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };
(
not_current_gl_context.take().unwrap().make_current(&surface).unwrap(),
gl_display,
window,
surface,
)
}
fn render(
context: &PossiblyCurrentContext,
surface: &Surface<WindowSurface>,
window: &Window,
canvas: &mut Canvas<OpenGl>,
) {
// Make sure the canvas has the right size:
let size = window.inner_size();
canvas.set_size(size.width, size.height, window.scale_factor() as f32);
// Make smol red rectangle
canvas.clear_rect(30, 30, 30, 30, Color::rgbf(1., 0., 0.));
// Tell renderer to execute all drawing commands
canvas.flush();
// Display what we've just rendered
surface.swap_buffers(context).expect("Could not swap buffers");
}
And when we run it, we see the red square we rendered:

The Event Loop
So far, our app isn't really "doing" anything – in fact, it won't even respond if we try to close it with the "x" button. To handle input from the user, we must use the event loop.
winit delivers each event to our App by calling a method of ApplicationHandler. Events that concern a window go to window_event; let's print them:
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
// [...] create the window, renderer and canvas, as before
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
println!("{:?}", event);
}
}
event_loop.run_app calls the application's methods for each new event, until the program exits.
window_event is the one we are most interested in. It receives:
- A
WindowId– but since we only have 1 window in this example, the ID will always match with our window's ID. - A
WindowEventenum, which contains information about the window event.
You can run the example to see the different types of WindowEvents being printed as you interact with the window! You might see something like:
CursorMoved { device_id: DeviceId(X(DeviceId(2))), position: PhysicalPosition { x: 931.1624145507813, y: 270.08074951171875 } }
CursorMoved { device_id: DeviceId(X(DeviceId(2))), position: PhysicalPosition { x: 981.8682861328125, y: 257.404296875 } }
CursorLeft { device_id: DeviceId(X(DeviceId(2))) }
ModifiersChanged(Modifiers { state: ModifiersState(0x0), pressed_mods: ModifiersKeyState(0x0) })
Focused(false)
Exiting on Close
Clicking on the window's close button also creates a WindowEvent. We'll capture this event to exit the application when the user requests it:
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
_ => {}
}
}
Try running it! The program should exit when you click the close button.
ActiveEventLoop::exitasks the event loop to stop;run_appthen returns.
Tracking Mouse Movement
Using the CursorMoved event, we can track the mouse position. Let's keep the latest position we know in the app state, and update it whenever we get a CursorMoved event:
struct AppState {
// [...] context, surface, window, canvas
mouse_position: PhysicalPosition<f64>,
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let Some(state) = self.state.as_mut() else {
return;
};
match event {
WindowEvent::CursorMoved { position, .. } => {
state.mouse_position = position;
}
// ...
}
}
Re-rendering
So far, our code only renders the window once – after that, there's no rendering, only event handling. But suppose we wanted the red square to follow the mouse. How can we re-render the square in the new position?
We'll need to re-render every time the mouse position changes. The correct way to do this is to request_redraw, so that the platform knows we want to draw some new stuff. Then, we'll receive a WindowEvent::RedrawRequested event, and that's when we can render a new frame:
match event {
WindowEvent::CursorMoved { position, .. } => {
state.mouse_position = position;
state.window.request_redraw();
}
WindowEvent::RedrawRequested => {
render(&state.context, &state.surface, &state.window, &mut state.canvas, state.mouse_position);
}
// ...
}
Finally, we should update our render function to take the mouse position into account – we can just use those coordinates for the square position:
fn render<T: Renderer>(
context: &PossiblyCurrentContext,
surface: &Surface<WindowSurface>,
window: &Window,
canvas: &mut Canvas<T>,
square_position: PhysicalPosition<f64>,
) {
//...
canvas.clear_rect(
square_position.x as u32,
square_position.y as u32,
30,
30,
Color::rgbf(1., 0., 0.),
);
//...
}
Our code runs! There's just one small problem...

What happened? Since there's no code to "erase" the old square, all the squares pile up on each other, creating a red mess. To fix this, we should clear the entire window before rendering anything new.
canvas.clear_rect(0, 0, size.width, size.height, Color::black());
Now, if you run the code, the red square will happily follow the cursor wherever it goes.

To recap, here's the code we've written:
use std::num::NonZeroU32;
use femtovg::renderer::OpenGl;
use femtovg::{Canvas, Color};
use glutin::surface::Surface;
use glutin::{context::PossiblyCurrentContext, display::Display};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalPosition;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::{dpi::PhysicalSize, window::Window};
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
struct App {
state: Option<AppState>,
}
struct AppState {
context: PossiblyCurrentContext,
surface: Surface<WindowSurface>,
window: Window,
canvas: Canvas<OpenGl>,
mouse_position: PhysicalPosition<f64>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
let (context, gl_display, window, surface) = create_window(event_loop);
let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) }
.expect("Cannot create renderer");
let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
canvas.set_size(1000, 600, window.scale_factor() as f32);
self.state = Some(AppState {
context,
surface,
window,
canvas,
mouse_position: PhysicalPosition::new(0., 0.),
});
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: winit::window::WindowId, event: WindowEvent) {
let Some(state) = self.state.as_mut() else {
return;
};
match event {
WindowEvent::CursorMoved { position, .. } => {
state.mouse_position = position;
state.window.request_redraw();
}
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
render(
&state.context,
&state.surface,
&state.window,
&mut state.canvas,
state.mouse_position,
);
}
_ => {}
}
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
let mut app = App { state: None };
event_loop.run_app(&mut app).unwrap();
}
fn create_window(event_loop: &ActiveEventLoop) -> (PossiblyCurrentContext, Display, Window, Surface<WindowSurface>) {
let window_attrs = Window::default_attributes()
.with_inner_size(PhysicalSize::new(1000., 600.))
.with_title("Femtovg");
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, template, |mut configs| configs.next().unwrap())
.unwrap();
let window = window.unwrap();
let gl_display = gl_config.display();
let raw_window_handle = window.window_handle().unwrap().as_raw();
let context_attributes = ContextAttributesBuilder::new().build(Some(raw_window_handle));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let raw_window_handle = window.window_handle().unwrap().as_raw();
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
raw_window_handle,
NonZeroU32::new(1000).unwrap(),
NonZeroU32::new(600).unwrap(),
);
let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };
(
not_current_gl_context.take().unwrap().make_current(&surface).unwrap(),
gl_display,
window,
surface,
)
}
fn render(
context: &PossiblyCurrentContext,
surface: &Surface<WindowSurface>,
window: &Window,
canvas: &mut Canvas<OpenGl>,
square_position: PhysicalPosition<f64>,
) {
// Make sure the canvas has the right size:
let size = window.inner_size();
canvas.set_size(size.width, size.height, window.scale_factor() as f32);
canvas.clear_rect(0, 0, size.width, size.height, Color::black());
// Make smol red rectangle
canvas.clear_rect(
square_position.x as u32,
square_position.y as u32,
30,
30,
Color::rgbf(1., 0., 0.),
);
// Tell renderer to execute all drawing commands
canvas.flush();
// Display what we've just rendered
surface.swap_buffers(context).expect("Could not swap buffers");
}