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