Back
Featured image of post Efficient custom shapes in QtQuick with Rust

Efficient custom shapes in QtQuick with Rust

One of the advantages of QWidgets when building a Qt application is the ability to build in a simple way custom widgets with the QPainter API. This gives the Qt developer almost total freedom to implement complex geometries for their widgets.

On the other hands, QML contains by default only rectangles. These rectangles can change the radius to create circles and rounded rectangles, but more complex shapes are more complicated.

The current state of custom geometry in QtQuick

Fortunally, the Qt API provides multiple ways to implement custom shapes, that depending on the needs might be enough.

There is the Canvas API using the same API as the canvas API on the web but in QML. It’s easy to use but very slow and I wouldn’t recommend it.

Instead of the Canvas API, from the QML side, there is the QtQuick Shapes module. This module allows creating more complex shapes directly from the QML with a straightforward declarative API. In many cases, this is good enough for the application developer but this module doesn’t offer a public C++ API.

If you need more controls, using C++ will be required to implement custom QQuickItem. Unfortunately drawing on the GPU using QQuickItem is more complex than the QPainter API. You can’t just use commands like drawRect, but will need to convert all your shapes in triangles first. This involves a lot of maths like it can be seen in the example from the official documentation or from the KDAB tutorial (Efficient custom shapes in Qt Quick).

A QPainer way is also available with QQuickPaintedItem, but it is slow because it renders your shape in a textured rectangle in the Scene Graph.

The Rusty way

What if we could transform arbitrary shapes into triangles? We would get a high level API but still get great performance. This process is called tessellation and there are a few libraries that implement it. For example in C++, we have Skia and CGAL. Unfortunatelly, both aren’t easy to use, so I decided to look at the Rust library ecosystem and in particular at Lyon, which was designed with performance and compliance to the SVG standard in mind since the goal is to use it in Servo in the future.

Lyon doesn’t have any C++ bindings but I got inspired by the recent blog post from Jonah and I need to say the experience of writing bindings was a breeze.

The first step was creating wrapper structs around the Lyon primitives. LyonPoint, LyonGeometry and LyonBuilder will later be directly usable from the C++ side.

#[cxx::bridge]
mod ffi {
    pub struct LyonPoint {
        x: f32,
        y: f32,
    }

    pub struct LyonVector {
        x: f32,
        y: f32,
    }

    pub struct LyonGeometry {
        vertices: Vec<LyonPoint>,
        indices: Vec<u16>,
    }

    extern "Rust" {
        type LyonBuilder;
        fn new_builder() -> Box<LyonBuilder>;
        fn move_to(self: &mut LyonBuilder, point: &LyonPoint);
        fn line_to(self: &mut LyonBuilder, point: &LyonPoint);
        fn relative_move_to(self: &mut LyonBuilder, to: LyonVector);
        fn close(self: &mut LyonBuilder);
        fn quadratic_bezier_to(self: &mut LyonBuilder, ctrl: &LyonPoint, to: &LyonPoint);
        fn cubic_bezier_to(self: &mut LyonBuilder, ctrl1: &LyonPoint, ctrl2: &LyonPoint, to: &LyonPoint);
        fn build_fill(builder: Box<LyonBuilder>) -> LyonGeometry;
        fn build_stroke(builder: Box<LyonBuilder>) -> LyonGeometry;
    }
}

We then need to define the methods we declared above. These are all trivial to implement since they are just wrapping the Lyon API.

use ffi:{LyonPoint, LyonVector, LyonGeometry};

// Create a wrapper arround Lyon svg path. This struct is opaque from
// the C++ side so we won't be able to access the internal object, but
// we still can call the methods on it.
pub struct LyonBuilder {
    builder: WithSvg<Builder>,
}

// Implement wrapping methods
impl LyonBuilder {
    fn close(&mut self) {
        self.builder.close();
    }

    fn move_to(&mut self, to: &LyonPoint) {
        self.builder.move_to(point(to.x, to.y));
    }

    fn line_to(&mut self, to: &LyonPoint) {
        self.builder.line_to(point(to.x, to.y));
    }

    fn quadratic_bezier_to(&mut self, ctrl: &LyonPoint, to: &LyonPoint) {
        self.builder.quadratic_bezier_to(point(ctrl.x, ctrl.y), point(to.x, to.y));
    }

    ...
}

// Lyon Builder constructor
pub fn new_builder() -> Box<LyonBuilder> {
    return Box::new(LyonBuilder{
        builder: Path::builder().with_svg()
    })
}

The next step was to add the build_fill that will transform the SVG path instructions into a set of vertices and indices. These vertices and indices will be directly available from the C++ side. This is extremely handy since this can be directly fed into the QSGGeometry painting method.

