That’s a really good write up. I find model-view-update to be the most natural approach, and I’m glad they presented other UI designs beyond Widget+inheritance. The fact that constraints in Rust make that difficult is a good thing in my opinion.
Lots of people get tripped up on that when coming to a language with any such restrictions. This is the “but I used to be able to do X in my previous language.” Well sometimes X is only
possible because it’s an unsafe and un-analyzeable mess. I’m always of the opinion that constraints liberate and liberties constrain.
I’m still torn on the lack on some kind of inheritance when building a GUI, but I agree with that in general. And I think if I did have inheritance available to me, I know the exact traps I’d fall into.
Right now I’m finding a lot of success with egui and immediate mode, which certainly comes with some (fun?) restraints, and is about as far away from model-view-update as possible.
I’m not convinced that inheritance was the right approach for MVC frameworks to begin with. Having a widget-tree-node entry object that owns a delegate that handles events and drawing would work just as well and make it a lot clearer about what methods are hooks that you should override and which are generic functionality that you shouldn’t change.
I agree that MVU and ‘immediate mode’ models are a better fit (I have really enjoyed using Dear ImGui, in spite of the fact that it’s relatively immature), but each of these tends to build a widget tree internally and update it for caching so the implementers will hit some of the problems in the article.
The root problem, in my mind, is that UIs are intrinsically cyclic data structures. Data flows from model to UI when the model changes and from UI to model when the user does something. Rust’s ownership model can’t express this shape without unsafe (or some standard library things that wrap unsafe).
How does Warp stack against other toolkits when it comes to accessibility and system integration?
In my system settings I set colors, default fonts (with fallback and hinting settings), animation preferences (reduce/eliminate animations), disable overlay scrollbars, set buttons to include text where possible, enable dark mode, configure keyboard shortcuts, and sometimes enable a screen reader. Windows users can enable High Contrast Mode to force their preferred palettes. To what degree will this toolkit respect these settings?
On Linux: the only options I know of with decent system integration, accessibility, and some presence outside the Freedesktop.org ecosystem are are Qt, GTK, and the Web. Flutter falls flat, with outstanding WCAG level A blockers like functional keyboard navigation and basic levels of customization (e.g. disabling animation); relevant issues typically get de-prioritized. This is despite its massive funding and development efforts, so I’m not optimistic about other contenders.
AccessKit looks like a start for cross-platform interoperability between accessibility APIs. Until it’s ready, support for each platform’s accessibility APIs and screen readers will need to be implemented and tested. It’s a monumental task. I worry that releasing yet another inaccessible toolkit will merely increase the disability gap.
Having played with some G/TUI Rust frameworks, I can’t really say I’ve been bit by the lack of multiple inheritance. However, the memory model makes a lot of UI programming very… obtuse. Plugging in application state to both general application logic and user interaction (i.e. the UI layer) is really awkward. You really do need to mutate it from both directions sometimes. Worse, as is usually the case with GUI applications, the scope of what is mutable from where isn’t immediately obvious, and tends to change as new features are added in later versions.
One solution I found is to isolate all state-updating (and state-presenting) logic behind a message passing interface. That works, and keeps the borrow checker off my back, as only one module ever needs to hold an exclusive reference to it. The downside to this very ellegant design is that every program now contains an ad-hoc implementation of one of the hardest pieces of a GUI toolkit – the event handling loop. It’s basically like a deconstructed Win32 API. I think, it’s been like twenty years since I last looked at that.
The other solution I found was to chuckle and make all my state a handful of SQLite tables.
I’m fairly sure there’s bound to be a third, better option, because both of these essentially work by bypassing the Rust memory model altogether, but I’m still looking for it…
The downside to this very ellegant design is that every program now contains an ad-hoc implementation of one of the hardest pieces of a GUI toolkit – the event handling loop.
Props to you for realizing how touchy event loops are, I meet too many programmers who think they’re easy. :-P This approach sounds somewhat Elm-y, which is appealing. Do you think there’s a good way to wrap the event loop up in some sort of reusable abstraction, or is it too closely tied to the message and state update logic?
The other solution I found was to chuckle and make all my state a handful of SQLite tables.
Based on how nicely Rust’s memory models work with ECS systems in games, my hunch is that this might be closer to the solution than it looks. Games are another case that naively result in a giant graph of interconnected objects, but the ECS pattern basically turns it into an in-memory database optimized for iteration through rows. Someday I’ll learn enough about practical GUI programming to try to adapt this approach…
Do you think there’s a good way to wrap the event loop up in some sort of reusable abstraction, or is it too closely tied to the message and state update logic?
Oh, I expect there is. There are plenty of successful examples in related problem domains – axum, the web framework built over tokyo, is basically an HTTP-based abstraction over an asynchronous event loop.
I suspect there just hasn’t been that much of a need for it yet. Most Rust toolkits out there are either at a pretty early stage or, at best, pretty early-stage bindings to mature toolkits (e.g. ftlk-rs). I don’t know of any application that needs a well-tuned event loop implementation, most Rust programs with T/GUIs that I’ve seen are small enough – both in terms of feature set/complexity and in terms of function – that they can just pipe message over mpscs in the standard library.
I’m not sure it’s the right approach everywhere though. This isn’t something I came up with because I thought it was great per se, it was just easier to implement and, more importantly, easier to change over time. I was sort of familiar to this via Phonon from back on QNX, but without the proper message-passing runtime, this is probably way too slow to be useful for anything more than simple CRUD apps and the like.
Based on how nicely Rust’s memory models work with ECS systems in games, my hunch is that this might be closer to the solution than it looks.
Probably – I didn’t really come up with this deliberately per se, I just wrote a web tool as a learning exercise and for the first time, like, ever, I found that it was actually a lot easier to manage state this way vs. in a native toolkit.
Thing is, neither of these are particularly smart solutions in my book – I didn’t get to them by trying to improve upon a worse design, I got to them by trying to come up with a way to spend less time refactoring models and model access code and, respectively, by accident. If I squint, depending on how the light falls, they look like either cool technical solutions or workarounds. That makes me grumpy.
(I hope all this makes sense? I got COVID and my brain is a little slow these days and also not very reliable :-D)
I’m no master either, but generally they start off simple and then suddenly need a lot more finesse when they get even a little more complicated. Fix Your Timestep is a good example of complication with real-time deadlines. I don’t know as much about other types of event loop but other complications I can think of include dealing with event priority, handling longer-running tasks without blocking the event loop, dealing with fallible messages/errors in senders… it’s just an area where there’s a lot of horrible little edge cases that go into making something run smoothly in all circumstances. When you do get it right, event loops are great because it results in things running rock-solid, but it’s easy for subtle ordering changes or such to make things run alllllmost right but occasionally do terrible things.
My favorite event loop bug I’ve made in a game so far was implementing pausing. It paused fine but then looked at the real-time clock when you unpaused, so if you paused for 10 seconds and then unpaused it ran the physics updates as fast as possible for 10 in-game seconds to make up for the lost time. Funny, but unhelpful in an action game! Trivial to fix: on unpause, set the “last updated time” to the current wall clock time. But… touchy.
It’s weird to see a Rust UI post that doesn’t at least mention Druid and related projects. Raph Levien and others have spent a long time thinking about why Rust GUI is “hard” and the right data model to tackle the problem.
That’s a really good write up. I find model-view-update to be the most natural approach, and I’m glad they presented other UI designs beyond Widget+inheritance. The fact that constraints in Rust make that difficult is a good thing in my opinion.
Lots of people get tripped up on that when coming to a language with any such restrictions. This is the “but I used to be able to do X in my previous language.” Well sometimes X is only possible because it’s an unsafe and un-analyzeable mess. I’m always of the opinion that constraints liberate and liberties constrain.
I’m still torn on the lack on some kind of inheritance when building a GUI, but I agree with that in general. And I think if I did have inheritance available to me, I know the exact traps I’d fall into.
Right now I’m finding a lot of success with egui and immediate mode, which certainly comes with some (fun?) restraints, and is about as far away from model-view-update as possible.
I’m not convinced that inheritance was the right approach for MVC frameworks to begin with. Having a widget-tree-node entry object that owns a delegate that handles events and drawing would work just as well and make it a lot clearer about what methods are hooks that you should override and which are generic functionality that you shouldn’t change.
I agree that MVU and ‘immediate mode’ models are a better fit (I have really enjoyed using Dear ImGui, in spite of the fact that it’s relatively immature), but each of these tends to build a widget tree internally and update it for caching so the implementers will hit some of the problems in the article.
The root problem, in my mind, is that UIs are intrinsically cyclic data structures. Data flows from model to UI when the model changes and from UI to model when the user does something. Rust’s ownership model can’t express this shape without unsafe (or some standard library things that wrap unsafe).
How does Warp stack against other toolkits when it comes to accessibility and system integration?
In my system settings I set colors, default fonts (with fallback and hinting settings), animation preferences (reduce/eliminate animations), disable overlay scrollbars, set buttons to include text where possible, enable dark mode, configure keyboard shortcuts, and sometimes enable a screen reader. Windows users can enable High Contrast Mode to force their preferred palettes. To what degree will this toolkit respect these settings?
On Linux: the only options I know of with decent system integration, accessibility, and some presence outside the Freedesktop.org ecosystem are are Qt, GTK, and the Web. Flutter falls flat, with outstanding WCAG level A blockers like functional keyboard navigation and basic levels of customization (e.g. disabling animation); relevant issues typically get de-prioritized. This is despite its massive funding and development efforts, so I’m not optimistic about other contenders.
AccessKit looks like a start for cross-platform interoperability between accessibility APIs. Until it’s ready, support for each platform’s accessibility APIs and screen readers will need to be implemented and tested. It’s a monumental task. I worry that releasing yet another inaccessible toolkit will merely increase the disability gap.
POSSE note from https://seirdy.one/notes/2023/02/16/ui-toolkits-accessibility-gap/
Having played with some G/TUI Rust frameworks, I can’t really say I’ve been bit by the lack of multiple inheritance. However, the memory model makes a lot of UI programming very… obtuse. Plugging in application state to both general application logic and user interaction (i.e. the UI layer) is really awkward. You really do need to mutate it from both directions sometimes. Worse, as is usually the case with GUI applications, the scope of what is mutable from where isn’t immediately obvious, and tends to change as new features are added in later versions.
One solution I found is to isolate all state-updating (and state-presenting) logic behind a message passing interface. That works, and keeps the borrow checker off my back, as only one module ever needs to hold an exclusive reference to it. The downside to this very ellegant design is that every program now contains an ad-hoc implementation of one of the hardest pieces of a GUI toolkit – the event handling loop. It’s basically like a deconstructed Win32 API. I think, it’s been like twenty years since I last looked at that.
The other solution I found was to chuckle and make all my state a handful of SQLite tables.
I’m fairly sure there’s bound to be a third, better option, because both of these essentially work by bypassing the Rust memory model altogether, but I’m still looking for it…
Props to you for realizing how touchy event loops are, I meet too many programmers who think they’re easy. :-P This approach sounds somewhat Elm-y, which is appealing. Do you think there’s a good way to wrap the event loop up in some sort of reusable abstraction, or is it too closely tied to the message and state update logic?
Based on how nicely Rust’s memory models work with ECS systems in games, my hunch is that this might be closer to the solution than it looks. Games are another case that naively result in a giant graph of interconnected objects, but the ECS pattern basically turns it into an in-memory database optimized for iteration through rows. Someday I’ll learn enough about practical GUI programming to try to adapt this approach…
Oh, I expect there is. There are plenty of successful examples in related problem domains – axum, the web framework built over tokyo, is basically an HTTP-based abstraction over an asynchronous event loop.
I suspect there just hasn’t been that much of a need for it yet. Most Rust toolkits out there are either at a pretty early stage or, at best, pretty early-stage bindings to mature toolkits (e.g. ftlk-rs). I don’t know of any application that needs a well-tuned event loop implementation, most Rust programs with T/GUIs that I’ve seen are small enough – both in terms of feature set/complexity and in terms of function – that they can just pipe message over
mpsc
s in the standard library.I’m not sure it’s the right approach everywhere though. This isn’t something I came up with because I thought it was great per se, it was just easier to implement and, more importantly, easier to change over time. I was sort of familiar to this via Phonon from back on QNX, but without the proper message-passing runtime, this is probably way too slow to be useful for anything more than simple CRUD apps and the like.
Probably – I didn’t really come up with this deliberately per se, I just wrote a web tool as a learning exercise and for the first time, like, ever, I found that it was actually a lot easier to manage state this way vs. in a native toolkit.
Thing is, neither of these are particularly smart solutions in my book – I didn’t get to them by trying to improve upon a worse design, I got to them by trying to come up with a way to spend less time refactoring models and model access code and, respectively, by accident. If I squint, depending on how the light falls, they look like either cool technical solutions or workarounds. That makes me grumpy.
(I hope all this makes sense? I got COVID and my brain is a little slow these days and also not very reliable :-D)
I’m a novice at event loops. What makes them difficult aside for the “every parcel of work must be fast or goes back to the event loop”?
I’m no master either, but generally they start off simple and then suddenly need a lot more finesse when they get even a little more complicated. Fix Your Timestep is a good example of complication with real-time deadlines. I don’t know as much about other types of event loop but other complications I can think of include dealing with event priority, handling longer-running tasks without blocking the event loop, dealing with fallible messages/errors in senders… it’s just an area where there’s a lot of horrible little edge cases that go into making something run smoothly in all circumstances. When you do get it right, event loops are great because it results in things running rock-solid, but it’s easy for subtle ordering changes or such to make things run alllllmost right but occasionally do terrible things.
My favorite event loop bug I’ve made in a game so far was implementing pausing. It paused fine but then looked at the real-time clock when you unpaused, so if you paused for 10 seconds and then unpaused it ran the physics updates as fast as possible for 10 in-game seconds to make up for the lost time. Funny, but unhelpful in an action game! Trivial to fix: on unpause, set the “last updated time” to the current wall clock time. But… touchy.
Question: Why is building a UI in Rust so hard? Answer: Because the U in UI is unsafe. Done.
/s
You get the same result if you read U as “Useful” :D
It’s weird to see a Rust UI post that doesn’t at least mention Druid and related projects. Raph Levien and others have spent a long time thinking about why Rust GUI is “hard” and the right data model to tackle the problem.
Raph’s “Ergonomic APIs for hard problems” keynote at RustLab 2022 is also worth a look if you haven’t seen it (recording, slides).
Bevy is another good rust ECS but geared to games