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::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 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.