pub fn build_fill(builder: Box<LyonBuilder>) -> LyonGeometry {
    let mut buffers: VertexBuffers<Point, u16> = VertexBuffers::new();
    {
        let mut vertex_builder = simple_builder(&mut buffers);

        // Create the tessellator.
        let mut tessellator = FillTessellator::new();

        let path = builder.builder.build();

        // Compute the tessellation.
        let result = tessellator.tessellate_path(
            &path,
            &FillOptions::tolerance(0.01),
            &mut vertex_builder
        );
        assert!(result.is_ok());
    }

    LyonGeometry {
        // convert_points transform lyon::point to our LyonPoint wrapper
        vertices: convert_points(buffers.vertices), 
        indices: buffers.indices,
    }
}

And we are almost done with the Rust side, we still need to create the cargo and corrosion configuration, but I won’t go into details in this post. You can look at how it was done in this pet project.

Using the generated bindings

To make it easy to store and manipulate the path, I create a simple abstraction to the various SVG path instructions.

#include <QList>
#include <variant>
#include <tessellation.rs.h>

/// Move to the point without drawing a line.
struct MoveTo
{
    /// The destination.
    LyonPoint to;
};

/// Drawe a line to a specific point.
struct LineTo
{
    /// The destination.
    LyonPoint to;
};

/// Draw a cubic bezier curve to the point.
struct CubicBezierTo
{
    /// First control point.
    LyonPoint ctrl1;
    /// Second control point.
    LyonPoint ctrl2;
    /// The destination.
    LyonPoint to;
};

/// Close a path.
struct Close
{};

/// SVG conform path commands
using PathSection = std::variant<MoveTo, LineTo, CubicBezierTo, Close>;

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

/// The SVG path data. It contains a list of instruction (move to, line to, ...).
using PathData = QList<PathSection>;

Now let finally use Lyon to generate the geometry primitives. This will need to be called every time the list of commands is updated. It’s using the command abstraction, I build previously, but this could directly call the LyonBuilder methods.

const auto commands << MoveTo { LyonPoint{0.0, 0.0} }
                    << LineTo { LyonPoint{0.0, 40.0} }
                    << LineTo { LyonPoint{40.0, 40.0} }
                    << CubicBezierTo{ LyonPoint{70.0, 40.0}, LyonPoint{70.0, 0.0}, LyonPoint{ 50.0, 20.0} }
                    << LineTo { LyonPoint{40.0, 0.0} }
                    << Close {};

auto lyonBuilder = new_builder();
for (const auto &command: commands) {
    std::visit(overloaded {
        [&lyonBuilder](MoveTo moveTo) { lyonBuilder->move_to(moveTo.to); },
        [&lyonBuilder](LineTo lineTo) { lyonBuilder->line_to(lineTo.to); },
        [&lyonBuilder](CubicBezierTo cubicBezierTo) { lyonBuilder->cubic_bezier_to(cubicBezierTo.ctrl1, cubicBezierTo.ctrl2, cubicBezierTo.to); },
        [&lyonBuilder](Close) { lyonBuilder->close(); },
    }, command);
}
auto m_geometry = build_fill(std::move(lyonBuilder));

And finally here is our updatePaintNode method. It’s using the GL_TRIANGLES drawing mode and the vertices and indices are copied directly from the geometry Lyon gave us.

QSGNode *PathItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGGeometryNode *node = nullptr;
    QSGGeometry *geometry = nullptr;

    if (!oldNode) {
        node = new QSGGeometryNode;
        geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),
                                   m_geometry.vertices.size(), m_geometry.indices.size());
        geometry->setIndexDataPattern(QSGGeometry::StaticPattern);
        geometry->setDrawingMode(GL_TRIANGLES);
        node->setGeometry(geometry);
        node->setFlag(QSGNode::OwnsGeometry);

        QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
        material->setColor(QColor(255, 0, 0));
        node->setMaterial(material);
        node->setFlag(QSGNode::OwnsMaterial);
    } else {
        node = static_cast<QSGGeometryNode *>(oldNode);
        geometry = node->geometry();
        geometry->allocate(m_geometry.vertices.size(), m_geometry.indices.size());
    }

    QSGGeometry::Point2D *points = geometry->vertexDataAsPoint2D();
    std::size_t i = 0;
    for (const auto &vertice: m_geometry.vertices) {
        points[i].set(vertice.x, vertice.y);
        i++;
    }

    quint16* indices = geometry->indexDataAsUShort();
    i = 0;
    for (const auto indice: m_geometry.indices) {
        indices[i] = indice;
        i++;
    }

    node->markDirty(QSGNode::DirtyGeometry);
    return node;
}

It is only using Lyon SVG path rendering, but Lyon provides a lot more APIs. For example, there is an abstraction that allows to draw circle, ellipse, rounded rectangle and other basic geometric forms.

There is also the possibility to add custom attributes for texture coordinate or color coordinate. Depending on your need more part of the API could be wrapped and I might create a small library wrapping most of the API.

Custom shape in action

I used this technique in a new toy I’m building. I’m not sure where it is going, but I currently have this:

Comments

You can use your Mastodon account to reply to this post.

Reply

Licensed under CC BY-SA 4.0