From 958a0ccf6bcb333a89be133b93f92e590d431bea Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Sun, 13 Apr 2025 13:07:37 -0600 Subject: write a third rust gui survey --- ...2025-04-13-2025-survey-of-rust-gui-libraries.md | 1393 ++++++++++++++++++++ 1 file changed, 1393 insertions(+) create mode 100644 _posts/2025-04-13-2025-survey-of-rust-gui-libraries.md (limited to '_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md') diff --git a/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md b/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md new file mode 100644 index 0000000..28eaf48 --- /dev/null +++ b/_posts/2025-04-13-2025-survey-of-rust-gui-libraries.md @@ -0,0 +1,1393 @@ +--- +title: A 2025 Survey of Rust GUI Libraries +--- + +I did this [in 2020](/_posts/2020-08-21-survey-of-rust-gui-libraries.md) and then again [in 2021](/_posts/2021-10-24-2021-survey-of-rust-gui-libraries.md), but I’m in the mood to look around again. +Let’s look through [Are We GUI Yet?](https://www.areweguiyet.com/) and see what’s up these days. + +The task today is to have a text label and an input field that can change the text in the label. +In React, for example, this is basically free: + +```jsx +const Demo = () => { + let [state, setState] = useState("Hello, world!"); + + return ( +
+

{state}

+ setState(evt.target.value)} /> +
+ ); +} +``` + +Choosing a task this simple means I can actually have a shot at completing this at a reasonable pace (although it took me two weeks), but it also means frameworks that prioritize scaling over initial setup will be at a disadvantage here. +If you’ve found this post looking for specific guidance for a project that’s going to be substantially more complicated than this, don’t assume my conclusions are valid in your context. + +A few other reasons my context may not match yours: + +- I’m developing this, like nearly all my personal projects, on Windows. + I’m in good company there — per the [2024 Stack Overflow developer survey](https://survey.stackoverflow.co/2024/technology/#1-operating-system), “Windows is the most popular operating system for developers, across both personal and professional use” — but for a handful of reasons Windows is an afterthought in a lot of open source development. + I find some of those reasons more compelling than others, but for GUI libraries in particular I think avoiding Windows is avoiding success, and if Windows support is lower on your roadmap than trend chasing AI bullshit, you are not serious. +- I am checking that the text label can be read out from Windows Narrator. + Screen reader accessibility is another frequent afterthought, and it’s not load-bearing for me personally but it’s a lot more important as a matter of principle (and potentially a matter of law, depending on the project). +- New in the 2025 version of this exercise: I will be using the [Windows Japanese IME](https://go.microsoft.com/fwlink/?linkid=2007440) to type in the kanji for Tokyo, `東京` (which on my US-layout-emulating keyboard I do with `toukyou`). + I don’t speak Japanese (although there are [more obscure IMEs](https://github.com/dec32/Ajemi) that I’m more interested in), and there are a lot of internationalization pieces that a minimal GUI library can reasonably decide to ignore, but if you’ve implemented text fields from scratch and you’re just appending keystrokes to a buffer then you have rejected compatibility with a lot of languages that are more complicated than that. + +I’m feeling very slightly more patient this time around, so I’m not going to give up instantly if something takes very slightly more setup than just `cargo add`, but I’ve got a lot of things to check, so that’s gotta be enough preamble. + +I’m writing this in linear order in parallel with my development, so it’s more of a journal than a reference and it probably reads best top to bottom, but if you want a TL;DR or you’re coming back for reference you can skip right to [the conclusion](#conclusion) or [the table](#the-table). + +## Azul + +[Azul](https://azul.rs/) is the first beneficiary of my newfound patience: you have to manually download the prebuilt `.dll` (via a link that doesn’t quite work, or directly off the GitHub release), and the last time I was here I balked at that request. +It’s not a great sign that the [getting started guide](https://azul.rs/guide/1.0.0-alpha1/GettingStarted) has samples for C++ and Python but not Rust, but there are [examples in the repo](https://github.com/fschutt/azul/tree/master/examples/rust) that aren’t too hard to follow. +However, the hello world sample doesn’t actually work if I copy and paste it into my `main.rs` - it looks like the API has changed somewhat since the latest release. + +There’s a bit of a theme of release versioning issues with Azul - following the guide appears to give me version `1.0.0-alpha4`, but the only Git tag is `1.0.0-alpha1` and it’s not clear what may have changed between alpha1 and alpha4. + +The broader issue, though, is that even if I download the `1.0.0-alpha1` examples off the GitHub release and try to run the same code myself, I am beset with `error LNK2019: unresolved external symbol __imp_AzCallbackInfo_getNodeIdOfRootDataset` and 47 other unresolved symbols. +I made an honest attempt to get Azul working, but it still doesn’t work. +I’ll see you in a couple years, Azul. + +## cacao + +[Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)) is some subset of the macOS API; it has [Rust bindings](https://github.com/ryanmcgrath/cacao) named Cacao. +I could have *sworn* that the cocoa/cacao wordplay had been done forever ago in the macOS space, but maybe I just couldn’t fucking read, because the only things I’m able to find are this crate. +Unsurprisingly, this does not work on Windows. + +## core-foundation + +[Core Foundation](https://en.wikipedia.org/wiki/Core_Foundation) is a different subset of the macOS API; it has [Rust bindings](https://github.com/servo/core-foundation-rs). +Also not useful from Windows. + +## Crux + +[Crux](https://github.com/redbadger/crux/) is new, and I find it really intriguing. +The idea of writing a shared library with business logic and then writing an ideally minimal native UI shell around it is also how Kotlin Multiplatform works (if you adopted it before Compose Multiplatform on iOS was out of alpha, at least), and I’ve been using that at my day job for a year and a half with only a handful of complaints. +The [initial project setup](https://redbadger.github.io/crux/getting_started/core.html) accurately describes itself as a sharp edge that needs better tooling, but it’s not miserable. + +[Manually defining](https://redbadger.github.io/crux/getting_started/core.html#the-interface-definitions) the entire shared library interface feels like it would get old fast, but this task is faster. +Unfortunately, though, I’m just now processing that Crux doesn’t actually support desktop GUI development, only mobile and web! +It’s very interesting that this exists, though, and if my aim today was actually mobile development I’d be very curious if actual Swift bindings solve my Kotlin Multiplatform woes (I suspect that they might). + +Hang on, though, this isn’t even a GUI library, the whole point is that you still use the native GUI library directly from each platform, and in fact several of the web examples use libraries that will be coming back later in this very list. +I’m not quite certain I agree with listing it on Are We GUI Yet? in the first place, although I guess the current design doesn’t have space for a separate section for non-GUI frameworks that would be useful for GUI applications. + +## Cushy + +[Cushy](https://crates.io/crates/cushy) is another new entrant - apparently there’s been a lot of movement in the space in the last four years. +The README code snippet has a funny but inconsequential mistake: + +```rust +// Create a dynamic usize. +let count = Dynamic::new(0_isize); +``` + +Maybe one day clippy will be able to detect comments that don’t actually match the code. + +The README example actually works (it’s a Christmas miracle!) but it spams stderr with a ton of eyebrow-raising Vulkan/DirectX 12 errors: + +```text +2025-04-03T04:20:03.810927Z ERROR wgpu_hal::auxil::dxgi::exception: ID3D12CommandQueue::ExecuteCommandLists: Using ClearRenderTargetView on Command List (0x0000026C511CF250:'Unnamed ID3D12GraphicsCommandList Object'): Resource state (0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]) of resource (0x0000026C510E4EB0:'Unnamed ID3D12Resource Object') (subresource: 0) is invalid for use as a render target. Expected State Bits (all): 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET, Actual State: 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT], Missing State: 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET. [ EXECUTION ERROR #538: INVALID_SUBRESOURCE_STATE] +2025-04-03T04:20:03.812586Z ERROR wgpu_hal::auxil::dxgi::exception: ID3D12CommandQueue::ExecuteCommandLists: Using IDXGISwapChain::Present on Command List (0x0000026C51057910:'Internal DXGI CommandList'): Resource state (0x4: D3D12_RESOURCE_STATE_RENDER_TARGET) of resource (0x0000026C510E4EB0:'Unnamed ID3D12Resource Object') (subresource: 0) is invalid for use as a PRESENT_SOURCE. Expected State Bits (all): 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT], Actual State: 0x4: D3D12_RESOURCE_STATE_RENDER_TARGET, Missing State: 0x0: D3D12_RESOURCE_STATE_[COMMON|PRESENT]. [ EXECUTION ERROR #538: INVALID_SUBRESOURCE_STATE] +``` + +It’s probably fine, though. +The actual application is about as simple as you’d hope it’d be: + +```rust +let text = Dynamic::new("Hello, world!".to_string()); +let label = text.map_each(|text| text.clone()); +let text_input = text.into_input(); +label.and(text_input).into_rows().run() +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-cushy.png) + +Unfortunately, Windows Narrator has no idea what’s inside this window. +Kanji input with the IME sorta works, though; I don’t get to see the `とうきょう` that my `toukyou` becomes as I type it, but when I press Return I do in fact get the kanji I was expecting. + +In IME jargon, turning `toukyou` into `とうきょう` is the job of the *composer* and turning `とうきょう` into `東京` is the job of the *converter*, so the composer step is hidden but the converter step works fine. + +I’m not sure whether Cushy has done anything in particular to accept IME results in its text input widgets or if the IME just dispatches the selected kanji as though they were typed directly, but I suspect it’s the latter. +I know Windows is pretty flexible with how it treats keyboard input — every curly quote in this blog post was hand-curled with [WinCompose](https://wincompose.info/) despite my Markdown renderer almost certainly doing smart quotes automatically — so maybe the IME result is just dispatched as though it’s a direct input of U+6771 CJK Unified Ideograph and then U+4EAC CJK Unified Ideograph. +(I had forgotten about Han unification; I’m curious how if at all using the Japanese IME causes the Japanese kanji to be displayed instead of the theoretically-equivalent hanzi/hanja, but I suspect the answer is that it doesn’t, and that scares me.) + +## CXX-Qt + +[CXX-Qt](https://kdab.github.io/cxx-qt/book/) is a framework for using the well-established Qt C++ GUI library from Rust. +I have been avoiding Qt for years, but it seems like it’s time to stop. + +It’s annoying that I have to make an account to install Qt, and it’s very annoying that they want my name and location before they’ll let me download it. +I was right to hate this the whole time. + +Their sample code will not run due to 1058 linker errors: + +```text + Creating library C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.lib and object C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.exp␍ +0245cd17b2ba3548-com_kdab_cxx_qt_demo_plugin_init.o : error LNK2019: unresolved external symbol "__declspec(dllimport) void __cdecl qRegisterStaticPluginFunction(struct QStaticPlugin)" (__imp_?qRegisterStaticPluginFunction@@YAXUQStaticPlugin@@@Z) referenced in function "public: __cdecl Staticcom_kdab_cxx_qt_demo_pluginPluginInstance::Staticcom_kdab_cxx_qt_demo_pluginPluginInstance(void)" (??0Staticcom_kdab_cxx_qt_demo_pluginPluginInstance@@QEAA@XZ)␍ +libcxx_qt_lib-c91f193d907e83ae.rlib(badec7f11aadc5df-qcoreapplication.o) : error LNK2001: unresolved external symbol "__declspec(dllimport) void __cdecl qt_assert(char const *,char const *,int)" (__imp_?qt_assert@@YAXPEBD0H@Z)␍ +<...> +libcxx_qt-464ae71fe424547a.rlib(0602fb52cb66f316-connection.o) : error LNK2019: unresolved external symbol "__declspec(dllimport) public: __cdecl QMetaObject::Connection::Connection(void)" (__imp_??0Connection@QMetaObject@@QEAA@XZ) referenced in function "class QMetaObject::Connection __cdecl rust::cxxqt1::qmetaobjectconnectionDefault(void)" (?qmetaobjectconnectionDefault@cxxqt1@rust@@YA?AVConnection@QMetaObject@@XZ)␍ +libcxx_qt-464ae71fe424547a.rlib(0602fb52cb66f316-connection.o) : error LNK2019: unresolved external symbol "__declspec(dllimport) public: static bool __cdecl QObject::disconnect(class QMetaObject::Connection const &)" (__imp_?disconnect@QObject@@SA_NAEBVConnection@QMetaObject@@@Z) referenced in function "bool __cdecl rust::cxxqt1::qmetaobjectconnectionDisconnect(class QMetaObject::Connection const &)" (?qmetaobjectconnectionDisconnect@cxxqt1@rust@@YA_NAEBVConnection@QMetaObject@@@Z)␍ +C:\Users\Melody\Projects\misc\2025\rust-gui-survey\target\debug\deps\cxx_qt_demo.exe : fatal error LNK1120: 1058 unresolved externals␍ +``` + +I suspect it’s the entire Qt standard library that’s missing. +The linker args all look reasonable, though — it’s looking for `C:/Qt/6.9.0/mingw_64/lib\libQt6Qml.a`, which aside from the mixed slashes is a real path that exists — so I’m not sure what the problem is. + +This is a complete bust. +There’s a section in the docs about building projects with CMake instead of cargo, but it says it’s optional, and it’s not like I’m desperate for more opportunities to use CMake, so I’m not going to try it. + +## Dioxus + +I think I remember [Dioxus](https://dioxuslabs.com/) as being one of the Rust frontend web dev frameworks; apparently they’ve branched out. + +Their tutorial involves building “*HotDog* - basically Tinder, but for dogs!”, and by that they mean the app lets you swipe through a pile of dog photos and then view the ones you’ve swiped whichever direction is good on. +That is not what I would expect “Tinder, but for dogs” to be, but maybe I don’t know what Tinder is. + +Apparently the way Dioxus supports desktop development is through WebView2/WebKitGTK, so they haven’t branched very far out. +I’m a little bit skeptical that Diet Electron is really the future, but given that [Electron](https://en.wikipedia.org/wiki/Electron_(software_framework)) is the present, maybe I need to take what I can get. +I also have some concerns about leaning this hard into cloning React — React hooks are a fascinating hack to almost build algebraic effects in JS, and it’s not like Rust really has algebraic effects, either, but maybe the real compilation step means they can do a little more magic and make it actually work reasonably. +At this scale, though, it’s hard to argue with the results: + +```rust +let mut text = use_signal(|| "Hello, world!".to_string()); +rsx! { + p { "{text}" } + input { + type: "text", + oninput: move |event| text.set(event.value()), + value: "{text}" + } +} +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-dioxus.png) + +Windows Narrator can even see what the text is, although it feels a little clumsy (it’s saying “Web content region” on its way into the body of the frame). +The IME works perfectly, too. +I guess there are benefits to letting Chrome-via-Edge-via-WebView2 be responsible for all the UI machinery. + +Dioxus did not invent the Diet Electron approach (that was Tauri, whose WebView2/WebKitGTK/macOS things library Dioxus builds its desktop support on), but the Rust-all-the-way-through approach feels like a way better idea than how Tauri works, which I’ll get into once I make it that far through my list. +If Diet Electron is really the best thing out there, I may be a little bit sad, but it’s probably possible to use Dioxus for real work without constantly being miserable, and that’s a new high water mark for this blog series. + +## Dominator + +[Dominator](https://github.com/Pauan/rust-dominator) is a Web-only UI crate, and unlike Dioxus it does not also offer a blessed desktop stack. + +## egui + +[egui](https://www.egui.rs/), and its framework `eframe`, have been around for a while. +The setup process has always been pretty straightforward, which is nice. +It’s pretty simple to use, too: + +```rust +egui::CentralPanel::default().show(ctx, |ui| { + ui.label(&self.label); + ui.text_edit_singleline(&mut self.label); +}); +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-egui.png) + +Windows Narrator can even see this text! +It feels a little janky trying to get Narrator into the text field, though, but maybe that’s just the ceiling of how well Windows Narrator can work, or maybe I’m just holding it wrong and there’s a way to get behavior that feels more intuitive that I just can’t think of. + +The default font doesn’t have hiragana or kanji coverage, though, and if I manually load a system font (which requires loading the bytes directly instead of just specifying a system font name, which is suboptimal but probably common), my Tab press to select `東京` as the kanji for `とうきょう` gets eaten by egui and I’m stuck with the hiragana forever. + +I prefer this default appearance to Cushy’s or Dioxus’s, although it’s probably possible to make anything look like anything if you try hard and believe in yourself. +I’m not sure I love immediate mode on principle, although at this scale it extremely doesn’t matter. + +
+Digression: “Immediate mode” and “retained mode” + +
+ +The fact that it’s possible to render your UI yourself and still have real accessibility support is definitely a good thing, and if it weren’t for the weird IME issues this would be perfect. +If you don’t need IME support and you want better styles out of the box or an immediate mode library you can plug into your existing game engine, egui seems like a perfectly reasonable choice, and that IME issue will probably get fixed eventually. + +## Floem + +[Floem](https://lap.dev/floem/) is the UI framework developed for [Lapce](https://lap.dev/lapce/), the cooler VSCode-but-in-Rust IDE. +I haven’t used Lapce in a while — several years ago when I last checked, its support for non-Rust languages was pretty weak, and I don’t actually prefer VSCode-style lightweight IDEs anyway (I pay for the JetBrains suite despite not doing enough personal development to justify that expense) — but everything that exists and is good enough for me once existed and was not. +It’s cool that it exists; let’s see if their UI framework is any good. + +Getting from zero to today’s sample is pretty straightforward: + +```rust +let label = create_rw_signal("Hello, world!".to_owned()); +( + dyn_view(move || label), + text_input(label), +).style(|s| s.flex_col()) +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-floem.png) + +Windows Narrator can’t see any of this text, and the IME won’t even start, I’m stuck with “toukyou” forever. + +Building layouts out of tuples feels a little bit weird — you can only have [up to 16](https://docs.rs/floem/0.2.0/floem/trait.IntoView.html#impl-IntoView-for-(A,+B,+C,+D,+E,+F,+G,+H,+I,+J,+K,+L,+M,+N,+O,+P)) widgets directly within a container at once, due to tuple generics in Rust being [still obviously incomplete after 11 years and counting](https://github.com/rust-lang/rfcs/issues/376) — but it’s probably better than Cushy’s `.and()`. +The complete lack of accessibility or IME support is the real issue, though. +Maybe one day they’ll fix it, but for now, this is no good. + +## fltk + +[FLTK](https://www.fltk.org/) is a C++ library with [Rust bindings](https://github.com/fltk-rs/fltk-rs). +Conveniently, the Rust bindings offer a `bundled` feature so I don’t have to figure out how to build FLTK from source on Windows. +Unfortunately, FLTK doesn’t appear to have an idea of widgets having an inherent size, and its whole layout subsystem leaves something to be desired: + +```rust +let app = App::default(); +let mut wind = Window::new(100, 100, 400, 300, "Hello from rust"); +let mut pack = Pack::default_fill().with_type(PackType::Vertical); +let mut label = Frame::default(); +label.set_label("Hello, world!"); +let mut input = Input::default(); +input.set_value("Hello, world!"); +input.set_callback(move |input| label.set_label(&input.value())); +input.set_trigger(CallbackTrigger::Changed); +pack.end(); +pack.auto_layout(); +wind.end(); +wind.show(); +app.run().unwrap(); +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-fltk.png) + +Windows Narrator has no idea what’s going on in here, but I did notice a two-year-stale [fltk-accesskit](https://github.com/fltk-rs/fltk-accesskit) repo under the `fltk-rs` org, and that works but it requires its own ugly setup (you have to pass it a redundant list of all your widgets, which is tough because I moved one of them into the callback for the other). +The IME works perfectly, which I wasn’t expecting. + +The main issue here is the layout subsystem, which appears to have no concept of widgets having intrinsic sizes. +You can manually position and size everything, or you can use one of the clumsy automatic layouts, but neither of those is particularly satisfying. +The fact that adding widgets to containers happens in implicit global state and you have to `.end()` a container to stop adding items to it is a little bit horrifying, I can’t lie. +I don’t like this API design one bit. + +## flutter_rust_bridge + +[Flutter](https://flutter.dev/) is a Google framework for cross-platform UI development, but you use it from [Dart](https://dart.dev/). +Dart has [`switch` statements](https://dart.dev/language/branches#switch-statements) and [`switch` expressions](https://dart.dev/language/branches#switch-expressions) with completely different syntax. +Dart sucks. +Maybe using Flutter from Rust doesn’t suck? + +It’s very funny to me that Flutter for Windows [claims](https://docs.flutter.dev/get-started/install/windows/desktop#hardware-requirements) you absolutely need a 1366x768 display; as an act of spite I will be disabling my primary display and only using my 1024x768 secondary display for the remainder of this section. + +Oh god this is cramped, I regret this already. + +Flutter hates my MSVC toolchain for some reason, and the Visual Studio installer does not want to be this narrow, but if I tab offscreen or move the window to the side I can still add the right components. +Apparently “MSVC v143 - VS 2022 C++ x64/x86 build tools” and “Windows 11 SDK” aren’t good enough, and Flutter absolutely insists on having specifically “MSVC v142 - VS 2019 C++ x64/x86 build tools” and “Windows 10 SDK”. + +If I install those, though, `flutter doctor` still doesn’t think I have them for some reason. +It seems like my issue, which `flutter doctor` failed to detect for some reason, was that I didn’t have the “Desktop development with C++” workload installed in Visual Studio. + +Getting flutter_rust_bridge set up is easy once Flutter itself is working, although having to run `flutter_rust_bridge_codegen generate` explicitly is no good; it can’t hook into the Flutter/Dart build process because [the only piece that anything can hook into](https://dart.dev/tools/build_runner) runs completely outside the Flutter/Dart build process. +However, what you actually get with flutter_rust_bridge is the opportunity to write your business logic in Rust and your UI in Dart still. +You can write your UI state in Rust if you want, but you still have to define your widgets in Dart: + +```rust +#[frb(ui_state)] +pub struct RustState { + pub label: String, +} + +impl RustState { + pub fn new() -> Self { + Self { + label: "Hello, world!".to_owned(), + base_state: Default::default(), + } + } + + #[frb(ui_mutation)] + pub fn set_label(&mut self, label: String) { + self.label = label; + } +} +``` + +```dart +void main() => runRustApp(body: body, state: RustState.new); + +Widget body(RustState state) { + return Column( + children: [ + Text(state.label), + TextField( + controller: TextEditingController(text: state.label), + onChanged: (text) => state.setLabel(label: text), + ), + ], + ); +} +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-flutter-rust-bridge.png) + +This doesn’t even actually work, though: typing happens in reverse in the input field, because the `TextEditingController` contains the caret position but keeps getting reset. +In pure Flutter, you’d probably solve this by moving the controller into the widget state, but it’s not obvious how to do that here since our widget state is being defined in Rust instead. +Windows Narrator appears to be able to see this text, at least, although it feels janky trying to move between the label and the input. +The IME sort of works, but for presumably the same `TextEditingController` reasons, the intermediate states aren’t being cleared as I type in the IME, so the actual value that I’ve entered with `toukyou` is `東京東京東京東京とうきょうとうきょうとうきょとうきょとうkyとうkyとうkとうkとうとうととt`. + +I’d get slightly better functionality if I moved this widget state to Flutter, I’m sure, but then there’d be no Rust code at all. +If I wanted to write my UIs in Flutter, I’d just do that. +If you want to write your UIs in Flutter and just some business logic in Rust, this might work alright for you, but that is not what I want. + +## Freya + +Per the README, [Freya](https://freyaui.dev/) is “a cross-platform GUI library for Rust powered by 🧬 Dioxus and 🎨 Skia.” +[Evidently](https://book.freyaui.dev/differences_with_dioxus.html), it takes the logic and structure of Dioxus and but renders everything itself instead of using Diet Electron. +I did grumble about the Diet Electron-hood of Dioxus, so maybe this is the exact thing I was hoping for all along. + +Freya’s latest stable release depends on the prior minor version of Dioxus, which may or may not have a slightly different `rsx!` macro (Dioxus 0.6 uses `rsx! {}` but Freya’s Dioxus 0.5 uses `rsx!()`, and I forget if that’s actually different or not), but this still looks a lot like our Dioxus code: + +```rust +let mut text = use_signal(|| "Hello, world!".to_string()); +rsx!( + label { "{text}" } + Input { + value: text.read().clone(), + onchange: move |value| text.set(value), + } +) +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-freya.png) + +Narrator appears to almost understand the structure of this window — it’s seeing that there’s a text edit, at least — but it can’t actually figure out what any of the text actually is. +The IME activates, but in the wrong place on screen, and neither the provisional kana nor the final kanji actually shows up in the text input. +Even my WinCompose shortcuts don’t work. + +There are some drawbacks to rendering things yourself instead of letting Chrome-via-Edge-via-WebView2 do it for you, it seems. +Regardless, it’s extremely cool that Dioxus is set up in a way that makes this possible, and it’s extremely cool that someone’s trying to do it. +It is not, however, at a point where I would recommend using it. + +## fui + +[FUI](https://github.com/marek-g/rust-fui) does not have a lot of high-level documentation I can use to figure out what to put here. + +The example in the README has drifted from the examples in the code, which I suppose is natural but which is rarely auspicious. + +Even less auspicious is that I can’t build `fui_system`: + +```text +qmake.stderr: Project ERROR: Cannot run compiler 'g++'. Output: +=================== +=================== +Maybe you forgot to setup the environment? +``` + +I can find no documentation about how to set up my environment, and if I really need `g++` then that’s not great. + +## GemGui + +The last commit to [GemGui](https://github.com/mmertama/gemgui-rs) was two years ago; that’s rarely a good sign. + +GemGui leans even further into Diet Electron by actually running your frontend on an HTTP server and by default just opening it in your regular browser. +There’s a setting to run it in its own application frame instead, but that appears to have a load-bearing dependency on Python being installed in a way that I currently don’t have it installed. +It also looks like you’re only really intended to define the UI elements in HTML and wire up the business logic in Rust. + +If I give this a venv and then manually ensure `python3` will do the right thing, that isn’t enough, because the Python dependency doesn’t actually work or something. +“Embed an HTTP server and then use a Web framework and open your server in the system default browser” is a very boring way to technically claim that you’re doing GUI development. + +This repo has four stars on GitHub. +Why is it even listed in Are We GUI Yet? + +## GPUI + +[GPUI](https://gpui.rs/) is the UI framework developed for [Zed](https://zed.dev/), the other VSCode-but-in-Rust IDE. +Are We GUI Yet? links to [someone squatting it on crates.io](https://crates.io/crates/gpui), which is silly. + +Remember this piece from the intro? + +> if Windows support is lower on your roadmap than trend chasing AI bullshit, you are not serious. + +Well, that’s Zed. +It’s a heavily-LLM-focused IDE with [no Windows support](https://github.com/zed-industries/zed/issues/5391). +GPUI appears to work alright on Windows, though. + +It looks like GPUI doesn’t have a basic text input widget, though; their [text input example](https://github.com/zed-industries/zed/blob/a2fbe82c42601221482e8422d7f8db5fee649b8e/crates/gpui/examples/input.rs) is over 700 lines of code. +It’s possible to shuffle that example around to at least get something that’ll meet the task I’m working on, though. + +```rust +// this isn’t even the bad part! +div() + .flex() + .key_context("TextInput") + .track_focus(&self.focus_handle(cx)) + .cursor(CursorStyle::IBeam) + .on_action(cx.listener(Self::backspace)) + .on_action(cx.listener(Self::delete)) + .on_action(cx.listener(Self::left)) + .on_action(cx.listener(Self::right)) + .on_action(cx.listener(Self::select_left)) + .on_action(cx.listener(Self::select_right)) + .on_action(cx.listener(Self::select_all)) + .on_action(cx.listener(Self::home)) + .on_action(cx.listener(Self::end)) + .on_action(cx.listener(Self::show_character_palette)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_move(cx.listener(Self::on_mouse_move)) + .bg(rgb(0xeeeeee)) + .line_height(px(30.)) + .text_size(px(24.)) + .child( + div() + .h(px(30. + 4. * 2.)) + .w_full() + .p(px(4.)) + .bg(white()) + .child(TextElement { + input: cx.entity().clone(), + }), + ) +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gpui.png) + +Narrator has no idea what’s going on inside this window (and it’s in good company there). +The IME works fine, though. + +I’m not sure you’re actually supposed to use GPUI at this stage; the documentation is spotty, the installation is janky, and the standard library is woefully inadequate. +But at least you can generate bad code way faster, and clearly that’s enough to get your Series A in. +Was inflicting Electron on us all by way of Atom not enough? + +## GTK 3 + +> UNMAINTAINED Rust bindings for the GTK+ 3 library (use gtk4 instead). + +OK then. + +## GTK 4 + +[GTK](https://gtk.org/) is the GNOME toolkit (although apparently that’s not actually what it stands for); it’s got [Rust bindings](https://gtk-rs.org/). +Conveniently, there are [specific installation instructions for Windows](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html). +It’s not clear whether or not just downloading the prebuilt binaries would work, and it’d certainly save a lot of time if they would, but I’m going to assume building the binaries myself will be more likely to work. +That only took five minutes, astonishingly. + +I find it a little bit counterintuitive that the single-line text widget is named `Entry`, but it does kinda rule that GTK’s property bindings mean I don’t have to keep any state at all: + +```rust +let label = Label::builder() + .label("Hello, world!") + .build(); + +let entry = Entry::builder() + .text("Hello, world!") + .build(); + +entry + .bind_property("text", &label, "label") + .build(); + +let r#box = Box::builder() + .orientation(Orientation::Vertical) + .build(); +r#box.append(&label); +r#box.append(&entry); + +let window = ApplicationWindow::builder() + .application(app) + .title("My GTK App") + .child(&r#box) + .build(); + +window.present(); +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gtk4.png) + +Not only can Narrator not see this text, it can’t even see the minimize/maximize/close buttons, and that’s not a failure state I had realized was possible. +The IME works fine. + +You may have noticed that this is not the idiomatic Windows window decoration; these minimize/maximize/close buttons are very GNOMEy, which makes sense but isn’t really what I want. +Maybe Adwaita will solve this? +For some reason, `gvsbuild build libadwaita librsvg` (as [recommended](https://gtk-rs.org/gtk4-rs/stable/latest/book/libadwaita.html#if-using-gvsbuild) by the `gtk-rs` book) is building fucking libsass; not the actually maintained stop-trying-to-make-Dart-happen rewrite dart-sass, but the old and busted last-commit-two-years-ago libsass. +rsvg also appears to depend on some yanked crate versions, which is concerning but not my problem today. +Well, Adwaita certainly makes it look different: + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-gtk4-adwaita.png) + +I’ve got way more drop shadow or something that’s extending the ShareX window capture way beyond the actual boundary of the window, and I’ve got dark mode and a purple emphasis color (which I’m not sure if they’re reading from somewhere or if the default was just picked out by someone with good taste). +It still doesn’t look like a Windows window, though, and Adwaita has not magically fixed the accessibility. + +Using GTK4 from Rust on Windows works, I guess, but I would have to lower my standards a lot to call this good enough. + +## Iced + +[Iced](https://iced.rs/) says it’s inspired by Elm, and that’s cool. +[Elm](https://elm-lang.org/) was your favorite programming language’s favorite programming language. +I miss it sometimes. + +```rust +struct State { + label: String, +} + +impl Default for State { + fn default() -> Self { + Self { label: "Hello, world!".to_owned() } + } +} + +impl State { + fn update(&mut self, message: Message) { + match message { + Message::SetLabel(label) => self.label = label + } + } + + fn view(&self) -> Column { + column![ + text(&self.label), + text_input("", &self.label) + .on_input(Message::SetLabel), + ] + } +} + +#[derive(Clone, Debug)] +enum Message { + SetLabel(String), +} +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-iced.png) + +Windows Narrator can’t see into this window, and the IME won’t even switch into active mode when I try to switch it into active mode, which may actually be a better failure state than having it just not work. + +Apparently System76 is all in on Iced for Pop!_OS’s COSMIC shell, so for their users’ sake I hope accessibility and IME support are actually happening at some point. + +## imgui + +[Dear ImGui](https://github.com/ocornut/imgui) is a minimalist C++ GUI library with [Rust bindings](https://github.com/imgui-rs/imgui-rs). +I will always think of it as being called `dear imgui,`, even though it hasn’t been canonically spelled that way since [2018](https://github.com/ocornut/imgui/commit/84d1ce39584161dfd027613b0defe305637f3867); the trailing comma is delightful in an [e e cummings](https://www.poetryfoundation.org/poetrymagazine/poems/49493/i-carry-your-heart-with-mei-carry-it-in) sort of way, and they should bring it back. + +Unfortunately, since Dear ImGui is designed to be plugged into an existing game engine, starting with it from scratch is a little bit annoying. +There’s a downright Linux-desktop-environment number of different ways to use imgui-rs, but apparently “The most tested platform/renderer combination is `imgui-glow-renderer` + `imgui-winit-support` + `winit`”, so that’s what I’ll use. +Or at least it would be what I’d use if the [examples](https://github.com/imgui-rs/imgui-examples) didn’t all use `imgui-glium-renderer` instead; that one’s [deprecated](https://github.com/imgui-rs/imgui-glium-renderer/issues/1) and it doesn’t appear to work with the latest version of `glium` but I can’t figure out how to get `glow` working instead. +Open source! + +```rust +// eliding the 160 lines of glue i copied and pasted without understanding +let mut label = "Hello, world!".to_string(); +support::simple_init(file!(), move |_, ui| { + ui.window("Hello world") + .size([300.0, 110.0], Condition::FirstUseEver) + .build(|| { + ui.text_wrapped(&label); + ui.input_text("Text", &mut label) + .build(); + }); +}); +``` + +![a screenshot of a text label and a text field both saying Hello, world! but the text field is labeled “Text”](/assets/2025-04-13-imgui.png) + +Windows Narrator can’t see this text, and the IME refuses to activate. + +The tiny window within the huge window is hilarious, and it makes sense if you’re actually doing game dev and there’s a game in the rest of the window, but I am not, and the massive white void is not something I would tolerate even in an application I was building solely for myself. +If I had a graphics stack picked out already because I was doing game dev, I might not mind the flexibility of supporting what feels like hundreds of different renderers and backends, but since that is not my current situation I very much do mind the flexibility. +This is what Sartre meant by being “condemned to freedom”: you have innumerable options available to you, but nobody can rescue you from the responsibility of deciding between them. + +
+digression: the irony you may have noticed + +
+ +## KAS + +[KAS](https://github.com/kas-gui/kas), the toolKit Abstraction System, is written from scratch in Rust. +The tutorials are a bit out of date — some things appear to have been moved around between when the tutorials were written and the most recent stable release — but the examples in the actual repo appear to work. +I’m not sure I quite understand how the state management is designed, but after a bit of fumbling I can at least complete the task: + +```rust +let tree = column![ + format_value!("{}"), + EditBox::instant_parser(|x: &String| x.clone(), SetLabel), +]; + +Adapt::new(tree, "Hello, world!".to_string()) + .on_message(|_, label, SetLabel(text)| *label = text) +``` + +![a screenshot of a text label and a text field both saying Hello, world!](/assets/2025-04-13-kas.png) + +Narrator can’t see this text, and the IME won’t activate. + +The part that confuses me the most is why the `EditBox` needs to be explicitly told that it’s a `String` that’s being edited; that seems like it shouldn’t require an explicit declaration. +Maybe if the tutorial were up to date it’d be easier to understand. +Regardless, it seems like this isn’t really ready for prime time yet. + +## kittest + +[kittest](https://github.com/rerun-io/kittest) is an AccessKit-driven testing library that only supports egui. +This is cool, but it’s not in the same category as the sort of thing I’m actually looking for. +If I maintained Are We GUI Yet? I’d probably split the list up into separate categories like [Are We Web Yet?](https://www.arewewebyet.org/) has. + +## Leptos + +[Leptos](https://github.com/leptos-rs/leptos) is a Web frontend framework that is for some reason on the Are We GUI Yet? list. +The README [has an FAQ about native GUIs](https://github.com/leptos-rs/leptos/tree/v0.7.8?tab=readme-ov-file#can-i-use-this-for-native-gui) that says it’d be possible to build native GUIs with Leptos but it’s not actually supported because it sent the whole codebase into generics hell when they tried it. + +## lvgl + +[LVGL](https://lvgl.io/), the Lightweight and Versatile Graphics Library, is a C GUI library designed for embedded use; it has [Rust bindings](https://github.com/lvgl/lv_binding_rust) that are `#![no_std]` compatible by default, which is neat if you need that. +Unfortunately, after copying around the C header files that define the configuration, I’m getting C compiler errors: + +```text + C:\Users\Melody\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lvgl-sys-0.6.2\vendor\lv_drivers\display\fbdev.c(13): fatal error C1083: Cannot open include file: 'unistd.h': No such file or directory +``` + +It seems like the LVGL configuration in the Rust binding samples is not designed to work on Windows, and if I figure out the flag to disable the framebuffer driver I get similar errors about not finding SDL, and if I disable SDL I get bindgen not being able to find libclang, and if I come back after fixing bindgen for a later library I get errors about the linker not finding SDL even though I already turned off SDL. +I wouldn’t be surprised if doing desktop development with LVGL is missing the point, though, and I’m holding it wrong by not cross compiling to some slightly cursed embedded Linux target. + +## Makepad + +[Makepad](https://github.com/makepad/makepad) is another novel Rust GUI framework. +They’re publishing versions to crates.io but not creating Git tags to match those versions, so it’s hard to find the examples that are supposed to work with the published crates, and it’s easier to just point the dependency right at the Git repo so the examples from their main branch will work. +They’ve got a macro DSL with no documentation I can find, but a bit of persistence is all it takes to turn the simplest example into something that works: + +```rust +live_design! { + import makepad_widgets::base::*; + import makepad_widgets::theme_desktop_dark::*; + App = {{App}} { + ui: { + main_window = { + body = { + flow: Down, + spacing: 10, + align: { x: 0.5, y: 0.5 }, + label1 =