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:
[dependencies]
winit = "0.28.6"
glutin = "0.30.10"
The first thing we need to do is create an Event Loop – we'll only really use it later, but we can't even create a window without it!
use winit::event_loop::EventLoop;
fn main() {
let event_loop = EventLoop::new();
}
Let's configure a window. We can specify many settings here, but let's just set the size and title:
use winit::window::WindowBuilder;
use winit::dpi::PhysicalSize;
let window_builder = WindowBuilder::new()
.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 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_builder(Some(window_builder));
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 context_attributes = ContextAttributesBuilder::new().build(Some(window.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 surface::{SurfaceAttributesBuilder, WindowSurface},
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
window.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 event_loop and current_context for the next step, but as promised, we can hide everything else in a function. Here's the code we have so far:
use std::num::NonZeroU32;
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasRawWindowHandle;
use winit::dpi::PhysicalSize;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
context::PossiblyCurrentContext,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
fn main() {
let event_loop = EventLoop::new().unwrap();
let _context = create_window(&event_loop);
}
fn create_window(event_loop: &EventLoop<()>) -> PossiblyCurrentContext {
let window_builder = WindowBuilder::new()
.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_builder(Some(window_builder));
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 context_attributes = ContextAttributesBuilder::new().build(Some(window.raw_window_handle()));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
window.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 immediately exits successfully.
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 main() {
let (context, gl_display, window, surface) = ...;
// [...]
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, but if you run your program, you won't get to look at it for very long – as soon as render completes, the program exits. To fix this, let's freeze the program with an infinite loop {}.
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::HasRawWindowHandle;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use winit::{dpi::PhysicalSize, window::Window};
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
fn main() {
let event_loop = EventLoop::new().unwrap();
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);
#[allow(clippy::empty_loop)]
loop {}
}
fn create_window(event_loop: &EventLoop<()>) -> (PossiblyCurrentContext, Display, Window, Surface<WindowSurface>) {
let window_builder = WindowBuilder::new()
.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_builder(Some(window_builder));
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 context_attributes = ContextAttributesBuilder::new().build(Some(window.raw_window_handle()));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
window.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.
Let's edit main to handle events instead of doing nothing with loop {}:
fn main() {
let event_loop = EventLoop::new();
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");
render(&context, &surface, &window, &mut canvas);
event_loop.run(|event, _target, control_flow| match event {
Event::WindowEvent { window_id, event } => {
println!("{:?}", event)
}
_ => {}
})
}
event_loop.run will call the provided closure for each new event, until the program exits.
Event is the first parameter of the closure, and the one we are most interested in. It is an enum with a few branches for different types of events. In the example above, we only capture and print WindowEvents, and ignore the rest.
Each Event::WindowEvent contains:
- 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.
Note:
Event::WindowEventis a branch of theEventenum, which contains another enum,WindowEvent, of the same name! It can get confusing, but Rust namespaces distinguish between the two – one isglutin::event::Event::WindowEventand the other isglutin::event::WindowEvent.
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 }, modifiers: (empty) }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 0, value: 969.1624302163254 }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 1, value: 372.08075569337234 }
CursorMoved { device_id: DeviceId(X(DeviceId(2))), position: PhysicalPosition { x: 981.8682861328125, y: 257.404296875 }, modifiers: (empty) }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 0, value: 1019.8683132424485 }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 1, value: 359.4042849368416 }
CursorLeft { device_id: DeviceId(X(DeviceId(2))) }
KeyboardInput { device_id: DeviceId(X(DeviceId(3))), input: KeyboardInput { scancode: 113, state: Released, virtual_keycode: None, modifiers: (empty) }, is_synthetic: true }
ModifiersChanged((empty))
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:
event_loop.run(move |event, _target, control_flow| match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
_ => {}
},
_ => {}
})
Try running it! Thr program should exit when you click the close button.
The third parameter of the closure is of type
ControlFlow. You can set it toControlFlow::Exitto request the application to quit.
Tracking Mouse Movement
Using the CursorMoved event, we can track the mouse position. Let's create a variable for storing the latest position we know, and updating it whenever we get a CursorMoved event:
let mut mouse_position = PhysicalPosition::new(0., 0.);
event_loop.run(move |event, _target, control_flow| match event {
Event::WindowEvent { window_id, event } => match event {
WindowEvent::CursorMoved { position, .. } => {
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 Event::RedrawRequested event, and that's when we can render a new frame:
let mut mouse_position = PhysicalPosition::new(0., 0.);
event_loop.run(move |event, _target, control_flow| match event {
Event::WindowEvent { window_id, event } => match event {
WindowEvent::CursorMoved { position, .. } => {
mouse_position = position;
window.request_redraw();
}
// ...
},
Event::RedrawRequested(_) => {
render(&context, &surface, &window, &mut canvas, 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::HasRawWindowHandle;
use winit::dpi::PhysicalPosition;
use winit::event::{Event, WindowEvent};
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use winit::{dpi::PhysicalSize, window::Window};
use glutin::{
config::ConfigTemplateBuilder,
context::ContextAttributesBuilder,
display::GetGlDisplay,
prelude::*,
surface::{SurfaceAttributesBuilder, WindowSurface},
};
fn main() {
let event_loop = EventLoop::new().unwrap();
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);
let mut mouse_position = PhysicalPosition::new(0., 0.);
event_loop
.run(move |event, target| {
if let Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::CursorMoved { position, .. } => {
mouse_position = position;
window.request_redraw();
}
WindowEvent::CloseRequested => target.exit(),
WindowEvent::RedrawRequested => {
render(&context, &surface, &window, &mut canvas, mouse_position);
}
_ => {}
}
}
})
.unwrap();
}
fn create_window(event_loop: &EventLoop<()>) -> (PossiblyCurrentContext, Display, Window, Surface<WindowSurface>) {
let window_builder = WindowBuilder::new()
.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_builder(Some(window_builder));
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 context_attributes = ContextAttributesBuilder::new().build(Some(window.raw_window_handle()));
let mut not_current_gl_context =
Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });
let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
window.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");
}