How can I cleanly modify this design to not rely on OOP-style inheritance?

I'm trying to design how I want my code in my Rust GUI library to be organized in a sane way. The way that I'd do it in OOP (which I'm explaining mainly to express my design goals and constraints) is to have an abstract base class called Widget that implements functionality common to all widgets (e.g. storing the ID of a widget, rendering a solid background color, caching preferred size for layout, and some other basic stubs that can be overwritten when needed (but not needed in most cases) later). Then, I'd add a PaneWidget: public Widget that overwrites some of the aforementioned behavior (e.g. overwrites the rendering function to render the background color and then child widgets). There is then a subclass of PaneWidget for each possible layout that overwrites an abstract function in PaneWidget to actually place the children, modifying a protected data structure in the process. Then, there's another subclass of Widget: DelegateWidget: public Widget that for the most part just trampolines all calls to a delegated widget member, but has facilities for catching events that that delegated widgetÂą and its children emit and processing them locally rather than simply passing them up the hierarchy as normal, thus allowing a user of my library to concretely subclass this widget to encapsulate a set of existing widgets as a single widget, and eventually their main program view will simply be a single widget in this way (of course, with other smaller ones inside it allowing for many levels of organization).

This seems like it'd work really well in an OOP language, but when I try to translate it to Rust's style -- trying to use composition instead of inerhitance, I find that I'm just rewriting inheritance myself in various ways, be it creating a Widget trait that functions like the abstract base class and having all sorts of duplicated functionality in each widget (e.g. event catching in each delegate, and facilities for storing the location of each child widget in the PaneWidget s, and recursing over the tree to find widgets as well²). I also tried to create a Widget struct that can enum between a SimpleWidgetDelegate, PaneWidgetDelegate, and DelegateWidgetDelegate each of which implements the non-shared functionality and then shared functionality is in Widget. This sounded great at first, but then I realized that it would have made the Widget struct absolutely massive and very convoluted (spaghetti) trying to deal with three different use cases, so I decided to have, as an implementation detail of the Widget struct, SimpleWidgetDelegateWrapper, PaneWidgetDelegateWrapper, and DelegateWidgetDelegateWrapper all of which implement the unique functionality with a shared interface via a trait and then the Widget struct has the shared functionality among all three, but…. “DelegateWidgetDelegateWrapper” it’s like I’m writing some horrifically convoluted enterprise Java – and I like Java, but even this is too much for me.

So, I ask, what is the proper Rusty way of doing this? I’m just reinventing OOP it seems, but spilling its insides out which is exactly what I’m trying to avoid by writing this GUI library (gtk is gobject, and gobject is OOP’s insides spilled out and it’s…. unpleasant go check out gtk-rs’s rustdoc if you don’t believe me).

1: The event system is to call spin() on the root widget frequently, which then calls it on all child widgets thus recursing over the hierarchy. Each spin() call returns 0 or more events, after checking for input, etc.

2: Since in Rust you can’t keep a reference to child widgets in the case of a DelegateWidget , I want a find(&self, id: &str) → Option<&dyn Widget> function (with mut too) on the Widget trait/base class, in almost all simple widgets (e.g. a label) it needs to check the current id against a stored id (which needs some code to store it) and then return either Some(self) or None depending on that check.

1 Like

GUI is probably one of the very few cases where OOP really is the best way to approach the problem.

Different paradigm invent different approach to the GUI programming. When it comes to the procedural, structured programming the immediate mode GUI was born. Dear ImGui (which also have C/Rust bindings) seems to be the most popular library in this category nowadays.

The stateful widget pattern is the result to applying the OO programming to the GUI. Since the OOP languages long dominated the user facing softwares, most gui libraries popular in the industry follows this pattern.

But as always, functional programming guys have their own approaches and the mainstream people regularly steal those ideas. When it comes to the GUI, one of the most successful results would be the Elm architecture. The React is a popular implementation of it in JS. There also are some Rust implementations including the iced.

Did you consider doing it through traits rather than structs?
Traits in rust can have: default impls, inheritance, and have generic impls for types which implement another trait. That's three ways to do inheritance.

Most of these problems like override behavior for one impl or allowing implementors to provide a specific function that gets used in a wider context where other features like caching should work just fine.

The problem with this method is that a trait can't contain default members and a default implementation -- they only support a default implementation. For example, if every widget has to keep track of its size and id, then at minimum there must be a size and id member in each widget. With subclassing, this is solved trivially by adding these members to the base class, but I don't know how to do this in Rust.

Yes. I don't think there is a sliver bullet there. But the best approach is going to depend on how many members you are talking about and how related they are.

For example if it is just size and ID, then it's probably reasonable to have getter methods on the trait without a default implementation and the implementers will be expected to have these two fields.

On the other hand, if it's a whole bunch of fields and they somewhat relate, then you could add a struct which contains them and has appropriate methods. Then the implementers of the trait just need to have one member which is of that struct and implement an accessor. This approach is especially preferred if you think you will add new members or functionality to that common struct, that way new members and functions can be added without affecting any of the impls.

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.