winio 0.12.0-rc.7

Single-threaded async GUI runtime based on compio.
Documentation

Winio

Winio is a single-threaded asynchronous GUI runtime. The GUI part is powered by native backends, and it is compatible with compio. All IO requests could be issued in the same thread as GUI, without blocking the user interface!

Platform support

Backend Platform
Win32 Windows 10 1703+ Windows 10 1809+ (windows-dark-mode)
WinUI Windows 11 21H2+ WinUI (3) 1.0+ WinUI (3) 1.2+ (media)
Qt Qt 5.15+ Qt 6.0+
GTK GTK 4.14+ WebkitGtk 6 2.42+ (webview)
AppKit macOS 11.0+
UIKit iOS 13.0+ Mac Catalyst 13.1+
Android Android SDK 36+ Android NDK 27+

[!WARNING] On systems other than macOS, iOS, and Android, you have to select only one backend by enabling features. The default one is win32 for Windows and qt for others.

[!WARNING] WGPU canvas doesn't work well on some platforms:

  • iOS simulator
  • Mac Catalyst
  • Android simulator
  • Qt
  • GTK

Example

Read the example and learn more!

Backend Light Dark
Win32 Win32 Light Win32 Dark
WinUI 3 WinUI Light WinUI Dark
Qt 6 Qt Light Qt Dark
GTK 4 GTK Light GTK Dark
AppKit macOS Light macOS Dark
UIKit (Mac Catalyst) Catalyst Light Catalyst Dark
UIKit (iOS) iOS Light iOS Dark
Android View Android Light Android Dark

Quick start

[!NOTE] Alternatively, a fully configured template is available as a starting point.

Winio follows ELM-like design, inspired by yew and relm4. The application starts with a root Component:

use winio::prelude::*;

struct MainModel {
    window: Child<Window>,
}

enum MainMessage {
    Noop,
    Close,
}

impl Component for MainModel {
    type Error = Error;
    type Event = ();
    type Init<'a> = ();
    type Message = MainMessage;

    async fn init(_init: Self::Init<'_>, _sender: &ComponentSender<Self>) -> Result<Self> {
        // create & initialize the window
        init! {
            window: Window = (()) => {
                text: "Example",
                size: Size::new(800.0, 600.0),
            }
        }
        window.show()?;
        Ok(Self { window })
    }

    async fn start(&mut self, sender: &ComponentSender<Self>) -> ! {
        // listen to events
        start! {
            sender, default: MainMessage::Noop,
            self.window => {
                WindowEvent::Close => MainMessage::Close,
            }
        }
    }

    async fn update_children(&mut self) -> Result<bool> {
        // update the window
        update_children!(self.window)
    }

    async fn update(&mut self, message: Self::Message, sender: &ComponentSender<Self>) -> Result<bool> {
        // deal with custom messages
        match message {
            MainMessage::Noop => Ok(false),
            MainMessage::Close => {
                // the root component output stops the application
                sender.output(());
                // need not to call `render`
                Ok(false)
            }
        }
    }

    fn render(&mut self, _sender: &ComponentSender<Self>) -> Result<()> {
        let csize = self.window.client_size()?;
        // adjust layout and draw widgets here
        Ok(())
    }

    fn render_children(&mut self) -> Result<()> {
        self.window.render()
    }
}

It is recommended to set the lib name to "main" for convenience.

[lib]
name = "main"

All platforms except Android start with main. You should add the code below to main.rs:

#[cfg(not(target_os = "android"))]
fn main() -> winio::Result<()> {
    use main::MainModel;
    use winio::prelude::*;

    App::builder()
        .name("rs.compio.winio.example")
        .build()?
        .block_on(MainModel::run_until_event(()))
}

#[cfg(target_os = "android")]
fn main() {
    unreachable!("Android entry point is `android_main` in `android.rs`")
}

[!NOTE] WindowEvent::Close will never be emitted on iOS, and the application will exit if the window (Mac Catalyst) or the app (iOS) closes. block_on doesn't return in that case.

The Android entry point is the android_main method:

use winio::prelude::*;

use crate::MainModel;

#[unsafe(no_mangle)]
fn android_main(app: AndroidApp) {
    let app = App::builder()
        .android_app(app)
        .build()
        .expect("cannot create app");
    app.spawn(|| async {
        if let Err(e) = MainModel::run_until_event(()).await {
            compio_log::error!("App error: {e:?}");
        }
    })
}

[!NOTE]

  • android_main might be called multiple times, but the lifetime of each calling don't overlap.
  • android_main runs on a dedicate thread, while all code of winio execute on the main thread.
  • You have to do the following to create a complete Android project with winio.

Integrate into an Android app

To integrate the winio app into an Android app with id "rs.compio.winio.example", the main activity should inherit rs.compio.winio.Activity:

package rs.compio.winio.example;

import rs.compio.winio.Activity;

public class MainActivity extends Activity {
    static {
        System.loadLibrary("main");
    }
}

Add the following to the <activity> section of AndroidManifest.xml:

<meta-data android:name="android.app.lib_name" android:value="main" />

Put the project folder "android" beside the "src" folder of the rust project, and modify the "dependencyResolutionManagement" part of android/settings.gradle

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = findWinioUiAndroidProject()
            metadataSources.artifact()
        }
    }
}
// ...
import groovy.json.JsonSlurper

String findWinioUiAndroidProject() {
    def dependencyText = providers.exec {
        it.workingDir = new File("../")
        commandLine("cargo", "metadata", "--format-version", "1", "--manifest-path", "Cargo.toml")
    }.standardOutput.asText.get()

    def dependencyJson = new JsonSlurper().parseText(dependencyText)
    def manifestPath = file(dependencyJson.packages.find { it.name == "winio-ui-android" }.manifest_path)
    return new File(manifestPath.parentFile, "maven").path
}

and gradle/libs.versions.toml

[libraries]
# ...
winio = { module = "compio:winio", version = "latest.release" }

and android/app/build.gradle

dependencies {
    // ...

    implementation libs.winio
}

[
        Debug  : null,
        Profile: '--release',
        Release: '--release'
].each {
    def taskPostfix = it.key
    def profileMode = it.value
    tasks.configureEach { task ->
        if (task.name == "assemble$taskPostfix") {
            task.dependsOn "cargoBuild$taskPostfix"
        }
    }
    tasks.register("cargoBuild$taskPostfix", Exec) {
        workingDir "../.."
        environment 'CARGO_TERM_COLOR', 'always'

        def cmdArgs = [
                'cargo', 'ndk',
                '-o', 'android/app/src/main/jniLibs',
                '-t', 'arm64-v8a',
                '-t', 'armeabi-v7a',
                '-t', 'x86_64',
                'rustc',
                '--crate-type', 'cdylib', '--lib',
        ]
        if (profileMode != null) {
            cmdArgs.add(profileMode)
        }

        commandLine cmdArgs
    }
}