OpenGL
## WebRender and OpenGL
The general concept of interacting between OpenGL and webrender is fairly simple - you draw to a texture,
hand it to webrender and webrender draws it when necessary. What can be quite confusing however, is the
way that the process of drawing to an OpenGL texture is structured.
The problem, however, is that if we'd allow directly pushing a `Texture` into the DOM, there would be
no way of knowing how large that texture needs to be, since the size can depend on the size and number
of its sibling DOM nodes.
We need to know the size of the texture for aspect ratio correction and preventing stretched / blurry
textures. Since the size of the rectangle that the texture should cover isn't known until it is time to
layout the frame, we have to "delay" the rendering of the texture. In azuls case, the DOM-building step
simply pushes a `GlTextureCallback` instead of the texture itself, i.e. a function that will render
the texture in the future (after the layout step).
The definition of a `GlTextureCallback` is the following:
```rust
pub struct GlTextureCallback(pub fn(&StackCheckedPointer, LayoutInfo, HidpiAdjustedBounds) -> Option);
```
The `HidpiAdjustedBounds` contains the `width, height` of the desired texture as well as HiDPI information
that you might need to scale the content of your texture correctly. If the callback returns `None`, the
result is simply a white square. The `LayoutInfo` allows you to create an OpenGL texture like this:
```rust
let mut texture = window_info.window.create_texture(
hi_dpi_bounds.physical_size.width as usize,
hi_dpi_bounds.physical_size.height as usize);
```
This creates an empty, uninitialized GPU texture. Note that we use the `physical_size` instead of
the `logical_size` - the "logical size" is the HiDPI-adjusted version (which is usually what you want
to calculate UI metrics), but the physical size is necessary in this case to provide the actual size of
the texture, without a HiDPI scaling factor.
Next, we clear the texture with the color red (255, 0, 0) and return it:
```rust
texture.as_surface().clear_color(1.0, 0.0, 0.0, 1.0);
return Some(texture);
```
Here, the `.as_surface()` activates the texture as the current FBO and draws to it. In this case we
only clear the texture and return it, but of course, you can do much more here - upload and draw
vertices and textures, activate and bind shaders, etc. See the `Surface` trait for more details.
Azul requires at least OpenGL 3.1, which is checked at startup. You can rely on any function of
OpenGL 3.1 being available at the time the callback is called.
Here is a simple, full example:
```rust
impl Layout for OpenGlAppState {
fn layout(&self, _info: LayoutInfo) -> Dom {
// See below for the meaning of StackCheckedPointer::new(self)
Dom::new(NodeType::GlTexture(GlTextureCallback(render_my_texture), StackCheckedPointer::new(self)))
}
}
fn render_my_texture(
state: &StackCheckedPointer,
info: LayoutInfo,
hi_dpi_bounds: HidpiAdjustedBounds)
-> Option
{
let mut texture = info.window.create_texture(
hi_dpi_bounds.physical_size.width as usize,
hi_dpi_bounds.physical_size.height as usize);
texture.as_surface().clear_color(0.0, 1.0, 0.0, 1.0);
Some(texture)
}
```
This should give you a window with a red texture spanning the entire window. Remember than if a
`Div` isn't limited in width / height, it will try to fill its parent, in this case the entire window.
Try adding another `Div` in the `layout()` function and laying them out horizontally via CSS. You can
decorate, skew, etc. your texture with CSS as you like. You can even use clipping and borders - OpenGL
textures get treated the same way as regular images.
## Using and updating the components state in OpenGL textures
Let's say you have a component that draws a cube to an OpenGL texture. You want to update the
cube's rotation, translation and scale from another UI component. How would you implement such a widget?
By now you have probably noticed the `StackCheckedPointer` that gets passed into the callback.
This allows you to build a stack-allocated "component" that takes care of rendering itself, without
the user calling any rendering code. As always, be careful to cast the pointer back to the type you
created it with (see the [https://github.com/maps4print/azul/wiki/Two-way-data-binding] chapter on
why `StackCheckedPointer` is unsafe and how to migitate this problem to build a type-safe API).
For example, we want to draw a cube, and control its rotation and scaling from another UI element.
So we build a `CubeControl` that renders the cube and exposes an API to control the cubes rotation
from any other UI element:
```rust
// This is your "API" that other UI elements will mess with. For example, a user could hook up a button
// to increase the scale by 0.1 every time a button is clicked.
//
// The point is that the CubeControl is just a dumb struct, it doesn't know about any other component.
// The CubeControl contains all the "state" necessary for your renderer, the renderer itself doesn't know
// about the state itself
pub struct CubeControl {
pub translation: Vector3,
pub rotation: Quaternion,
pub scaling: Vector3,
}
// This is your "rendering component", i.e. the thing that generates the DOM.
// The procedure is similar to how you'd use a regular StackCheckedPointer
#[derive(Default)]
pub struct CubeRenderer { /* no state! */ }
impl CubeRenderer {
// The DOM stores the pointer to the state of the renderer. This state may be modified
// by other UI controls before the GlTextureCallback is invoked.
pub fn dom(data: &CubeControl) -> Dom {
// Regular two-way data binding. Yes, you should use unwrap() here, since StackCheckedPointer
// will only fail if the data is not on the stack.
//
// Think of this as a StackCheckedPointer - internally the `` type is erased
// and you need to cast it back manually in the rendering callback
let ptr = StackCheckedPointer::new(data);
Dom::new(NodeType::GlTexture(GlTextureCallback(Self::render), ptr))
}
// Private rendering function. External code doesn't need to know or care how the
// `CubeRenderer` renders itself.
fn render(
state: &StackCheckedPointer,
info: LayoutInfo,
bounds: HidpiAdjustedBounds)
{
// Important: The type of the StackCheckedPointer has been erased
// Casting the pointer back to anything else than a &mut CubeControl will invoke undefined behaviour.
// HOWEVER: This function is (and should be) private. Only you, the **creator** of this component
// can invoke UB, not the user.
//
// The way that the pointer is casted back is by giving it a function that has the same signature
// as the render() function, but with a `&mut CubeControl` instead of a `&StackCheckedPointer`.
// You do not have to worry about aliasing the pointer or race conditions, that is what the
// StackCheckedPointer takes care of.
fn render_inner(component_state: &mut CubeControl, info: LayoutInfo, bounds: HidpiAdjustedBounds) -> Texture {
let texture = info.window.create_texture(width as u32, height as u32);
// render_cube_to_surface (not included in this example for brevity) takes the texture
// **and the current state of the component** and draws the cube on the surface according to the state
render_cube_to_surface(texture.as_surface(), &component_state);
// You could update your component_state here, if you'd like.
texture
}
// Cast the StackCheckedPointer to a CubeControl, then invoke the render_inner function on it.
Some(unsafe { state.invoke_mut_texture(render_inner, state, info, bounds) })
}
}
```
Now, why is this so complicated? The answer is that now the API for the user of this component is very easy:
```rust
// The data model of the final program
struct DataModel {
// Stores the state of the cubes rotation, scaling and translation.
// The user doesn't need to know about any other details
cube_control: CubeControl,
}
impl Layout for DataModel {
fn layout(&self, _info: LayoutInfo) -> Dom {
CubeRenderer::default().dom(&self.cube_control)
}
}
```
That's it! Now the user can hook up other components or custom callbacks that modify `self.cube_control` -
but the application data is cleanly seperated from its view or other components.
The user does also not need to care about how the component (in this case our `CubeRenderer`) renders
itself, it is done "magically" by the framework - the framework determines when to call the
`GlTextureCallback` and does so behind the back of the user. There is **no code** that the user
has to write in order to render a `CubeRenderer`. The only limitation is that the `CubeControl`
has to be stack-allocated, it can't be stored in a `Vec` or similar (because then, azul can't
reason about the lifetime of the component, to make sure it's not dereferencing a dangling pointer).
## What is `StackCheckedPointer::new(self)` ?
If you followed closely, you can probably already see what this is doing: `StackCheckedPointer` takes a reference to something on the stack that is contained in `T`. However, it can also take a reference to the **entire data model** (i.e. `T` itself). So `StackCheckedPointer::new(self)` essentially builds a `StackCheckedPointer` - the pointer can be safely casted back to a `OpenGlAppState`,
at which point the callback has full control over the entire data model. Usually this is only
something you'd want to do for prototyping, it's better for maintentance to build a custom
component as shown above, for type safety reasons.
## Raw OpenGL
For ease of use, azul exposes primitives of the `glium` library, which provide functions, such as
for example `.clear_color()` - usually it's easier to work with that than with raw OpenGL - glium
provides primitives for GLSL shader compilation and linking.
Right now OpenGL is the only supported backend and that will probably stay this way in the future -
since webrender is only portable to platforms that can target OpenGL, it wouldn't make sense to
support other rendering backends, since webrender, the main appeal of this entire library,
wouldn't run on them. There are experiments of porting webrender to Vulkan / DirectX or Metal,
however these are, as the name implies, experimental indeed.
## Notes
- It is not possible to render directly to the screen (for example, to use the built-in MSAA).
![Azul SVG demo](https://i.imgur.com/JQvtmxA.png)
For drawing custom graphics, azul has a high-performance 2D vector API. It also
allows you to load and draw SVG files (with the exceptions of gradients: gradients
in SVG files are not yet supported). But azul itself does not know about SVG shapes
at all - so how the SVG widget implemented?
The solution is to draw the SVG to an OpenGL texture and hand that to azul. This
way, the SVG drawing component could even be implemented in an external crate, if
you really wanted to. This mechanism also allows for completely custom drawing
(let's say: a game, a 3D viewer, etc.) to be drawn.
## Necessary features
SVG rendering has a number of dependencies that are not enabled by default, to save
on compile time in simple cases (i.e. by default the SVG module is disabled, so
that a Hello-World doesn't need to compile those unnecessary dependencies):
```toml
[dependencies.azul]
git = "https://github.com/maps4print/azul"
rev = "..."
features = ["svg", "svg_parsing"]
```
You only need to enable the `svg_parsing` feature if you want to parse SVG files.
For building the documentation, you'll likely want to run `cargo doc --all-features`.
You can import the SVG module using `use azul::widgets::svg::*;`.
## Getting started
The SVG component currently uses the `resvg` parser, `usvg` simplification and the
`lyon` triangulation libraries). Of course you can also add custom shapes
(bezier curves, circles, lines, whatever) programmatically, without going through
the SVG parser:
```rust
use azul::prelude::*;
use azul::widgets::svg::*;
const TEST_SVG: &str = include_str!("tiger.svg");
impl Layout for Model {
fn layout(&self, _info: LayoutInfo) -> Dom {
if let Some((svg_cache, svg_layers)) = self.svg {
Svg::with_layers(svg_layers).dom(&info.window, &svg_cache)
} else {
Button::labeled("Load SVG file").dom()
.with_callback(load_svg)
}
}
}
fn load_svg(app_state: &mut AppState, _: &mut CallbackInfo) -> UpdateScreen {
let mut svg_cache = SvgCache::empty();
let svg_layers = svg_cache.add_svg(TEST_SVG).unwrap();
app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers)));
Redraw
}
```
This is one of the few exceptions where azul allows persistent data across frames
since it wouldn't be performant enough otherwise. Ideally you'd have to load, triangulate
and draw the SVG file on every frame, but this isn't performant. You might have
noticed that the `.dom()` function takes in an extra parameter: The `svg_cache`
and the `info.window`. This way, the `svg_cache` handles everything necessary to
cache vertex buffers / the triangulated layers and shapes, only the drawing itself
is done on every frame.
Additionally, you can also register callbacks on any item **inside** the SVG using the
`SvgCallbacks`, i.e. when someone clicks on or hovers over a certain shape. In order
to draw your own vector data (for example in order to make a vector graphics editor),
you can build the "SVG layers" yourself (ex. from the SVG data). Each layer is
batch-rendered, so you can draw many lines or polygons in one draw call, as long as
they share the same `SvgStyle`.
Back to overview
Back to overview