Request for feedback on the API of my GUI Library

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).

counter

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>

It seems like some of this could be accomplished with traits to make it easier to write custom widgets. In particular, the process_event function might be implemented in some general trait with a default implementation:

enum UserEvent<E> {
  /// Maybe a mouse click of a specific button at some position
  PredefinedEventOne{x: f64, y: f64, button: MouseButton}, 
  /// Maybe a keyboard input of a specific key
  PredefinedEventTwo(SomeKeyCodeEnum, KeypressState), 
  /// Some event defined by the user for their entire app 
  CustomEvent(E), 
}

trait WidgetTrait<E> {
  /// This version can not be dispatched on the trait object, 
  /// but the trait object is still object safe, and the process_event
  /// can be called on the original implementer
  fn process_event<E>(&mut self, event: UserEvent<E>) {
     .. no-op by default
  }

impl <E> WidgetTrait<E> for  CouterWidget {
 fn process_event<E>(&mut self, event: UserEvent<E>) {
    match event { 
       ... // accomplish whatever handling might be necessary
    }
  }

}

This makes it less complicated to match events without downcasting/dynamic typing. The generic type E could be some user-defined enum type that includes CounterEvents, etc. Or if the CounterWidget is intended to be supported as a default part of the API, it might be one of the predefined UserEvent variants.

This method does present an issue with object-safety.

As mentioned in the commented code provided, the process_event method can't be dispatched on the WidgetTrait dynamic object. This does not, however, make it impossible to do dynamic things, like having a Vec of WidgetTrait. For instance, it ought be workable to iterate through such a collection and do any downcast-matching there to call the process event on an unknown event.


Or:

As a final alternative, consider not trying to process unknown or dynamic events at the widget-level all. If a counter widget only ever generates a counter widget event, perhaps just redefine the code so that the is handled locally at the time it's generated by the counter. Then maybe just pass a message or some common data struct about the event to some other part of the code. This could make it easier for the user to opt-in to other optional trait methods, like process_click, process_focus, etc, assuming all the types of events the API could handle fall into a limited subset of UI interactions.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.