waterui-core 0.2.0

Core functionality for the WaterUI framework
Documentation

waterui-core

The foundational crate providing essential building blocks for the WaterUI cross-platform reactive UI framework.

Overview

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.

Installation

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.

Quick Start

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)
}

Core Concepts

The View Trait

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 view
  • Option<V> - renders Some(view) or nothing
  • Result<V, E> - renders either the success or error view
  • Closures Fn() -> impl View - lazy view construction

Environment

The 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:

  • Typed storage: Insert values of any 'static type
  • Plugin installation: Modular extensions via the Plugin trait
  • View hooks: Intercept and modify view configurations globally
  • Cloning: Cheap cloning via Rc for child environments

AnyView - Type Erasure

AnyView 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.

Reactive Primitives

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 state
  • Computed<T> - Derived reactive values
  • Signal<T> - Read-only reactive values
  • SignalExt - Extension methods for all reactive types

The 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

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);

Examples

Custom Component with State

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" })
        })
    }
}

Environment-based Configuration

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(),
    })
}

Reactive Computed Values

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)
});

Plugin System

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);

API Overview

Core Traits

  • View - The fundamental UI component trait
  • IntoView - Convert types into views
  • TupleViews - Convert tuples of views into collections
  • ConfigurableView - Views with configuration objects
  • ViewConfiguration - Configuration types for configurable views

Type Erasure

  • AnyView - Type-erased view container
  • Native<T> - Wrapper for platform-native components
  • NativeView - Trait for native platform widgets

Reactive Components

  • Dynamic - Runtime-updatable view component
  • DynamicHandler - Handle for updating dynamic views
  • watch() - Helper to create reactive views

Environment & Context

  • Environment - Type-indexed dependency injection store
  • UseEnv - View that accesses environment values
  • use_env() - Helper to create environment-aware views
  • With<V, T> - Wrap a view with additional environment value

Metadata & Hooks

  • Metadata<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 lifetime
  • Hook<C> - Intercept and modify view configurations

Layout Primitives

  • Rect, Size, Point - Geometry types (logical pixels)
  • ProposalSize - Size proposals for layout negotiation
  • StretchAxis - Specify which axis a view expands on
  • Layout - Trait for custom layout algorithms
  • SubView - Proxy for querying child view sizes

Event Handling

  • Event - Enumeration of UI events (Appear, Disappear)
  • OnEvent - Event handler component
  • Handler<T>, HandlerOnce<T> - Handler traits for environment-based callbacks
  • ActionObject - Type alias for action handlers

View Collections

  • Views - Trait for collections with stable identities
  • AnyViews<V> - Type-erased view collection
  • ForEach<C, F, V> - Transform data collections into views
  • ViewsExt - Extension methods for view collections

Animation

  • Animation - Declarative animation specifications
  • AnimationExt - Extension trait for reactive values
  • .animated() - Apply default animation
  • .with_animation() - Apply specific animation

Features

Default Features

  • None - The crate has no default features for maximum flexibility

Optional Features

  • std - 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 types

Architecture Notes

Layout System

WaterUI uses logical pixels (points) for all layout values, matching design tools like Figma:

  • 1 logical pixel = 1 point in design tools
  • Backends handle conversion to physical pixels based on screen density
  • Consistent physical size across platforms and densities

Layout is a two-phase process:

  1. Sizing: Determine container size given a proposal from parent
  2. Placement: Position children within the final bounds

The Layout trait defines custom layout algorithms. The StretchAxis enum specifies which axes views expand on:

  • None - Content-sized
  • Horizontal / Vertical - Expand on one axis
  • Both - Greedy, fills all space
  • MainAxis / CrossAxis - Relative to container direction

View Rendering Pipeline

Custom View → body() → ... → body() → Raw View → Native Backend → Platform Widget
  1. Custom views define body() that returns other views
  2. Framework recursively calls body() until reaching raw views
  3. Raw views (marked with NativeView) are handled by the platform backend
  4. Backend translates to native widgets (SwiftUI views, Compose composables, etc.)

Handler System

The 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 self
  • HandlerOnce<T> - Single-use, consumes self
  • HandlerFn<P, T> - Function-like trait with parameter extraction

License

MIT