Interactive 2D applications with Carboxyl and Elmesque |
2015-04-23 |
Since my introductory post on Carboxyl, I have been working on abstracting windowing APIs to build interactive applications. The result can be found on GitHub. This post discusses how to create interactive 2D applications using this windowing API on top of Carboxyl and Elmesque, a port of Elm's graphics API.
At its core the crate provides implementations of the following trait:
pub trait StreamingWindow {
fn position(&self) -> Cell<(i32, i32)>;
fn size(&self) -> Cell<(u32, u32)>;
fn buttons(&self) -> Stream<ButtonEvent>;
fn text(&self) -> Stream<String>;
fn cursor(&self) -> Cell<(f64, f64)>;
fn wheel(&self) -> Cell<(f64, f64)>;
fn focus(&self) -> Cell<bool>;
}
This is essentially an abstraction of all IO events associated with an open
window. I tried to make a clear conceptual distinction between discretely
occurring events wrapped in a Stream
and values that vary over time as a
Cell
. So while the button press/release events received by a window are
independent singular events, the cursor position always has a current value
that is tracked in the cell.
(Although, it should be noted here, that in Carboxyl everything is discrete on the most fundamental level, but this is only a limitation of the concrete implementation. The library still receives discrete mouse-move events, but translates them into a cell. I have actually been studying an alternative way to implement FRP, described in Conal Elliott's paper “Push-Pull FRP”. This would allow cells with actual piece-wise non-constant continuous behaviour. Maybe I could adopt this for Carboxyl, but this would require a fundamental redesign of the internals and I don't know yet how well the concept translates to a strict imperative language like Rust.)
Thanks to the recent development in Piston's windowing abstraction facilities, it was possible to implement this generically over the type of window. So now you can plug-in a Glutin, SDL2 or GLFW window and the interface is satisfied.
Here's how you set it up for Glutin, for example:
let glutin_window = Rc::new(RefCell::new(GlutinWindow::new(
OpenGL::_3_2,
WindowSettings::new("Title", (1920, 1080))
)));
let window = WindowWrapper::new(glutin_window.clone(), 10_000_000);
The event handling logic of your application can then simply take an
implementation of StreamingWindow
and map that to a Cell<Model>
, where
Model
is the type of whatever underlying model your application has.
fn app_logic<W: StreamingWindow>(window: &W) -> Cell<Model> { ... }
Now you have a time-varying model. Great. So how do you actually display it in the window? I have been struggling with this question, as this had to be done with side-effects so far. Fortunately, Mitchell Nordine has ported Elm's purely functional graphics API to Rust. The port is called elmesque.
It turns out that Carboxyl and Elmesque work together pretty much perfectly.
Given the Model
defined above you can now write a view function like this:
fn view(model: Model) -> Form { ... }
It maps the Model you created earlier to an Elmesque form. In the main function you can then declare the displayed scene as follows,
let scene = lift!(view, &app_logic(&window));
where window
is the WindowWrapper
defined earlier.
With minimal imperative glue code in the main function we can then run this as
an application. (I might add convenience functions for this at some point.) The
carboxyl_window
repository contains two example
applications.
simple.rs
is a very simple application with no complicated state. It merely
displays a circle that follows the mouse cursor and displays the current state
of the mouse wheel, which also affects the circle's color.
drag.rs
is more involed: it is a modular implementation of mouse drag & drop.
Essentially, you can spawn orange squares by pressing space and drag them
around with the mouse cursor. This is a use case easy to mess up using
imperative event handling mechanisms because of all the state updates involved.
This example showcases a lot of Carboxyl's API and how it is used to build more
complex logic.
If something in these examples can be improved or the code is hard to follow, please open an issue on GitHub. They are intended to be instructive to get people started creating functional interactive applications in Rust.
One last thing I should note is that due to Carboxyl's usage of weak pointers this does not work on 1.0-beta right now. I don't think there's an easier way to fix this at the moment than simply waiting for weak pointers to stabilize. (I hope this happens soon.)
I am pretty happy with the current development and am also glad to see that Conrod has moved to Elmesque. I'm curious to see how one could interface Conrod's event handling with Carboxyl to build a reactive GUI framework. Apart from that, the next things to do are testing in practical applications, implementing more features and probably some critical review of the current API.