I'm currently working on a Rust GUI library that I believe has various strengths, however one part that I'm a bit less sure about is my API for the users of my library. I have a working (albeit very basic) GUI library right now, and a sample/test application written in it. Below is the source code to this sample application -- I'm hoping to get feedback on the API that it shows. I hope that how it will be extended later for more features is clear. Note that the names of some types, modules, etc. are renamed as if the library was called "mygui" to make it less likely that I'll find the real name taken when I go to register a domain, get crates.io names, etc. As is likely very clear from this code, the library is in a very early state right now -- although I think that this is the right time to get feedback like this because now the cost of making significant changes is much lower than in a more fleshed out library.
Before the code, here's a demo of what the program looks like running. Please excuse the poor FPS and gif dithering (I wanted to keep the file size small and the forum sofrtware wouldn't let me upload a proper video file even though the file is actually smaller than the gid -- the GUI is perfectly capable of running at well over 60Hz without dithering artifacts -- this video is here just to show what the program does).
And now, the code:
src/main.rs:
use mygui_core::event::{SystemEvent, UserEvent, UserEvents};
use mygui_core::mygui_static::init_mygui;
use mygui_core::theme::Theme;
use mygui_core::Widget;
use mygui_desktop::Window;
use mygui_proc::{view, Widget}; //not part of mygui_core because technically one could use the library without it so it's best if it's an optional dependency
use mygui_widgets::Label; //same reason as mygui_proc for not being in the core library
//When an event takes place (e.g. a button press) callbacks are implemented via events that simply describe what happened and don't implement the handling themself
#[derive(Copy, Clone)]
struct CounterEvent {
delta: i64,
}
impl CounterEvent {
#[inline]
pub fn new(delta: i64) -> Self {
CounterEvent { delta }
}
}
//This tags a type as an event for said callbacks, and UserEvent has an impl on it for downcasting which will be useful later
impl UserEvent for CounterEvent {}
//The model for how to encapsulate functional chunks of widgets together is to create one's own widget
//there's a Widget trait that's implemented for every widget
#[derive(Widget)]
struct CounterWidget {
id: Option<String>, //every widget must have an ID, no good way to automate this for users (see https://users.rust-lang.org/t/how-can-i-cleanly-modify-this-design-to-not-rely-on-oop-style-inheritance/66639)
delegate: Box<dyn Widget>, //the derived Widget implementation delegates most things to a delegate, think of this kindof like extending this widget
count: i64, //our widget has some of its own application state
}
impl CounterWidget {
pub fn new(id: Option<String>) -> Self {
Self {
id: id.into(),
delegate: view!("src/view.xml"), //view! macro evaluates to an expression that builds, as efficiently as possible, a widget tree from the specified xml file
count: 0,
}
}
//here is the event handling that was talked about earlier -- this function is called by the derived Widget implementation
//I'd like to make this part a bit cleaner to allow users to not muck around with event lists, etc. but I'm unsure how
fn process_event(&mut self, event: Box<dyn UserEvent>) -> UserEvents { //UserEvents is a type that is a thin wrapper around Vec<Box<dyn UserEvent>>
match event.downcast::<CounterEvent>() {
Ok(event) => { //handle events that are ours
self.count += event.delta;
self.delegate
.find_mut_downcast::<Label>("counter-label") //panics if the widget ID is invalid or for a different type, non-panicking versions exist
.set_text(format!("{}", self.count));
UserEvents::new() //return an empty user events list as we're not passing any events to the parent of this widget
}
Err(original) => { //pass along any even that isn't ours
let mut events = UserEvents::new();
events.add_boxed(original);
events
}
}
}
}
fn main() {
init_mygui(Theme::mygui_light()); //initialize theme stored in a static once cell, as it is needed all over the code
//one could theoretically have a WasmCanvas::new(…) or something like that -- Windowis in its own crate, mygui_desktop, to support this type of modularity. Window types that use different drawing backends (e.g. CPU rendering, GPU acceleration via some particular GPU API, JS canvas tools) are also possible
let mut window = Window::new("Counter Example", 800, 600, CounterWidget::new(None))
.expect("Failed to create Mygui window.");
//spin() takes over the thread until we're done
window.spin().expect("Failed to spin Mygui window.");
}
src/view.xml:
<mygui xmlns="mygui"> <!-- proper namespace url is todo -->
<!-- These evaluate to use declarations that apply in a block expression around the widget tree creation code-->
<use>mygui_widgets::{Button, CenterBox, FlexBox, Label}</use>
<use>mygui_2d::math::Orientation</use>
<!-- There must be exactly one root widget -->
<!-- Any widget that implements the ProcConstructableWidget trait can be added in an xml file -->
<widget type="CenterBox">
<widget type="FlexBox" orientation="Orientation::Vertical">
<widget type="Label" id="counter-label" text='"0"'/>
<widget type="FlexBox" orientation="Orientation::Horizontal">
<widget type="Button" on_click="CounterEvent::new(1)">
<widget type="Label" text='"+"'/> <!-- Attributes' values are any Rust expression, and in Rust a string literal expression must have quotation marks around it -->
</widget>
<widget type="Button" on_click="CounterEvent::new(-1)"> <!-- as arbitrary Rust expressions, the above use tags can be used -->
<widget type="Label" text='"-"'/> <!-- the use tags also apply to the type attributes, but these are a Rust type and not a Rust expression -->
</widget>
</widget>
</widget>
</widget>
</mygui>