ecton.dev

Cushy v0.3: New widgets, offscreen capture, Plotters and Tokio integrations, and more

Yesterday I released Cushy v0.3. Cushy is an early-in-development GUI framework for Rust featuring a reactive data model. While this is a feature-packed release with a lot of cool features, I want to preface this post with a warning: Cushy is very much alpha software.

Why ~6 months since the last update?

This release took longer than I would have liked. I'm hoping to have a quicker release cadence moving forward. I am a solo developer, and several factors got in my way of getting this out the door sooner. The major one is that I bit off a little more than I could chew by trying to build a user's guide. Halfway through trying to stub out the list of widgets Cushy already has, I got bored of it and distracted myself with other shiny projects.

This also coincided with a period of time where I felt like I finally wanted to start building a game -- readers of this blog may remember that I was trying to do that before getting distracted writing a database. It took me entirely too long to finally figure out what sounded interesting for me to build, but the good news is that I'm excited by the concept I'm starting to work on.

Once I had a concept, I realized I needed a few more things in Cushy. When I went to update into the changelog and got lost briefly, it became clear I really needed to get a release done.

Now compatible with any* wgpu program

The new CushyWindow type is designed with the ideas found in the Encapsulating Graphics Work page from the wgpu wiki. It also provides functions for passing or simulating input to the Cushy window.

While I want to claim that anyone can use Cushy in their wgpu-based applications, I don't have any direct experience. I would love feedback from anyone who tries this out!

Offscreen rendering + recording

Cushy now supports offscreen rendering using the VirtualWindow type. At first glance, the type is very similar to CushyWindow. The main difference is that it keeps track of various state rather than expecting the operating system/winit to own that state.

To make capturing offscreen renderings easier, the VirtualRecorder type provides an easy wrapper around a VirtualWindow that can be used to test pixel values, simulate inputs and animations, record individual screen captures, or record animated pngs.

Here's the offscreen-apng.rs example in the repository showing how simple it is to record an animation:

use std::time::Duration;

use cushy::animation::easings::EaseInOutSine;
use cushy::widget::MakeWidget;
use cushy::window::VirtualRecorderError;
use figures::units::Px;
use figures::{Point, Size};

fn ui() -> impl MakeWidget {
    "Hello World".into_button().centered()
}

fn main() -> Result<(), VirtualRecorderError> {
    let mut recorder = ui().build_recorder().size(Size::new(320, 240)).finish()?;
    let initial_point = Point::new(Px::new(140), Px::new(150));
    recorder.set_cursor_position(initial_point);
    recorder.set_cursor_visible(true);
    recorder.refresh()?;
    let mut animation = recorder.record_animated_png(60);
    animation.animate_cursor_to(
        Point::new(Px::new(160), Px::new(120)),
        Duration::from_millis(250),
        EaseInOutSine,
    )?;
    animation.wait_for(Duration::from_millis(500))?;
    animation.animate_cursor_to(initial_point, Duration::from_millis(250), EaseInOutSine)?;
    animation.wait_for(Duration::from_millis(500))?;
    animation.write_to("examples/offscreen-apng.png")
}

That example produces this image:

offscreen-apng produced image

While it's not perfect, it's been a huge help in starting to create a user's guide with screenshots and animations that are always up-to-date and tested. Additionally, every image or animation on this blog post was generated using this API. The current limitation in getting perfect animations in captures is refactoring the animation system to allow external control or simulation of time.

Work-in-Progres: Cushy User's Guide

One of my big steps forward was getting a barebones framework for starting to develop a user's guide for Cushy. It's even more alpha than Cushy itself, and most pages are just stubs that need to be filled out. That being said, there is the obligatory "hello world" tutorial, an introduction to the reactive data model, and some overviews of how Cushy's APIs were designed.

Hello Ferris Example

Some pages, such as the Align widget page, are closer to my final vision of what all widget pages should contain.

New Widget: List

The List widget is Cushy's implementation of HTML's ordered and unordered lists. This is powered by Nominals, a new crate I developed that provides numbered indicators in every locale supported by the HTML/CSS specification. I now know more about number systems in more locales than I want to admit.

List widget example

New Widget: Disclose

The Disclose widget is a disclosure indicator paired with a widget that is being hidden or shown. Optionally, a label can be shown alongside the disclosure indicator.

Disclose widget example

New Widget: Menu

When trying to imagine what I need for a prototype of the game I'm wanting to start building, one "hack" to minimize the amount of user interfaces is to hide options behind contextual menus. In fact, the prototype that no one will ever see is going to be nearly 100% contextual menus. It was this inspiration that led me to extend the OverlayLayer to support absolute positioning and add the Menu widget.

The widget isn't completely finished, as it doesn't support keyboard accessibility yet. However, through mouse interaction is supports submenus, separators, and enabling/disabling items even while the menu is displayed.

Context menu example

Batching invalidations from background processes

One potential challenge when integrating background processes and user interfaces is ensuring that the user interface redraws at points in time that make sense. For example, if you know you're in the middle of updating several related properties, it would be nice to only redraw the window once after the final property is updated.

The newly introduced InvalidationBatch groups all invalidations that happen while executing a closure so that any affected windows are only notified after the closure finishes executing. The example explains how this works in more detail.

plotters integration

With this release, Kludgine and Cushy both have gained integration with plotters. This allows rendering a wide variety of graphs directly in a Canvas or other drawing context. An example is included in the repository.

plotters Sierpinksi Carpet example

tokio integration

Integrating async into Cushy isn't hard from a data-model perspective. The fundamental reactive types support both blocking and asynchronous approaches. The tricky part comes when trying to use APIs like tokio::spawn() within a callback being invoked from Cushy's code. This is because every window in Cushy is its own OS thread, and the animation system also uses its own thread.

With this release, enabling the tokio feature will ensure a tokio runtime is available in every context that Cushy runs your code. This is powered by an abstraction that hopefully can be used to integrate any async runtime. An example is included in the repository.

Give Cushy a Try

Cushy is still missing some important widgets, but it's quickly becoming a capable framework. My aim for Cushy is to make GUI development in Rust easy and painless, and I already think it's a joy to work with.

To give Cushy a try, I recommend checking out the examples folder in the repository. It's an ever-growing collection of examples that cover most major features, and exploring them can be a good way to learn how various APIs and widgets work. If you want to learn more about Cushy's design, the user's guide may have some additional information.

If you have any questions or suggestions, don't hesitate to open an issue or discussion on GitHub, ask on my Discord server, or ask me on Mastodon. Thank you for reading!