Bring your app to all platform, learning once and applying anywhere
See the codeWaterUI is a cross-platform UI framework for Rust. You write views once, and each backend maps them onto whatever the platform actually uses: UIKit and AppKit on Apple, GTK4 on Linux. Where no native toolkit fits, Hydrolysis is the self-drawn renderer: it draws through Android's hardware-accelerated renderer (HWUI) on Android and on the GPU through Cherenkov elsewhere.
State is plain values. Put mutable state in a Binding, derive from it with Computed, and hand those to views. When a value changes, the views that read it update. There is no virtual tree to diff, and changing one string never rebuilds the subtree around it.
Install the water CLI. The installer downloads a prebuilt binary; nothing is
compiled:
# macOS and Linux
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/water-rs/cli/releases/latest/download/waterui-cli-installer.sh | sh
# Windows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/water-rs/cli/releases/latest/download/waterui-cli-installer.ps1 | iex"
Then create and run a project:
water create counter
cd counter
water run
water update upgrades the CLI in place, and water update --check reports the
newest release without installing it.
A WaterUI project is a plain Rust crate; the CLI keeps native projects out of your source tree and manages them on demand. src/lib.rs looks like this:
use waterui::app::App;
use waterui::prelude::*;
use waterui::preview;
#[preview]
fn main() -> impl View {
let count = Binding::i32(0);
vstack((
text("Hello, WaterUI!").size(28),
text!("Count: {count}"),
stepper("Count", &count),
))
.padding()
}
pub fn app(env: Environment) -> App {
App::new(main, env)
}
That's the whole entry point. The app crate depends on waterui and nothing else; FFI glue and backend projects are generated and owned by the CLI.
water run targets the current host by default. To run somewhere else:
water run --platform ios
water run --platform android
water run --platform linux
water doctor tells you what's missing from a toolchain, and water devices lists simulators and connected devices.
Any function marked #[preview] can be rendered to an image without launching the app:
water preview main --output preview.png
water preview main --frame 800x600 --output preview.png
The same surface drives water preview test for semantic interaction tests and water preview perf for GPU measurements.
The same project you iterate on is the one that ships: the CLI generates and manages the platform projects, and builds, signs and packages the app from the declarations in Water.toml:
water create my-app
cd my-app
water run --platform ios
Water.toml holds package metadata, permissions, and theming. The backend is chosen per command — Android builds use Hydrolysis, which you can select explicitly with --backend hydrolysis.
To give the app an icon, drop a square Icon.svg or Icon.png into assets/. The CLI renders every platform format from that one file: full-bleed squares for iOS, the rounded-rect shape for macOS, and adaptive icon layers for Android, so the artwork survives each platform's mask. New projects start with the WaterUI logo there until you replace it.
Binding<T> is mutable state, Computed<T> is derived state. Signal-aware APIs take either, and only their readers update:
use waterui::prelude::slider::slider;
use waterui::prelude::*;
fn progress_editor() -> impl View {
let value = Binding::f64(0.25);
let percent = value.map(|value| value * 100.0);
vstack((
slider("Progress", &value).range(0.0..=1.0),
text!("Progress: {percent:.0}%"),
))
}
Collections work the same way. Give List or ForEach a reactive collection of Identifiable items and membership changes are diffed by id:
use waterui::prelude::*;
use waterui::Identifiable;
#[derive(Clone, Identifiable)]
struct Contact {
#[id]
id: u64,
name: &'static str,
}
fn contacts() -> impl View {
let contacts = [
Contact { id: 1, name: "Alice Chen" },
Contact { id: 2, name: "Bob Smith" },
];
List::for_each(contacts, |contact| ListItem::new(text(contact.name)))
}
One thing to know early: watch replaces the subtree it wraps, losing any state inside it. It exists for intentional structural swaps. For routine updates, pass signals into components and let the framework do the precise thing.
| Target | Backend | Renders through |
|---|---|---|
| iOS and macOS | Apple | UIKit / AppKit |
| Android | Hydrolysis | Self-drawn, HWUI RenderNode display lists |
| Linux | GTK4 | GTK4 widgets |
| macOS, Linux, Windows, web | Hydrolysis | Self-drawn, GPU (Cherenkov) |
Pre-1.0. The API still moves, and we break it on purpose when a better shape is found. The Apple backend is the most complete; Hydrolysis, which also serves Android, is close behind; GTK4 is younger. If you hit a wall, an issue with a small reproduction is genuinely useful.
To run one from a checkout:
cargo install --locked --path cli
cd examples/gallery
water run --platform macos
core/ — View, Environment, layout contracts, reactive integrationcomponents/ — layouts, text, controls, forms, navigation, media, charts, and friendsbackends/ — the mainline backends: Apple (UIKit/AppKit) and Hydrolysis (self-drawn, and the Android backend)cli/ — waterui-cli, the water command and project generators (a workspace member)testing/ — semantic UI testing over the accessibility treeexamples/ — runnable applicationsTarget the dev branch; main is for releases. For anything substantial, open an issue first so the design can be discussed before you sink time into it. AI-assisted contributions are welcome as long as a human understands and reviews the result; fully autonomous pull requests are not accepted.
MIT or Apache 2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.
957 followers · starred Aug 2026
469 followers · starred Sep 2026
1,063 followers · starred Dec 2025
1,976 followers · starred Dec 2025
Bring your app to all platform, learning once and applying anywhere
See the codeWaterUI is a cross-platform UI framework for Rust. You write views once, and each backend maps them onto whatever the platform actually uses: UIKit and AppKit on Apple, GTK4 on Linux. Where no native toolkit fits, Hydrolysis is the self-drawn renderer: it draws through Android's hardware-accelerated renderer (HWUI) on Android and on the GPU through Cherenkov elsewhere.
State is plain values. Put mutable state in a Binding, derive from it with Computed, and hand those to views. When a value changes, the views that read it update. There is no virtual tree to diff, and changing one string never rebuilds the subtree around it.
Install the water CLI. The installer downloads a prebuilt binary; nothing is
compiled:
# macOS and Linux
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/water-rs/cli/releases/latest/download/waterui-cli-installer.sh | sh
# Windows
powershell -ExecutionPolicy Bypass -c "irm https://github.com/water-rs/cli/releases/latest/download/waterui-cli-installer.ps1 | iex"
Then create and run a project:
water create counter
cd counter
water run
water update upgrades the CLI in place, and water update --check reports the
newest release without installing it.
A WaterUI project is a plain Rust crate; the CLI keeps native projects out of your source tree and manages them on demand. src/lib.rs looks like this:
use waterui::app::App;
use waterui::prelude::*;
use waterui::preview;
#[preview]
fn main() -> impl View {
let count = Binding::i32(0);
vstack((
text("Hello, WaterUI!").size(28),
text!("Count: {count}"),
stepper("Count", &count),
))
.padding()
}
pub fn app(env: Environment) -> App {
App::new(main, env)
}
That's the whole entry point. The app crate depends on waterui and nothing else; FFI glue and backend projects are generated and owned by the CLI.
water run targets the current host by default. To run somewhere else:
water run --platform ios
water run --platform android
water run --platform linux
water doctor tells you what's missing from a toolchain, and water devices lists simulators and connected devices.
Any function marked #[preview] can be rendered to an image without launching the app:
water preview main --output preview.png
water preview main --frame 800x600 --output preview.png
The same surface drives water preview test for semantic interaction tests and water preview perf for GPU measurements.
The same project you iterate on is the one that ships: the CLI generates and manages the platform projects, and builds, signs and packages the app from the declarations in Water.toml:
water create my-app
cd my-app
water run --platform ios
Water.toml holds package metadata, permissions, and theming. The backend is chosen per command — Android builds use Hydrolysis, which you can select explicitly with --backend hydrolysis.
To give the app an icon, drop a square Icon.svg or Icon.png into assets/. The CLI renders every platform format from that one file: full-bleed squares for iOS, the rounded-rect shape for macOS, and adaptive icon layers for Android, so the artwork survives each platform's mask. New projects start with the WaterUI logo there until you replace it.
Binding<T> is mutable state, Computed<T> is derived state. Signal-aware APIs take either, and only their readers update:
use waterui::prelude::slider::slider;
use waterui::prelude::*;
fn progress_editor() -> impl View {
let value = Binding::f64(0.25);
let percent = value.map(|value| value * 100.0);
vstack((
slider("Progress", &value).range(0.0..=1.0),
text!("Progress: {percent:.0}%"),
))
}
Collections work the same way. Give List or ForEach a reactive collection of Identifiable items and membership changes are diffed by id:
use waterui::prelude::*;
use waterui::Identifiable;
#[derive(Clone, Identifiable)]
struct Contact {
#[id]
id: u64,
name: &'static str,
}
fn contacts() -> impl View {
let contacts = [
Contact { id: 1, name: "Alice Chen" },
Contact { id: 2, name: "Bob Smith" },
];
List::for_each(contacts, |contact| ListItem::new(text(contact.name)))
}
One thing to know early: watch replaces the subtree it wraps, losing any state inside it. It exists for intentional structural swaps. For routine updates, pass signals into components and let the framework do the precise thing.
| Target | Backend | Renders through |
|---|---|---|
| iOS and macOS | Apple | UIKit / AppKit |
| Android | Hydrolysis | Self-drawn, HWUI RenderNode display lists |
| Linux | GTK4 | GTK4 widgets |
| macOS, Linux, Windows, web | Hydrolysis | Self-drawn, GPU (Cherenkov) |
Pre-1.0. The API still moves, and we break it on purpose when a better shape is found. The Apple backend is the most complete; Hydrolysis, which also serves Android, is close behind; GTK4 is younger. If you hit a wall, an issue with a small reproduction is genuinely useful.
To run one from a checkout:
cargo install --locked --path cli
cd examples/gallery
water run --platform macos
core/ — View, Environment, layout contracts, reactive integrationcomponents/ — layouts, text, controls, forms, navigation, media, charts, and friendsbackends/ — the mainline backends: Apple (UIKit/AppKit) and Hydrolysis (self-drawn, and the Android backend)cli/ — waterui-cli, the water command and project generators (a workspace member)testing/ — semantic UI testing over the accessibility treeexamples/ — runnable applicationsTarget the dev branch; main is for releases. For anything substantial, open an issue first so the design can be discussed before you sink time into it. AI-assisted contributions are welcome as long as a human understands and reviews the result; fully autonomous pull requests are not accepted.
MIT or Apache 2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.
957 followers · starred Aug 2026
469 followers · starred Sep 2026
1,063 followers · starred Dec 2025
1,976 followers · starred Dec 2025