| Crates.io | waterui-core |
| lib.rs | waterui-core |
| version | 0.2.0 |
| created_at | 2025-09-07 14:21:58.43815+00 |
| updated_at | 2025-12-13 19:48:47.273179+00 |
| description | Core functionality for the WaterUI framework |
| homepage | |
| repository | https://github.com/water-rs/waterui |
| max_upload_size | |
| id | 1828170 |
| size | 156,008 |
The foundational crate providing essential building blocks for the WaterUI cross-platform reactive UI framework.
waterui-core establishes the architectural foundation for WaterUI applications, enabling declarative, reactive user interfaces that render to native platform widgets. This crate provides the core abstractions used throughout the framework: the View trait for composable UI components, the Environment for type-safe context propagation, reactive primitives powered by the nami library, and type erasure utilities for dynamic composition.
Unlike traditional immediate-mode or retained-mode frameworks, WaterUI uses a reactive composition model where views automatically update when reactive state changes, and the entire view tree is transformed into native platform widgets (UIKit/AppKit on Apple platforms, Android View on Android) rather than custom rendering.
This crate is no_std compatible (with allocation) and works consistently across desktop, mobile, web, and embedded environments.
Add to your Cargo.toml:
[dependencies]
waterui-core = "0.1.0"
For most applications, use the main waterui crate which re-exports all core functionality along with component libraries.
use waterui_core::{View, Environment, binding, Binding};
// Define a custom view
fn counter(count: Binding<i32>) -> impl View {
Dynamic::watch(count, |value| {
format!("Count: {}", value)
})
}
// Create an application environment
fn init() -> Environment {
Environment::new()
}
// Define the root view
fn main() -> impl View {
let count = binding(0);
counter(count)
}
The View trait is the foundation of all UI components in WaterUI. It defines a single method that transforms the view into its rendered representation:
pub trait View: 'static {
fn body(self, env: &Environment) -> impl View;
}
Views compose recursively - a view's body method returns another view, allowing complex UIs to be built from simple primitives. The framework handles the recursion, eventually reaching "raw views" (like Text, Button) that map directly to native widgets.
Implementing View for custom types:
use waterui_core::{View, Environment};
struct Greeting {
name: String,
}
impl View for Greeting {
fn body(self, _env: &Environment) -> impl View {
format!("Hello, {}!", self.name)
}
}
Many standard types implement View automatically:
&'static str, String, Cow<'static, str> - render as text() - empty viewOption<V> - renders Some(view) or nothingResult<V, E> - renders either the success or error viewFn() -> impl View - lazy view constructionThe Environment is a type-indexed store that propagates context through the view hierarchy without explicit parameter passing:
use waterui_core::Environment;
#[derive(Clone)]
struct AppConfig {
api_url: String,
}
let env = Environment::new()
.with(AppConfig {
api_url: "https://api.example.com".to_string(),
});
// Later, in a view:
use waterui_core::env::use_env;
use waterui_core::extract::Use;
let config_view = use_env(|Use(config): Use<AppConfig>| {
format!("API: {}", config.api_url)
});
The environment supports:
'static typePlugin traitRc for child environmentsAnyView enables storing different view types in homogeneous collections:
use waterui_core::AnyView;
let views: Vec<AnyView> = vec![
AnyView::new("Hello"),
AnyView::new(42.to_string()),
AnyView::new(()),
];
Type erasure is essential for dynamic UIs where the concrete view type isn't known at compile time. AnyView automatically unwraps nested erasure to avoid performance overhead.
WaterUI integrates the nami reactive system for fine-grained updates:
use waterui_core::{binding, Binding, Dynamic};
// Create reactive state
let count: Binding<i32> = binding(0);
// Create a view that watches the state
let counter_view = Dynamic::watch(count.clone(), |value| {
format!("Count: {}", value)
});
// Updating the binding automatically updates the view
count.set(5);
Key reactive types (re-exported from nami):
Binding<T> - Mutable reactive stateComputed<T> - Derived reactive valuesSignal<T> - Read-only reactive valuesSignalExt - Extension methods for all reactive typesThe Dynamic component bridges reactive state to the view system. When a watched value changes, the view automatically re-renders with the new data.
Native views are leaf components that map directly to platform widgets. The NativeView trait marks types that should be handled by the platform backend:
use waterui_core::{NativeView, layout::StretchAxis};
struct CustomWidget;
impl NativeView for CustomWidget {
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Horizontal
}
}
The raw_view! macro simplifies creating native views:
raw_view!(Spacer, StretchAxis::MainAxis);
raw_view!(Divider, StretchAxis::CrossAxis);
use waterui_core::{View, Environment, binding, Binding, Dynamic};
struct Toggle {
label: String,
is_on: Binding<bool>,
}
impl Toggle {
fn new(label: impl Into<String>) -> (Binding<bool>, Self) {
let is_on = binding(false);
(is_on.clone(), Self {
label: label.into(),
is_on,
})
}
}
impl View for Toggle {
fn body(self, _env: &Environment) -> impl View {
Dynamic::watch(self.is_on, move |value| {
format!("{}: {}", self.label, if value { "ON" } else { "OFF" })
})
}
}
use waterui_core::{View, Environment, env::use_env, extract::Use};
#[derive(Clone, Debug)]
struct Theme {
primary_color: String,
}
fn themed_view() -> impl View {
use_env(|Use(theme): Use<Theme>| {
format!("Using theme color: {}", theme.primary_color)
})
}
fn init() -> Environment {
Environment::new().with(Theme {
primary_color: "#007AFF".to_string(),
})
}
use waterui_core::{binding, Binding, Computed, Dynamic, SignalExt};
let count = binding(0);
let doubled: Computed<i32> = count.map(|n| n * 2);
let view = Dynamic::watch(doubled, |value| {
format!("Doubled: {}", value)
});
use waterui_core::{plugin::Plugin, Environment};
struct AnalyticsPlugin {
app_id: String,
}
impl Plugin for AnalyticsPlugin {
fn install(self, env: &mut Environment) {
env.insert(self);
}
}
let mut env = Environment::new();
AnalyticsPlugin {
app_id: "my-app".to_string(),
}.install(&mut env);
View - The fundamental UI component traitIntoView - Convert types into viewsTupleViews - Convert tuples of views into collectionsConfigurableView - Views with configuration objectsViewConfiguration - Configuration types for configurable viewsAnyView - Type-erased view containerNative<T> - Wrapper for platform-native componentsNativeView - Trait for native platform widgetsDynamic - Runtime-updatable view componentDynamicHandler - Handle for updating dynamic viewswatch() - Helper to create reactive viewsEnvironment - Type-indexed dependency injection storeUseEnv - View that accesses environment valuesuse_env() - Helper to create environment-aware viewsWith<V, T> - Wrap a view with additional environment valueMetadata<T> - Attach metadata to views (must be handled by renderer)IgnorableMetadata<T> - Optional metadata (can be ignored by renderer)Retain - Keep values alive for view lifetimeHook<C> - Intercept and modify view configurationsRect, Size, Point - Geometry types (logical pixels)ProposalSize - Size proposals for layout negotiationStretchAxis - Specify which axis a view expands onLayout - Trait for custom layout algorithmsSubView - Proxy for querying child view sizesEvent - Enumeration of UI events (Appear, Disappear)OnEvent - Event handler componentHandler<T>, HandlerOnce<T> - Handler traits for environment-based callbacksActionObject - Type alias for action handlersViews - Trait for collections with stable identitiesAnyViews<V> - Type-erased view collectionForEach<C, F, V> - Transform data collections into viewsViewsExt - Extension methods for view collectionsAnimation - Declarative animation specificationsAnimationExt - Extension trait for reactive values.animated() - Apply default animation.with_animation() - Apply specific animationstd - Enable standard library support (currently no-op, reserved for future use)nightly - Enable nightly-only features (e.g., ! never type)serde - Enable Serde serialization support for core typesWaterUI uses logical pixels (points) for all layout values, matching design tools like Figma:
Layout is a two-phase process:
The Layout trait defines custom layout algorithms. The StretchAxis enum specifies which axes views expand on:
None - Content-sizedHorizontal / Vertical - Expand on one axisBoth - Greedy, fills all spaceMainAxis / CrossAxis - Relative to container directionCustom View → body() → ... → body() → Raw View → Native Backend → Platform Widget
body() that returns other viewsbody() until reaching raw viewsNativeView) are handled by the platform backendThe handler system supports automatic parameter extraction from environments:
use waterui_core::{handler::into_handler, extract::Use};
struct Config { value: i32 }
let handler = into_handler(|Use(config): Use<Config>| {
println!("Config value: {}", config.value);
});
Handlers come in three flavors:
Handler<T> - Reusable, takes &mut selfHandlerOnce<T> - Single-use, consumes selfHandlerFn<P, T> - Function-like trait with parameter extractionMIT