Kan jeg lage et native GUI i nettleseren?

Glenn Bitar

I bakgrunnen til mitt forrige blogginnlegg om caching nevnte jeg at jeg jobba med en eksperimentell, alternativ frontend til intern-appen vår Mittari. Som jeg skrev der, så har jeg tidligere brukt egui i Rust til å raskt utvikle GUI-er som løser utvikler-retta oppgaver. Dette var native-applikasjoner som kjørte på maskina til utviklerne. Da jeg holdt på med dette var det også tydelig at det var mulig å servere egui i nettleseren. Dette gjøres ved å kompilere til WebAssembly (WASM), som jeg har vært interessert i å prøve.

Demonstrasjon av en skrivebordsapplikasjon skrevet i egui.

I dette innlegget skal jeg vise hvordan man kan

  • bygge et GUI i Rust med egui
  • bruke trunk til å kompilere GUI-et til WASM og produsere HTML + JS + WASM man kan servere som en app

Om du ikke har hørt om WASM før, så er det en av måtene man kan kjøre kode i nettleseren på. Ved siden av HTML og CSS, så er det tradisjonelt kun JavaScript som har fått lov å kjøre i nettleseren. WASM er støtta i alle de store nettleserne fra år 2017, og fra da var det mulig å skrive klient-kode i alle programmeringsspråk kan kompileres til WASM. Dette inkluderer C, C++, Rust, Go, .NET-språk, med flere.

Illustrasjon av at WASM har gjort det mulig å skrive kode som kjører i nettleseren med mange programmeringsspråk.
WASM gjør det mulig for nettlesere å kjøre kode skrevet i f.eks. C, C++, Rust og Go.

egui

egui er en implementasjon av immediate mode GUI for Rust. Immediate mode er i kontrast til retained mode ved at elementene som defineres ikke finnes før koden som benytter seg av dem kjøres. Der man i retained mode definerer alle elementer ett sted, og interagerer med dem fra forskjellige steder rundt i applikasjonskoden, må man i immediate mode lage elementer underveis. Dette er kanskje enklere å demonstrere med et eksempel.

Skjermdump av eksempel-GUI med markering rundt en knapp med navn 'Increment'.
Kodesnutten under definerer alle elementene i dette GUI-et.

Her er koden for applikasjonen demonstrert i bildet og videoen over:

/// Kalles hver gang GUI-et skal tegnes
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
    egui::CentralPanel::default().show(ctx, |ui| {
        ui.heading("My egui Application");
        ui.horizontal(|ui| {
            ui.label("Your name: ");
            ui.text_edit_singleline(&mut self.name);
        });
        ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
        // På dette punktet i kjøringa av koden finnes ikke Increment-knappen.
        if ui.button("Increment").clicked() {
            // Nå som ui.button() er kalt så finnes knappen, og den er tegnet.
            // I tillegg vet vi at knappen ble trykket på, og vi kan utføre
            // den tilknyttede operasjonen.
            self.age += 1;
        }
        if self.name.is_empty() {
            ui.label("Please enter your name");
        }
        else {
            ui.label(format!("Hello '{}', age {}", self.name, self.age));
        }
    });
}

Denne modellen gjør at koden som påvirker eller leser tilstanden til elementene i GUI-et er meget nær der elementene defineres. Dette gjør igjen at applikasjonen din har mindre global tilstand som må håndteres og synkroniseres.

I og med at elementer ikke finnes før de kalles for hver iterasjon av tegne-loopen, gjør at elementer som defineres tidlig ikke kan vite om elementer som defineres seinere. Dette skaper en stor utfordring når det gjelder layout. F.eks., om man skal tegne en tabell, kan man ikke vite hvor bred man skal tegne en kolonne om man definerer alle elementene i kolonna etter kolonna blir definert. Da må man i mange tilfeller sette bredden på forhånd, og passe på at ingen av elementene er større enn dette. Det gjør at immediate mode ofte ikke brukes i forbruker-retta applikasjoner hvor design og estetikk er en stor del av produktet. Immediate mode er derimot veldig populært i spill eller «in-house» verktøy.

Få det på nett

Så, hvordan kan vi kjøre dette i nettleseren? Det som er fint med egui er at rammeverket i bunn og grunn bare produserer et sett av triangler med tekstur, og man kan derfor i teorien bruke egui overalt man kan tegne dette. Nettlesere vet hvordan man gjør dette, ved hjelp av f.eks. WebGL eller WebGPU, og det gjør det fullt mulig å få lik opplevelse som i skrivebordsapplikasjonen.

Standardrammeverket til egui, eframe, støtter wasm32-unknown-unknown, som er en target triplet som beskriver nettopp WASM, det binære formatet nettlesere støtter. Dette gjør at man kan skrive cargo build --target wasm32-unknown-unknown for å bygge en my_app.wasm fil, og denne kan man benytte seg av fra JavaScript, f.eks. slik:

WebAssembly.instantiateStreaming(fetch("my_app.wasm"), importObject).then(
  (obj) => {
    obj.instance.exports.my_function();
  },
);

Dersom WASM-koden gjør noe fancy, som f.eks. å allokere minne eller bruke funksjonalitet som krever at man kaller tilbake til JavaScript, må man gjerne sette opp en del for at WASM-koden skal fungere. Det er en del verktøy som hjelper til med å gjøre denne jobben:

  • wasm-bindgen genererer bindinger som gjør at man enkelt kan kalle JavaScript-funksjoner fra Rust og vice versa. Prosjektet inkluderer også:
    • js-sys som gir tilgang til funksjoner som man får fra JavaScript-standarden
    • web-sys som gir tilgang til funksjoner som man får via nettleseren
  • wasm-pack er et verktøy som gjør at du enkelt kan bunte sammen WASM og JavaScript hjelpekode til NPM-pakker eller for å enkelt servere dette på nett

I tillegg har vi det som heter trunk som gjør det til en enkel prosess å lage apper fra Rust-prosjekter, og lager konfigurasjoner for wasm-bindgen og for wasm-pack. Man kan også hvordan appen ser ut i lokal utvikling ved å kjøre trunk serve. trunk build vil generere filer i en dist/-katalog som man kan servere med en helt vanlig web-server.

For å slippe å sette opp konfigurasjonen til noen av verktøyene over, finnes eframe template, som er et fint utgangspunkt for å komme i gang med et prosjekt som bygger et egui GUI som kjører både native og i browser.

Skrivebordsapplikasjon og web-versjon side om side.