KAS Blog

Please see outline on the left.

Why I created kas-text

September 2020

Existing libraries

This is going to be a very brief (and fairly naive) dive into text rendering and the state of available Rust libraries, written in easy English with minimal jargon.

The basics

Lets see. You're writing a small graphical game/tool/general UI and wish to print a simple string of text, say, "Hello World!" There are many APIs that can do this, but still several steps involved:

  1. Selecting a font: this might be embedded or the app may try to discover a "standard" system font. The font-kit crate can discover system fonts.
  2. Reading the font: there are a selection of libraries available, from the low level ttf-parser to the mid-level ab_glyph to more complex tool sets including font-kit, rusttype and fontdue.
  3. Translating the input string to a sequence of positioned and scaled glyphs (layout): the above-mentioned font-kit, rusttype and fontdue can all accomplish this, along with the more specialised glyph_brush_layout.
  4. Rasterising glyphs: this is the job of glyph_brush and, again, font-kit, rusttype and fontdue.
  5. Integrating rasterisation with the graphics pipeline: e.g. gfx_glyph and wgpu_glyph do this job. Several high-level libraries (e.g. ggez and amethyst_ui) provide direct text support.

Of course, mostly you only need consider steps 1 and 5 and you're done.

Wrapping and alignment

Above, we considered only a short line of text: "Hello World!" Frequently you will have longer pieces of text that require wrapping (usually on word boundaries), and you may wish the result to be aligned to the left, centre or right, or even justified. Fortunately the above libraries already have you covered (aside from justified text, which is less often supported).

Text wrapping is a deceptively simple problem, but more on that in my next post.

Shaping

The above libraries all use a fairly simple layout system: match each char to a font glyph, place this at the previous glyph's "advance position", then if the font provides kerning data for this pair of glyphs use it to adjust the gap between the two. For English (and probably most languages) this is sufficient, at least most of the time.

Shaping allows more complex glyph selection and positioning, and is an important part of real text libraries. See e.g. HarfBuzz on shaping and Wikipedia on Complex Text Layout (although the latter also concerns bidirectional text; more on that later).

Essentially, shaping takes a text string along with a font and spits out a sequence of positioned glyphs. This means that replacing the basic layout system used above with a text shaper should, in theory, be straightforward.

For Rust, we have the HarfBuzz binding harfbuzz-rs, as well as two immature pure-Rust libraries, rustybuzz and allsorts. Of the general-purpose text libraries, only fontdue includes shaping in its roadmap.

Right-to-left and bidirectional text

Several scripts, such as Hebrew and Arabic, are written right-to-left. Supporting such scripts generally also requires supporting embedded left-to-right fragments, and thus requires supporting bidirectional texts.

Unicode, as part of TR9, specifies how to determine the bidi embedding level and thus the text direction for each character in the source text, as well as how to re-order the input from logical order into display order. Unfortunately, integrating support for bidi texts into layout is not so simple (this is most of the topic of the next post).

The unicode-bidi crate implements much of Unicode TR9, but on its own this is insufficient. None of the above libraries support bidirectional text.

Formatting

Formatting may apply one of several effects to text. These include:

  • adjusting the font size
  • changing the font's (foreground) colour
  • using a bold variant (or more generally, adjusting the weight)
  • using an italic variant (either via use of another font variant or via slanting glyphs during rendering)
  • drawing underline or strike-through

Some of the above font libraries have limited support for formatting specified via a list of text sections each with a font size and font identifier, and in some cases also colour information. The user is expected to translate formatted text from its source format as well as to select appropriate fonts.

Font properties and variable fonts

When mentioning font selection above, we glossed over a couple of issues. How does one select an appropriate italic or bold variant? Can these be synthesised if not provided?

The font-kit crate can help with the above: it allows font selection by family, weight and style. Some fonts are variable, meaning that glyphs can be synthesised to the required weight (giving much more control than simply "bold" or "normal"). The style allows selection of normal, italic (curved) or oblique (slanted) variants.

I have not investigated this topic, but it appears that only font-kit's rasteriser API supports selection of weight or style; all other Rust libraries select font only by path/bytes and variant index (for multiple fonts embedded in a font pack).

Font fallbacks

What if your chosen font doesn't include all required glyphs, e.g. if you choose a font covering most European alphabets, but your user starts writing Korean? I believe that no Rust libraries have even started trying to address this problem.

The first sub-problem is an extension of font selection: choosing appropriate fallback font(s). CSS allows the user direct control here: see article. On Linux this is usually handled by Fontconfig.

The other sub-problem(s) concern integration: breaking text layout into multiple runs should your shaper not support multiple fonts (as HarfBuzz doesn't), then stitching the result back together.

KAS-text

The above libraries cover simple layout and rasterisation fairly well, but also leave a lot out, especially regarding complex layout and use of formatted input texts.

KAS-text attempts to fill this gap: translate from a raw input text to a sequence of positioned glyphs, which can be easily adapted for use by renderers such as wgpu_glyph. Additionally, it exposes a stateful prepared::Text object, allowing fast re-draws and re-wrapping of a given input text (though this stateful API may not be appropriate for all users).

Since this article is written after-the-fact, kas-text v0.1 already exists. You can read its API docs here. v0.1 already supports shaping (via HarfBuzz) and bidirectional text, which, to my knowledge, makes it the first Rust library to support these features (on the full layout cycle from raw text input to positioned glyphs), admittedly with imperfections.

Near-future plans include support for formatted text, including translation from Markdown or (simple) HTML input and mostly-automatic font selection. Other topics, such as vertical text, font synthesis, font fallbacks and emoticons, have not been planned but could eventually be added.

KAS GUI

The v0.5 release of KAS has integration with KAS-text, including a reasonably functional multi-line text-editor. Check it out or run yourself:

git clone https://github.com/kas-gui/kas.git
cd kas/kas-wgpu
cargo run --example layout --features shaping

Line wrapping: the hardest problem in text layout?

September 2020

Obviously the title can't be true. Can it?

Unicode provides a short somewhat long technical report on line breaking. To quote from its overview:

Line breaking, also known as word wrapping, is the process of breaking a section of text into lines such that it will fit in the available width of a page, window or other display area. The Unicode Line Breaking Algorithm performs part of this process. Given an input text, it produces a set of positions called "break opportunities" that are appropriate points to begin a new line. The selection of actual line break positions from the set of break opportunities is not covered by the Unicode Line Breaking Algorithm, but is in the domain of higher level software with knowledge of the available width and the display size of the text.

To summarise, this algorithm tells us about mandatory breaks (e.g. after \n) and optional breaks (roughly speaking, the start of each word).

Fortunately for us, the Unicode Line Breaking Algorithm has already been implemented in Rust, by the xi-unicode library (from the same people as Xi Editor and Druid).

One foot in front of the other

glyph metrics

Lets start with the basics: horizontal, left-to-right text with simple layout. In this case, we use a caret starting at the line's origin. The first glyph is placed on this caret, then the caret is advanced by the glyph's advance width. The next glyph is placed similarly, except that if this pair of glyphs appears in the font's kerning table, then an offset is applied (this allows e.g. the 'T' in 'To' to hang over the 'o').

Line wrapping such text is simple: whenever the caret position extends beyond the available width, we back-step to the last optional line-break position, and line-break there. Well, not quite: we allow whitespace to extend beyond the line width (so if a bunch of extra spaces are inserted at the point a line is wrapped, they don't actually add space anywhere).

The above is what the glyph_brush_layout crate (of glyph-brush) does. It works fine for most left-to-right languages, provided no complicated layout is needed.

Actually, there is another point missing from this story: hyphenation. We leave this as a foot-note for now.

Shaping

Shaping was discussed previously and is an essential part of complex text support: the above advance-and-apply-kerning rules are insufficient for ligatures and entirely insufficient for complex texts like Arabic. A shaper is a separate tool which, given a sequence of (Unicode) text and a font, returns a list of positioned glyphs from this font. One such shaper is HarfBuzz.

Modifying our text layout system to support a shaper is not so hard (given a suitable design). Integrating line wrapping with an external shaper is only a little harder: the shaper returns a single line of text, within which we must track the positions of optional line-breaks.

KAS-text implements support for both simple layout (directly) and complex layout (via `harfbuzz-rs) within its shaper module.

Right-to-left and bi-directional text

This is where things start to get fun. When going left-to-right (hereafter LTR), we only need to implement the caret position. When going RTL, if only we could simply use the same logic but with flipped direction: alas, all font glyphs are positioned from the left, so we have to typeset them backwards.

Worse, RTL texts may embed short sequences (such as numbers) or even quote another language in the LTR direction — in some cases even embedding RTL text within LTR within RTL. Unicode TR9 specifies the Basic Display Algorithm, which essentially has three parts:

  1. Split the input text into paragraphs (trivial).
  2. Resolve embedding levels, where levels run from 0 to 125 and odd levels indicate RTL direction. This is complex but well specified and implemented by libraries such as unicode-bidi (albeit with bugs).
  3. Re-order the text. This is complex and inseparable from line-wrapping.

To go into further detail, according to Unicode TR9, re-ordering text involves:

  1. Split the input text into level runs: maximal sub-sets of characters with consistent embedding level.
  2. For each level run, apply shaping to yield a glyph sequence.
  3. Using the result of shaping, calculate line-wrapping positions.
  4. For each line, apply a sequence of rules (L1-L4) to re-order characters on that line.

Unfortunately this leaves us a problem: we cannot resolve where lines start and end without first applying shaping, and we but are given a set of rules to re-order characters (i.e. Unicode code points) not glyphs. There are two ways of, er, "solving", this problem:

  1. Apply shaping, calculate line-break positions, re-order (at char level), shape again. Not only does this require doing shaping twice, but further there is no guarantee that the result of doing so will still fit within our length bounds. Also, shapers like HarfBuzz expect text in logical order.
  2. Transform the re-ordering logic to work with glyph sequences instead of characters. HarfBuzz has implicit support for RTL text, so we never re-order characters, but only runs and only then at embedding level 2 and higher.

Option (2) is now the obvious choice, but there are still several details to work out: line-wrapping both LTR and RTL text, correctly applying alignment, embedding LTR within RTL and vice-versa, and a few corner cases. Each line has a dominant (initial) direction (which may not be the same as the paragraph direction). On that line one may append a whole run (in either direction) or part of a wrapped line — but since the logical end of a line should not be in its middle we only allow line wrapping when the run direction matches the line direction. [This may need adjustment.]

Summary

As we have seen, line-wrapping is only a small problem within the scope of text layout, but surprisingly complex in practice due to its inherent inseparability from other aspects of text layout.

Easy Cast

Introducing: easy-cast.

Problem: correct casts aren't easy

Let me ask you, have you ever done this?

#![allow(unused)]
fn main() {
fn make_some_vec() -> Vec<u8> { vec![] }
let v: Vec<_> = make_some_vec();

// We need a fixed-size type (not usize), or just want to save space.
// We can assume length <= u32::MAX.
let len = v.len() as u32;
}

Of course, you could write this instead:

#![allow(unused)]
fn main() {
fn make_some_vec() -> Vec<u8> { vec![] }
let v: Vec<_> = make_some_vec();

// We need a fixed-size type (not usize), or just want to save space.
use std::convert::TryFrom;
let len = u32::try_from(v.len()).unwrap();
}

What about this?

#![allow(unused)]
fn main() {
let x: f32 = 1234.5678;

// Convert to the nearest integer, assuming it fits
let x = (x.round()) as i32;
assert_eq!(x, 1235);
}

Or this?

#![allow(unused)]
fn main() {
use rand_distr::{Poisson, Distribution};
let poi = Poisson::new(1e6_f64).unwrap();
let num = poi.sample(&mut rand::thread_rng());

// Note: u64::MAX as f64 rounds to 2^64, so we need to use < not <= here!
assert!(0.0 <= num && num < u64::MAX as f64);
let num = num as u64;
}

Or even this?

#![allow(unused)]
fn main() {
fn some_value() -> i32 { 0 }
let x: i32 = some_value();
let y = x as f32;
if y as i32 == x {
    println!("No loss of precision!");
    // WARNING: this test is WRONG since e.g. i32::MAX rounds to 2^31 on cast
    // to f32, then rounds back to i32::MAX on cast to i32.
}
}

Summary

It should be easy and safe to convert one numeric type to another, but frequently it's not:

  • Using x as T is easy but not safe: it may truncate, sign-convert, round, or saturate. (Before Rust 1.45.0 behaviour might even be undefined.)
  • Using T::try_from(x).unwrap() is clunky when you expect the conversion to succeed.
  • TryFrom doesn't even support float conversions.
  • Conceptually, "convert a float to the nearest integer" is a single operation, yet most solutions require separate rounding and conversion steps.

It turns out this is not an easy problem to solve in general. I wrote an RFC on this. Nearly three years and a long discussion later, it's still unsolved.

Existing solutions

The standard library provides:

  • From, but it only covers portably infallible conversions
  • TryFrom, but it misses float conversions and expects you to do your own error handling

There are existing libraries on crates.io, yet all of them fall short in some way:

  • num_traits::NumCast: API is not extensible to user types; error handling is the user's responsibility; it's a relatively big library if you just want casts
  • cast: different API for fallible and infallible conversions; API around isize and usize is platform-dependent; error handling is the user's responsibility

Introducing easy-cast

Eventually I gave up trying to find a standard solution and wrote my own, starting with one particular problem (converting between usize and u32 indices in KAS-text, which stores many text and list indices). This tiny beginning grew into the easy-cast library:

  • a From-like trait, Conv, for all integer conversions
  • direct support for float-to-int-with-rounding via ConvFloat
  • easier usage via the Cast trait (like Into)
  • try_cast and try_conv, allowing user error handling on all conversions
  • and of course a few bug fixes and diagnostic improvements

Examples

Now, we can revisit the above problems using easy-cast:

#![allow(unused)]
fn main() {
use easy_cast::Conv;

fn make_some_vec() -> Vec<u8> { vec![] }
let v: Vec<_> = make_some_vec();
// Expect success, panic on failure
let len = u32::conv(v.len());
}
#![allow(unused)]
fn main() {
use easy_cast::CastFloat;

let x: f32 = 1234.5678;
// Convert to the nearest integer, panic if out-of-range
let x: i32 = x.cast_nearest();
}
#![allow(unused)]
fn main() {
use easy_cast::CastFloat;

use rand_distr::{Poisson, Distribution};
let poi = Poisson::new(1e6_f64).unwrap();
let num = poi.sample(&mut rand::thread_rng());

let num: u64 = num.cast_trunc();
}
#![allow(unused)]
fn main() {
use easy_cast::Conv;

fn some_value() -> i32 { 0 }
let x: i32 = some_value();
match f32::try_conv(x) {
    Ok(y) => println!("{x} is exactly representable by f32: {y}"),
    Err(e) => println!("{x} is not representable by f32: {e}"),
}
}

For any integer x and any integer or float type T, we get:

  • T::conv(x)
  • x.cast() with type inference
  • T::try_conv(x) and x.try_cast() returning Result<T, Error>

And for any float value x and integer type T we have:

  • x.cast_nearest(), x.cast_trunc(), x.cast_floor(), x.cast_ceil()
  • T::conv_nearest(x), etc.
  • T::try_conv_trunc(x), x.try_cast_floor(), etc.

Don't need the fuses?

One of the design goals for easy-cast was check everything in Debug builds, maybe not in Release builds. In a Debug build, or when the always_assert crate feature is used, cast and conv will panic on inexact conversions. In Release builds without this flag, the cast reduces to just x as T.

The same is not true for try_cast and try_from which always fail on inexact conversions.

We may make some adjustments here in future versions, e.g. to use always_assert by default (but if so, it will be considered a breaking change).

Impl-tools: beyond derive

Allow me introduce the impl-tools crate, by discussing the limitations of #[derive].

Deriving Default...

... over a method

Frequently, a type's public API includes a fn new() -> Self constructor, with Default implemented over this. impl_default lets us skip some boilerplate:

#![allow(unused)]
fn main() {
use impl_tools::impl_default;

#[impl_default(Foo::new())]
pub struct Foo {
    // fields here
}

impl Foo {
    pub fn new() -> Foo {
        Foo { /* fields here */ }
    }
}

let foo = Foo::default();
}

... for an enum

Similarly, we can derive Default for enums:

#![allow(unused)]
fn main() {
#[impl_tools::impl_default(Option::None)]
pub enum Option<T> {
    None,
    Some(T),
}

// (Yes, the impl is correct with regards to generics — see below.)
let x: Option<std::time::Instant> = Default::default();
}

... with specified field values

Lets say we want to implement Default for a struct with non-default values for fields:

#![allow(unused)]
fn main() {
struct CarStats {
    num_doors: u8,
    fuel_is_diesel: bool,
    fuel_capacity_liters: f32,
}
}

Wouldn't it be nice to be able to specify our default values in-place? We can, if we re-write using impl-tools:

#![allow(unused)]
fn main() {
use impl_tools::{impl_scope, impl_default};

impl_scope! {
    #[impl_default]
    struct CarStats {
        num_doors: u8 = 3,    // specified default value
        fuel_is_diesel: bool, // no initializer: uses type's default value
        fuel_capacity_liters: f32 = 50.0,
    }
}
}

Note that field: Ty = val is not (currently) Rust syntax. The impl_scope macro has special support for this, besides other functionality.

Deriving Debug: ignoring hidden fields

For example, let us consider Lcg64Xsh32 (also known as PCG32). This is a simple random number generator, and as per policy of the RngCore trait, does not print out internal state in its Debug implementation.

#![allow(unused)]
fn main() {
#[derive(Clone, PartialEq, Eq)]
pub struct Lcg64Xsh32 {
    state: u64,
    increment: u64,
}

// We still implement `Debug` since generic code often requires it
use std::fmt;
impl fmt::Debug for Lcg64Xsh32 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Lcg64Xsh32 {{}}")
    }
}
}

Using impl-tools we could reduce this to:

#![allow(unused)]
fn main() {
#[impl_tools::autoimpl(Debug ignore self.state, self.increment)]
#[derive(Clone, PartialEq, Eq)]
pub struct Lcg64Xsh32 {
    state: u64,
    increment: u64,
}
}

(This applies equally to fields which do not themselves implement Debug.)

Deriving Deref

How could we derive Deref and DerefMut? By using a specified field:

#![allow(unused)]
fn main() {
#[impl_tools::autoimpl(Deref, DerefMut using self.animal)]
struct Named<A> {
    name: String,
    animal: A,
}
}

Generics

An experienced Rustacean should know that #[derive] makes some incorrect assumptions with regards to generics. For example,

/// Implements Clone where T: Clone (correct)
#[derive(Clone)]
enum Option<T> {
    None,
    Some(T),
}

/// Implements Clone where T: Clone (unnecessary bound)
#[derive(Clone)]
struct Shared<T> {
    inner: std::rc::Rc<T>,
}

/// Attempts to implement Clone where T: Clone
/// (error: Clone for Cell<T> requires T: Copy)
#[derive(Clone)]
struct InnerMutable<T> {
    inner: std::cell::Cell<T>,
}

The #[autoimpl] macro takes a different approach: do not assume any bounds, but allow explicit listing of bounds as required.

#![allow(unused)]
fn main() {
use impl_tools::autoimpl;

// Note: autoimpl does not currently support enums (issue #6)

// No bound on T assumed
#[autoimpl(Clone)]
struct Shared<T> {
    inner: std::rc::Rc<T>,
}

// Explicit bound on T
#[autoimpl(Clone where T: Copy)]
struct InnerMutable<T> {
    inner: std::cell::Cell<T>,
}
}

To simplify the most common usage and to cover the case where multiple traits are implemented simultaneously, the keyword trait may be used as a bound:

#![allow(unused)]
fn main() {
use impl_tools::autoimpl;
#[autoimpl(Clone, Debug, Default where T: trait)]
struct Wrapper<T>(T);
}

Auto trait implementations

Lets say you write a trait, and wish to implement that trait for reference types:

#![allow(unused)]
fn main() {
trait Greet {
    fn greet(&self, name: &str);
}

impl<T: Greet + ?Sized> Greet for &T {
    fn greet(&self, name: &str) {
        (*self).greet(name);
    }
}

// Also impl for &mut T, Box<T>, ...
}

This can be quite tedious, enough so that macros (by example) are often used to deduplicate the implementations. But why should we have to write even the first implementation? It's all trivial code!

#![allow(unused)]
fn main() {
use impl_tools::autoimpl;

// One line to do it all:
#[autoimpl(for<T: trait + ?Sized> &T, &mut T, Box<T>)]
trait Greet {
    fn greet(&self, name: &str);
}

// A test, just to prove it works:
impl Greet for String {
    fn greet(&self, name: &str) {
        println!("Hi {name}, my name is {self}!");
    }
}
let s = "Zoe".to_string();
s.greet("Alex");
(&s).greet("Bob");
Box::new("Bob".to_string()).greet("Zoe");
}

Limitations

To conclude this article, let us briefly discuss limitations.

Support other standard traits: should be easy. #[autoimpl] currently supports only the most-frequently-used subset of traits supported by #[derive] (plus Deref and DerefMut). The crate should likely support everything that #[derive] does by default, and possibly more (see derive_more).

Extensibility: #[autoimpl] could be useful for user-defined traits; impl_scope could be useful for user-defined attribute macros. It would be nice if these could be extended in the same way as #[derive] is, but that is impossible (without direct support from Rust itself). Instead, the impl-tools crate is split in two: impl-tools-lib (contains all functionality) and impl-tools itself (just a thin proc-macro driver). An alternative driver containing additional functionality may be used instead of impl-tools. Exemplar: kas-macros (from v0.11).

Rust: state of GUI, December 2022

There was a recent call for blogs about Rust GUI. So, Are we GUI yet?

Contents:

Categorised listing of toolkits

Lets start by categorising entries from Are we GUI yet, ignoring those which appear abandoned or not very functional.

Bindings

Wrappers around platform-specific toolkits:

  • Mac OS / iOS - cacao - Rust bindings for AppKit and UIKit
  • Mac OS / iOS - core-foundation - Bindings to Core Foundation for macOS
  • Win32 - winsafe - Windows API and GUI in safe, idiomatic Rust

Wrappers around multi-platform non-Rust toolkits:

Web/DOM based

Frameworks built over web technologies:

  • Dioxus - React-like Rust toolkit for deploying DOM-based apps
  • Tauri - Deploy an app written in HTML or one of several web frontends to desktop or mobile platforms

Note that Dioxus uses Tauri for desktop deployments.

Rust toolkits

Finally, Rust-first toolkits (excluding the web-based ones above):

  • Druid - Data-oriented Rust UI design toolkit
  • egui - an easy-to-use GUI in pure Rust
  • fui - MVVM UI Framework
  • Iced - A cross-platform GUI library inspired by Elm
  • KAS - A pure-Rust GUI toolkit with stateful widgets
  • OrbTk - The Orbital Widget Toolkit
  • Relm4 - An idiomatic GUI library inspired by Elm
  • Slint - a toolkit to efficiently develop fluid graphical user interfaces for any display

Details

Lets take a deeper look at a few of the above.

Tauri is a framework for bringing web apps (supporting several major web frontend frameworks) to the desktop and (eventually) mobile. Dioxus is a React-like frontend written in Rust with built-in support for Tauri. Tauri has its own rendering library (WRY) over platform-specific WebView libraries along with a windowing library (TAO), a fork of winit.

Druid, a data-first Rust toolkit, is interesting in a few ways. First is the UI design around a shared data model, giving widgets focussed views through a system of lenses. Second is that it uses its own platform-integration library, druid-shell, along with its own graphics library, Piet. Finally, all widgets (from the user or library) implement only a simple interface, but must be stored within a special WidgetPod. Watch a recent talk by Raph Levien on the state and design of Druid, including the current Xilem re-design.

egui, an immediate-mode pure-Rust toolkit, succeeds both in being extremely easy to use and in having an impressive feature set while being fast. It has some limitations. Widgets may be implemented with a single function. Windowing may use winit, glutin or integration; rendering may use glow, Glium or wgpu.

Iced is an Elm-like toolkit. It supports integration, winit or glutin windowing with wgpu or glow for rendering or web deployment.

Relm4 is an Elm-like Rust toolkit over GTK 4.

Slint is a UI built around its own markup language (.slint) in the style of QML, with support for apps written in Rust, C++ and JavaScript. The markup language makes description of simple UIs easy, but as a result user-defined widgets have completely different syntax and limited capabilities compared to library-provided widgets. Slint uses either Qt or winit + femtovg + various platform-specific libraries.

KAS, by myself, is an efficient retained-state toolkit. It supports windowing via winit and rendering via wgpu.

State of KAS

Being a toolkit author myself, I ought to say a few words about the status of KAS. In case you hadn't guessed already, I like lists:

  • In September 2021, KAS v0.10 reorganised crates, added support for dynamic linking, improved the default theme, and standardised keyboard navigation.
  • Exactly a year later, v0.11 heavily revised the Widget traits and supporting macros, supported declarative complex static layout within widgets, plus many more small changes all aimed at making KAS easier to use.
  • I have just released v0.12 which uses Rust's recent support for Generic Associated Types to update temporary APIs.

To state the obvious, KAS isn't very popular. Despite this, it compares well with other Rust toolkits on features, missing a few (such as screen-reader support and dynamic declarative layout), but also supporting some less common ones, such as complex text formatting (currently limited to web frameworks, bindings, Druid and KAS) and fast momentum touch scrolling over large data models.

My main concerns regarding KAS are:

  • Without a userbase and with a (now) healthy competition, motivation to further develop the library is limited.
  • Immediate-mode GUIs, the Elm model and Druid all make dynamic layout easy. In KAS, all widgets must be declared with static layout (though hiding, paging etc. is possible and the declaration is often implicit).
  • Several aspects of KAS from glyph fallback and font discovery to the drawing interface and themes are hacks. Producing good implementations from these is possible, but alternatives should also be considered (such as switching to COSMIC Text or Piet). Either way, significant breaking changes should be expected before KAS v1.0.
  • Some redesign to EventState is needed to properly support overlay layers.

From another point of view, KAS has had several successes:

  • A powerful size model, if a little more complex to use than most toolkits
  • Robust and powerful event-handling model
  • Very fast and scalable
  • Not too hard to use for non-dynamic layout (if you tried KAS v0.10 or earlier, usability is much improved, and certainly much better than traditional toolkits like GTK or Win32)
  • The easy-cast library
  • I learned a plenty about proc-macros, resulting in a rather interesting method of implementing the Widget trait and the impl-tools library.

Future work, if I feel so inspired, may involve the following:

  • Dynamic declarative layout. Druid's Xilem project has given me ideas for a solution which should work over KAS's current model, though likely less efficient. This is not the right place to elaborate on this.
  • Async support? Currently, KAS only supports async code via threads and a proxy which must be created from the toolkit before the UI starts. Real async support allows non-blocking slow event handlers in a single thread. KAS could support this, e.g. via a button-press handler generating a future which yields a message on completion, then resuming the current event-handling stack with that message. However, this may not be the best approach (especially not if it requires every event-handling widget to directly support resuming a future). A variant of this approach may be more viable together with the dynamic declarative layout idea mentioned above.

State of GUI

The purpose of this post is not just to talk about KAS, but about Rust GUIs in general. To quote a reddit comment:

GUI will never be "solved" because it has become a problem too complex to be solved with one solution to rule them all. GUI is not one problem anymore, it is a family of problems.

As listed above, we now have many (nascent) solutions. These share a few common requirements:

  • A need to create (at least one) window
  • A need to render, often with GPU acceleration
  • A need to type-set complex text
  • A need to support accessibility (a11y) and internationalisation (i18n)

So lets talk a little about libraries supporting those things.

Windowing

winit is, perhaps, the de-facto Rust windowing/platform integration library. It handles window creation and input from at least keyboard, mouse and touchscreen devices. Making things more difficult is that there is room for considerable scope for feature creep in this project: Input Method Editors, theme (dark mode) detection, gamepad/joystick support, clipboard integration, overlay/pop-up layers, virtual-keyboard invocation, and probably much more.

Designing a cross-platform windowing and system-integration library is a hard problem, and though Winit already has a lot of success, it also has a lot of open issues and in-progress features. Winit's nearest equivalent is probably SDL2, which focussess mainly on game requirements (window creation and game controller support). Winit attempts to be significantly more (particularly regarding text input).

So, is Winit the right answer, or is attempting to solve all windowing and input requirements on all platforms for all types of applications a doomed prospect? My understanding is that Winit's biggest issue is a lack of maintainers with adequate time and knowledge of the various target platforms.

Thanks to raw-window-handle, there is a degree of flexibility between the renderer and window manager used, supporting some alternatives to Winit:

  • Tauri maintains TAO, a fork of winit with deeper GTK integration, most notably to support WRY via WebKitGTK.
  • Druid has its own druid-shell library designed for use with Piet (also supporting raw-window-handle)
  • Glazier is a new project (also by the Druid team) for a GUI-oriented alternative to Winit
  • We don't have to use Rust! Some of toolkits above, even those considered Rust toolkits like Slint and Relm4, make use of GTK or Qt for platform integration and rendering.

Rendering (backends)

Quite a few solutions are available for drawing to windows. Pick one, or like a few of the above toolkits, support several:

  • softbuffer facilitates pure-CPU rendering over a raw-window-handle
  • glutin supports the creation of an OpenGL context over a raw-window-handle, thus supporting glow (GL on Whatever; the library may also be used with SDL2) or Glium (an "elegant and safe OpenGL wrapper")
  • Several (low- and high-level) bindings are available to Vulkan, Metal and Direct3D; to my knowledge none of these are used directly by any significant Rust GUI toolkits
  • wgpu is "a cross-platform, safe, pure-rust graphics api. It runs natively on Vulkan, Metal, D3D12, D3D11, and OpenGLES; and on top of WebGPU on wasm." As a high-level, portarble and modern accelerated graphics API, it is (optionally) used by multiple Rust GUI toolkits (eGUI, Iced, KAS) despite adding a considerable layer of complexity between the toolkit renderer and the GPU.

Rendering (high-level)

Several Rust toolkits add their own high-level rendering libraries:

  • WRY is a rendering layer over platform-dependent WebView backends
  • Piet is Druid's rendering layer, leveraging system libraries especially for complex font support
  • Vello, formerly known as Piet-GPU, is a research project to construct a high-level GPU-accelerated rendering layer
  • Kurbo is used by Piet for drawing curves and paths
  • epaint is "a bare-bones 2D graphics library for turning simple 2D shapes and text into textured triangles"

Unaffiliated high-level rendering libraries used by GUI toolkits include:

  • Lyon, a path tessellation library (supports GPU-accelerated rendering of SVG content)
  • resvg is a high-quality CPU-rendering library for static SVGs using tiny-skia
  • tiny-skia is "a tiny Skia subset ported to Rust"
  • femtovg is an "antialiased 2D vector drawing library written in Rust"

Text and type-setting

To say that type-setting text is a complex problem is a gross understatement. Sub-problems for fonts include discovering system fonts and configuration, matching a "font family", use of "fallback" fonts for missing glyphs, glyph rendering, hinting, sub-pixel rendering and font synthesis. Sub-problems for text include breaking into level runs (i.e. same left/right direction), breaking these into same-font runs, shaping (or at least kerning), potentially multiple times where multiple fonts are required, line-breaking, hyphenating, indenting, dealing with ligatures, combining diacritics, navigation, and then there's still everyone's favourite: emojis. Then there is drawing highlighting and underlines, and if you're still looking for things to do, correctly justifying Arabic texts (by extending words) and gapping underlines around descenders.

Right, what do we have? We really have to thank Yevhenii Reizner (@RazrFalcon) for his efforts including ttf-parser, fontdb and rustybuzz (not to mention tiny-skia!). We have several libraries for rendering and caching glyphs including ab_glyph and Fontdue which perform basic rendering; more recently Swash has significantly raised the bar on the quality of typesetting and rendering.

The above are all low-level components. If you want a high-level interface able to type-set and render text, what libraries are out there?

  • glyph_brush is around four years old, covering rendering and basic line wrapping. Though this is enough for quite a few use-cases, it cannot handle complex text nor does it properly handle navigation (which is presumably why Iced still does not support multi-line text editing).
  • Piet (or more accurately piet-common), around three years old, supports at rich text, leveraging system libraries (Cairo on Linux).
  • KAS Text, around two years old and written by myself, supports (at least partially) rich text and bi-directional text, while missing a few features such as emojis and using a few crude hacks (indentation and font discovery).
  • femtovg, at least since around a year, supports moderately complex text.
  • COSMIC Text is a (very) recent project by System76 (creators of Pop! OS) for complex text layout in Rust, leveraging fontdb, rustybuzz, Swash and other Rust font libraries. It seems we can finally have high-quality pure-Rust type-setting. Thanks System76!
  • parley is a nascent project by Chad Brokaw (the author of Swash) for rich text layout, intended for usage with Piet

Accessibility and internationalisation

Admittedly this is not a topic I have researched. Accessibility for GUIs means at least supporting keyboard control (now common) and screen readers (less widely supported). Internationalisation, though a little more complicated than just string substitution, is less reliant on toolkit support. We now have a few libraries on these topics:

  • AccessKit, "UI accessibility infrastructure across platforms and programming languages"
  • TTS "provides a high-level Text-To-Speech (TTS) interface"
  • rust-i18n is "a crate for loading localized text from a set of YAML mapping files", inspired by ruby-i18n and Rails i18n
  • Fluent is "a Rust implemetnation of Project Fluent"
  • gettext-rs, "Safe bindings for gettext"