WaterUI Tutorial Book

Welcome to the complete guide for building cross-platform applications with WaterUI! This book will take you from a complete beginner to an advanced WaterUI developer, capable of building sophisticated applications that run on desktop, web, mobile, and embedded platforms.

What is WaterUI?

WaterUI is a modern, declarative UI framework for Rust that enables you to build applications using a single codebase for multiple platforms. It combines the safety and performance of Rust with an intuitive, reactive programming model inspired by SwiftUI and React.

Key Features

  • πŸš€ Cross-Platform: Write once, deploy everywhere - desktop, web, mobile, embedded
  • πŸ¦€ Type-Safe: Leverage Rust's powerful type system for compile-time correctness
  • ⚑ Reactive: Automatic UI updates when data changes
  • πŸ“ Declarative: Describe what your UI should look like, not how to build it

Prerequisites

Before starting this book, you should have:

  • Basic Rust Knowledge: Understanding of ownership, borrowing, traits, and generics
  • Programming Experience: Familiarity with basic programming concepts
  • Command Line Comfort: Ability to use terminal/command prompt

If you're new to Rust, we recommend reading The Rust Programming Language first.

Contributing

This book is open source! Found a typo, unclear explanation, or want to add content?

  • Source Code: Available on GitHub
  • Issues: Report problems or suggestions
  • Pull Requests: Submit improvements

Installation and Setup

Before we dive into building applications with WaterUI, let's set up a proper development environment. This chapter will guide you through installing Rust, setting up your editor, and creating your first WaterUI project.

Installing Rust

WaterUI requires Rust 1.85 or later with the 2024 edition. The easiest way to install Rust is through rustup.

On macOS, Linux, or WSL

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

On Windows

  1. Download the installer from rustup.rs
  2. Run the downloaded .exe file
  3. Follow the installation prompts
  4. Restart your command prompt or PowerShell

Verify Installation

After installation, verify that everything works:

rustc --version
cargo --version

You should see output like:

rustc 1.85.0 (a28077b28 2024-02-28)
cargo 1.85.0 (1e91b550c 2024-02-27)

Note: WaterUI requires Rust 1.85 or later. If you have an older version, update with rustup update.

Editor Setup

While you can use any text editor, we recommend VS Code for the best WaterUI development experience.

  1. Install VS Code: Download from code.visualstudio.com

  2. Install Essential Extensions:

    • rust-analyzer: Provides IntelliSense, error checking, and code completion
    • CodeLLDB: Debugger for Rust applications
    • Better TOML: Syntax highlighting for Cargo.toml files
  3. Optional Extensions:

    • Error Lens: Inline error messages
    • Bracket Pair Colorizer: Colorizes matching brackets
    • GitLens: Enhanced Git integration

IntelliJ IDEA / CLion:

  • Install the "Rust" plugin
  • Excellent for complex projects and debugging

Vim / Neovim:

  • Use rust.vim for syntax highlighting
  • Use coc-rust-analyzer for LSP support

Emacs:

  • Use rust-mode for syntax highlighting
  • Use lsp-mode with rust-analyzer

Creating Your First Project

Let's create a new WaterUI project from scratch:

cargo new hello-waterui
cd hello-waterui

This creates a new Rust project with the following structure:

hello-waterui/
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ src/
β”‚   └── main.rs
└── .gitignore

Adding WaterUI Dependencies

Edit your Cargo.toml file to include WaterUI:

Filename: Cargo.toml

[package]
name = "hello-waterui"
version = "0.1.0"
edition = "2021"

[dependencies]
waterui = "0.1.0"
# Choose your backend(s)
waterui_gtk4 = "0.1.0"    # For desktop applications
# waterui_web = "0.1.0"     # For web applications

Tip: You can include multiple backends in the same project to support different platforms.

Platform-Specific Setup

Depending on your target platform, you may need additional system dependencies.

Desktop Development (GTK4)

Ubuntu/Debian:

sudo apt update
sudo apt install libgtk-4-dev build-essential

Fedora/RHEL:

sudo dnf install gtk4-devel gcc

Arch Linux:

sudo pacman -S gtk4 base-devel

macOS:

# Install Homebrew if you haven't already
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install GTK4
brew install gtk4

Windows:

  1. Install MSYS2
  2. Open MSYS2 terminal and run:
    pacman -S mingw-w64-x86_64-gtk4 mingw-w64-x86_64-toolchain
    
  3. Add MSYS2 to your PATH: C:\msys64\mingw64\bin

Web Development (WebAssembly)

For web development, install additional tools:

# Install wasm-pack for building WebAssembly packages
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# Add WebAssembly target
rustup target add wasm32-unknown-unknown

Hello,world!

Let's create a simple "Hello, World!" application to verify everything works.

Filename: src/main.rs

use waterui::View;
use waterui_gtk4::{Gtk4App, init};

fn home() -> impl View { "Hello, WaterUI! 🌊" }

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize the GTK4 backend
    init()?;

    // Create and run the application
    let app = Gtk4App::new("com.example.hello-waterui");
    Ok(app.run(home).into())
}

Building and Running

Build and run your application:

cargo run

If everything is set up correctly, you should see a window with "Hello, WaterUI! 🌊" displayed.

Troubleshooting Common Issues

GTK4 Not Found

Error: Package 'gtk4' not found

Solution: Install GTK4 development libraries for your platform (see Platform-Specific Setup above).

Rust Version Too Old

Error: error: package requires Rust version 1.85

Solution: Update Rust:

rustup update

Windows Build Issues

Error: Various Windows compilation errors

Solutions:

  1. Ensure you have the Microsoft C++ Build Tools installed
  2. Use the x86_64-pc-windows-msvc toolchain
  3. Consider using WSL2 for a Linux-like environment

Your First WaterUI App

Now that your development environment is set up, let's build your first interactive WaterUI application! We'll create a counter app that demonstrates the core concepts of views, state management, and user interaction.

What We'll Build

Our counter app will feature:

  • A display showing the current count
  • Buttons to increment and decrement the counter
  • A reset button
  • Dynamic styling based on the counter value

By the end of this chapter, you'll understand:

  • How to create interactive views
  • How to manage reactive state
  • How to handle user events
  • How to compose views together

Setting Up the Project

Create a new project for our counter app:

cargo new counter-app
cd counter-app

Update your Cargo.toml:

Filename: Cargo.toml

[package]
name = "counter-app"
version = "0.1.0"
edition = "2021"

[dependencies]
waterui = { path = ".." }
waterui_gtk4 = { path = "../backends/gtk4" }

Building the Counter Step by Step

Let's build our counter app incrementally, learning WaterUI concepts along the way.

Step 1: Basic Structure

Start with a simple view structure. Since our initial view doesn't need state, we can use a function:

Filename: src/main.rs

use waterui::View;
use waterui_gtk4::{Gtk4App, init};

pub fn counter() -> impl View {
    "Counter App"
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    init()?;
    let app = Gtk4App::new("com.example.counter-app");
    Ok(app.run(counter).into())
}

Run this to make sure everything works:

cargo run

You should see a window with "Counter App" displayed.

Step 2: Adding Layout

Now let's add some layout structure using stacks:

use waterui::{component::layout::stack::vstack, View};

pub fn counter() -> impl View {
    vstack((
        "Counter App",
        "Count: 0",
    ))
}

Note: vstack creates a vertical stack of views. We'll learn about hstack (horizontal) and zstack (overlay) later.

Step 3: Adding Reactive State

Now comes the exciting part - let's add reactive state! We'll use the s! macro from nami for reactive computations and the text! macro for reactive text:

use waterui::{
    component::{
        layout::stack::{vstack, hstack},
        button::button,
    },
    View,
};
use waterui_text::text;
use waterui::reactive::binding;

pub fn counter() -> impl View {
    let count = binding(0);
    vstack((
        "Counter App",
        // Use text! macro for reactive text
        text!("Count: {}", count),
        hstack((
            button("- Decrement").action_with(&count, |count| count.update(|n| n - 1)),
            button("+ Increment").action_with(&count, |count| count.update(|n| n + 1)),
        )),
    ))
}

Run this and try clicking the buttons! The counter should update in real-time.

Understanding the Code

Let's break down the key concepts introduced:

Reactive State with binding

let count = Binding::int(0);

This creates a reactive binding with an initial value of 0. When this value changes, any UI elements that depend on it will automatically update.

Reactive Text Display

// βœ… Use the text! macro for reactive display
text!("Count: {}", count)
  • The text! macro automatically handles reactivity
  • The text will update whenever count changes

Event Handling

button("- Decrement").action_with(&count, |count| count.update(|n| n - 1))
  • .action_with() attaches an event handler with captured state
  • Binding<T>::update(|v| ...) updates the value and notifies watchers

Layout with Stacks

vstack((...))  // Vertical stack
hstack((...))  // Horizontal stack

Stacks are the primary layout tools in WaterUI, allowing you to arrange views vertically or horizontally.

Understanding Views

The View system is the heart of WaterUI. Everything you see on screen is a View, and understanding how Views work is crucial for building efficient and maintainable applications. In this chapter, we'll explore the View trait in depth and learn how to create custom components.

What is a View?

A View in WaterUI represents a piece of user interface. It could be as simple as a text label or as complex as an entire application screen. The beauty of the View system is that simple and complex views work exactly the same way.

The View Trait

Every View implements a single trait:

pub trait View: 'static {
    fn body(self, env: &Environment) -> impl View;
}

This simple signature enables powerful composition patterns. Let's understand each part:

  • 'static lifetime: Views can't contain non-static references, ensuring they can be stored and moved safely
  • self parameter: Views consume themselves when building their body, enabling zero-cost moves
  • env: &Environment: Provides access to shared configuration and dependencies
  • -> impl View: Returns any type that implements View, enabling flexible composition

Built-in Views

WaterUI provides many built-in Views for common UI elements:

Text Views

// Static text
"Hello, World!"

// Equal to text(name.map(|n| format!("Hello, {n}!")))
text!("Hello, {}!", name)

// Styled text
waterui_text::Text::new("Important!")
    .size(24.0)

Control Views

// Button
button("Click me")
    .action(|| println!("Clicked!"))

// Text field
let input = waterui::reactive::binding(String::new());
text_field(&input)
    .placeholder("Enter text...")

// Toggle switch
let enabled = waterui::reactive::binding(false);
toggle(&enabled)

Layout Views

// Vertical stack
vstack((
    "First",
    "Second",
    "Third",
))

// Horizontal stack
hstack((
    button("Cancel"),
    button("OK"),
))

// Overlay stack
zstack((
    background_view(),
    content_view(),
    overlay_view(),
))

Creating Custom Views

The real power of WaterUI comes from creating your own custom Views. Let's explore different patterns:

// Simpler and cleaner - no View trait needed!
fn welcome_message(name: &str) -> impl View {
    vstack((
        waterui_text::Text::new("Welcome!").size(24.0),
        waterui_text::Text::new(format!("Hello, {}!", name)),
    ))
    .frame(waterui::component::layout::Frame::new().margin(waterui::component::layout::Edge::round(20.0)))
}

// Usage - functions are automatically views!
welcome_message("Alice")

// Can also use closures for lazy initialization
let lazy_view = || welcome_message("Bob");

Struct Views (For Components with State)

Only use the View trait when your component needs to store state, or you prefer direct access to the environment in body.

// Only needed when the struct holds state
struct CounterWidget {
    initial_value: i32,
    step: i32,
}

impl View for CounterWidget {
    fn body(self, _env: &Environment) -> impl View {
        let count = binding(self.initial_value);
        
        vstack((
            text!("Count: {}", count),
            button("+")
                .action_with(&count, move |count| count.update(|n| n + self.step)),
        ))
    }
}

// Usage
CounterWidget { 
    initial_value: 0,
    step: 5,
}

Nami - Reactive System of WaterUI

Reactive state management is the heart of interactive WaterUI applications. When your data changes, the UI automatically updates to reflect those changes. This chapter teaches you how to master WaterUI's reactive system powered by the nami crate.

Understanding the Foundation: Signal Trait

Everything in nami's reactive system implements the Signal trait. This trait represents any value that can be observed and computed:

pub trait Signal: Clone + 'static {
    type Output;
    type Guard:WatcherGuard;
    
    // Get the current value
    fn get(&self) -> Self::Output;
    
    // Watch for changes (used internally by the UI system)
    fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> Self::Guard;
}

Key insight: A Signal represents a reactive value that knows how to:

  1. Compute its current value (get())
  2. Notify observers when it changes (watch())

Types of Signals

There are several types that implement Signal, each serving different purposes:

1. Constants - Never Change

use nami::constant;

let fixed_name = constant("WaterUI");  // Never changes
let fixed_number = constant(42);       // Never changes

// Even literals implement Signal automatically! (but not all!)
let literal_string = "Hello World";   // Already a Signal!
let literal_number = 100;             // Already a Signal!

2. Binding - Mutable Reactive State

Binding<T> is for mutable reactive state that can be changed and will notify the UI:

use waterui::{binding, Binding};

// Create mutable reactive state
let counter: Binding<i32> = binding(0);
let name: Binding<String> = binding("Alice");

// Set new values (triggers UI updates)
counter.set(42);
name.set("Bob".to_string());

3. Computed Signals - Derived from Other Signals

These are created by transforming other signals using SignalExt methods:

use nami::SignalExt;

let first_name = binding("Alice".to_string());
let last_name = binding("Smith".to_string());

// Create computed signals that update automatically
let full_name = first_name.zip(last_name).map(|(first, last)| {
    format!("{} {}", first, last)
});

let name_length = first_name.map(|name| name.len());

⚠️ WARNING: The Dangers of .get()

.get() is the #1 reactivity killer! Here's why it's dangerous:

let name = binding("Alice".to_string());
let age = binding(25);

// ❌ WRONG: Using .get() breaks reactivity
let broken_message = format!("Hello {}, you are {}", name.get(), age.get());
text(broken_message); // This will NEVER update when name or age change!

// βœ… CORRECT: Keep reactive chain intact  
let reactive_message = s!("Hello {name}, you are {age}");
text(reactive_message); // This updates automatically when name or age change

When you call .get():

  • You extract a snapshot of the current value
  • The reactive connection is permanently broken
  • UI will never update even when the original signal changes
  • You lose all the benefits of the reactive system

Only use .get() when you absolutely need the raw value outside reactive contexts (like debugging, logging, or interfacing with non-reactive APIs).

Working with Bindings - Mutable Signals

Now that you understand signals, let's dive into Binding<T> - the mutable reactive state container:

Basic Operations

let counter = Binding::int(0);

// Set new values (triggers UI updates)
counter.set(42);

// Bindings automatically provide their current value in reactive contexts
// No need to extract values with .get() - just use the binding directly!

Type-Specific Convenience Methods

Nami provides specialized methods for different types to make common operations more ergonomic:

Integer Bindings

let counter = Binding::int(0);

// Convenient arithmetic operations
counter.increment(1);     // counter += 1
counter.decrement(2);     // counter -= 2
counter.set(10);

Boolean Bindings

let is_enabled = Binding::bool(false);

// Toggle between true/false
is_enabled.toggle();

// Logical NOT operation
let is_disabled = !is_enabled; // Creates a new reactive binding

String Bindings

let text = Binding::container(String::from("Hello"));

// Append text
text.append(" World");
text.clear();  // Empty the string

Vector Bindings

let items = binding(vec![1, 2, 3]);

// Collection operations
items.push(4);              // Add to end
items.insert(1, 99);        // Insert at index
let last = items.pop();      // Remove and return last
items.clear();               // Remove all elements

// For sortable vectors
let sortable = binding(vec![3, 1, 4, 1, 5]);
sortable.sort();             // Sort in-place

Creating Computed Signals with SignalExt

All signals get powerful transformation methods through the SignalExt trait:

Basic Transformations

use nami::SignalExt;

let numbers = Binding::container(vec![1, 2, 3, 4, 5]);

// Transform the data
let doubled = numbers.map(|nums| {
    nums.iter().map(|&n| n * 2).collect::<Vec<_>>()
});

// Single value transformations
let count = numbers.map(|nums| nums.len());
let sum = numbers.map(|nums| nums.iter().sum::<i32>());

Combining Multiple Signals

let a = binding(10);
let b = binding(20);

// Combine two signals
let sum = a.zip(b).map(|(x, y)| x + y);
let product = a.zip(b).map(|(x, y)| x * y);
let complex = a.zip(b).map(|(x, y)| x * 2 + y / 2);

Performance Optimizations

let expensive_data = binding(vec![1, 2, 3, 4, 5]);

// Cache expensive computations (only recomputes when data changes)
let sum = expensive_data.cached().map(|nums| {
    // Expensive operation here
    nums.iter().sum::<i32>()
});

The s! Macro - Reactive String Formatting

The s! macro from nami is a specialized macro for string formatting with automatic variable capture from reactive signals:

use nami::s;
use waterui::{binding, text};

let name = binding("Alice".to_string());
let age = binding(25);
let score = binding(95.5);

// βœ… s! macro for reactive string formatting with automatic capture
let greeting = s!("Hello {name}!");                    // Captures 'name' automatically
let info = s!("Name: {name}, Age: {age}");             // Multiple variables  
let detailed = s!("{name} is {age} years old");        // Clean, readable syntax

// Use with text to display
text(greeting);     // Automatically updates when 'name' changes
text(info);         // Updates when either 'name' or 'age' changes

// You can also use positional arguments
let positioned = s!("Hello {}, you are {} years old", name, age);

// The s! macro is specifically for string formatting - 
// for other reactive computations, use SignalExt methods

Advanced Features

Mutable Access Guard

let data = binding(vec![1, 2, 3]);

// Get mutable access that automatically updates on drop
let mut guard = data.get_mut();
guard.push(4);
guard.sort();
// Updates are sent when guard is dropped

Filtered/Constrained Bindings

let temperature = Binding::int(25);

// Create a binding constrained to a range
let safe_temp = temperature.range(0..=100);

// Custom filters
let even_numbers = binding(0);
let only_even = even_numbers.filter(|&n| n % 2 == 0);

Debounced Bindings

use std::time::Duration;

let search_query = Binding::container(String::new());

// Only update after user stops typing for 300ms
let debounced_search = search_query.debounced(Duration::from_millis(300));

Working with Optional Values

let maybe_name: Binding<Option<String>> = binding(None);

// Provide default when None
let display_name = maybe_name.unwrap_or_else(|| "Anonymous".to_string());

// Transform the inner value if present
let maybe_upper = maybe_name.map(|opt| opt.map(|s| s.to_uppercase()));

The Environment System

The Environment system is WaterUI's approach to dependency injection and configuration management. It provides a type-safe way to pass data, themes, services, and other dependencies through your view hierarchy without explicit parameter passing. Think of it as a context that flows down through your UI tree.

Basic Environment Usage

Storing Values

The most common way to add values to an environment:

use waterui::{Environment, View, ViewExt};
use waterui::component::layout::{Edge, Frame};

#[derive(Debug, Clone)]
struct AppConfig {
    api_url: String,
    timeout_seconds: u64,
}

#[derive(Debug, Clone)]
struct Theme {
    primary_color: waterui::core::Color,
    background_color: waterui::core::Color,
}

pub fn entry() -> impl View {
    home
        .with(AppConfig {
        api_url: "https://api.example.com".to_string(),
        timeout_seconds: 30,
    })
    .with(Theme {
        primary_color: (0.0, 0.4, 1.0).into(),
        background_color: (1.0, 1.0, 1.0).into(),
    })
}

pub fn home() -> impl View{
	// Your home page
}

Accessing Values in Views

For Struct Views

Views can access environment values in their body method:

struct ApiStatusView;

impl View for ApiStatusView {
    fn body(self, env: &Environment) -> impl View {
        // Get configuration from environment
        let config = env.get::<AppConfig>()
            .expect("AppConfig should be provided");
            
        let theme = env.get::<Theme>()
            .expect("Theme should be provided");
        
        vstack((
            waterui_text::Text::new(config.api_url.clone()).foreground(theme.primary_color.clone()),
            waterui_text::Text::new(format!("Timeout: {}s", config.timeout_seconds)).size(14.0),
        ))
        .background(waterui::background::Background::color(theme.background_color.clone()))
        .frame(Frame::new().margin(Edge::round(12.0)))
    }
}

For Function Views

You can pass values through the environment at the call site and read them inside struct views as shown above. Function views typically compose other views and don’t receive env directly.

In action

use waterui::{View};
use waterui::reactive::binding;
use waterui_text::text;
use waterui::component::{layout::stack::vstack, button::button};

#[derive(Debug, Clone)]
pub struct Message(&'static str);

pub fn click_me() -> impl View {
    let value = binding(String::new());
    vstack((
        button("Show environment value").action_with(&value, |value, msg: waterui::core::extract::Use<Message>| {
            value.set(msg.0 .0.to_string());
        }),
        text!("{}", value),
    ))
    .with(Message("I'm Lexo"))
}

Conditional Rendering

Conditional rendering is a fundamental technique for creating dynamic user interfaces that respond to changing application state. WaterUI provides powerful and ergonomic components for conditional rendering through the when function and related components.

The when Function

The when function is the primary tool for conditional rendering in WaterUI. It takes a reactive boolean condition and a closure that returns a view to display when the condition is true.

use waterui::widget::condition::when;
use waterui_text::text;
use waterui::reactive::{binding, s};

let is_logged_in = binding(false);

when(&is_logged_in, || {
    text!("Welcome back!")
})

Basic Conditional Rendering

Here's a simple example showing how to conditionally display content:

use waterui::widget::condition::when;
use waterui_text::text;
use waterui::component::{button::button, layout::stack::vstack};
use waterui::reactive::binding;

pub fn login_view() -> impl View {
	#[derive(Debug,Clone,Default)]
	pub struct Authenticated(Binding<Bool>)
    
    vstack((
        use_env(|Authenticated(auth):Authenticated|{
	        when(&auth, || {
		            text!("You are logged in!")
	        }),
        
	        when(&!is_authenticated, |Authenticated(auth):Authenticated| {
	            button("Login").action(move || auth.set(true)})
	        })
        })
    )).with(Authenticated::default())
}

Reactive Negation with !

WaterUI's Binding type implements the Not trait, which means you can use ! directly on bindings without wrapping them in s!(). This maintains full reactivity:

use waterui::{when, text};
use nami::binding;

let is_visible = binding(true);

when(!is_visible, || text!("Hidden"));

Complete Conditional Rendering with or

For situations where you need to display one of two views, use the .or() method:

use waterui::widget::condition::when;
use waterui_text::text;
use waterui::component::button::button;
use waterui::reactive::binding;

let has_data = binding(false);

when(&has_data, || {
    text!("Data loaded successfully!")
}).or(|| {
    text!("Loading...")
})

Layout Components

WaterUI provides powerful layout components for arranging UI elements. This chapter covers the essential layout tools.

Stack Layouts

VStack - Vertical Arrangement

use waterui::{View, ViewExt};
use waterui::component::layout::stack::vstack;
use waterui::component::layout::{Edge, Frame};

fn vertical_layout() -> impl View {
    vstack((
        "First item",
        "Second item",
        "Third item",
    ))
    .frame(Frame::new().margin(Edge::round(20.0)))
}

HStack - Horizontal Arrangement

use waterui::{View, ViewExt};
use waterui::component::layout::stack::hstack;
use waterui::component::layout::spacer::spacer;
use waterui::component::button::button;
use waterui::component::layout::{Edge, Frame};

fn navigation_bar() -> impl View {
    hstack((
        button("← Back"),
        spacer(),  // Pushes items apart
        "Title",
        spacer(),
        button("Menu"),
    ))
    .frame(Frame::new().margin(Edge::round(15.0)))
}

ZStack - Overlay Arrangement

// Overlay examples depend on your backend renderer; use zstack to layer views.

Grid Layout

use waterui::View;
use waterui_layout::grid::Grid;
use waterui_layout::{row, Alignment};

fn photo_grid() -> impl View {
    Grid::new(
        Alignment::Center,
        [
            row((photo("1.jpg"), photo("2.jpg"), photo("3.jpg"))),
            row((photo("4.jpg"), photo("5.jpg"), photo("6.jpg"))),
            row((photo("7.jpg"), photo("8.jpg"), photo("9.jpg"))),
        ],
    )
}

Scrolling

// Scrolling helpers exist; see waterui_layout::scroll for details.

Sizing and Constraints

// Use Frame to control size constraints:
// view.frame(Frame::new().width(250.0).max_width(300.0))

Next: Text and Typography

Text and Typography

Text is fundamental to any UI framework. WaterUI provides two distinct approaches for displaying text: labels for static text without styling, and the Text component for reactive text with rich styling capabilities.

Understanding Labels vs Text Components

Labels: Simple Static Text

In WaterUI, several types implement the View trait directly and are called "labels":

  • &'static str - String literals
  • String - Owned strings
  • Str - WaterUI's optimized string type

Labels are rendered as simple, unstyled text and are perfect for static content:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;

fn label_examples() -> impl View {
    vstack((
        // String literal as label
        "Simple static text",

        // String variable as label
        String::from("Dynamic string as label"),

        // Multi-line text
        r#"Multi-line text with
line breaks"#,
    ))
}
}

Text Component: Reactive and Styleable

The Text component provides reactive updates and rich styling options:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::reactive::binding;
use waterui::component::layout::stack::vstack;
use waterui::component::button::button;
use waterui_text::{text, Text};

fn text_component_examples() -> impl View {
    let count = binding(0);
    let name = binding("Alice".to_string());

    vstack((
        // Basic Text component
        "Styleable text content",

        // Reactive text with text! macro
        text!("Count: {}", count),
        text!("Hello, {}!", name),

        // Text component with styling
        Text::new("Styled text").size(20.0),

        button("Increment")
            .action({
                let count = count.clone();
                move |_| count.update(|c| c + 1)
            }),
    ))
}
}

Text Styling with the Text Component

Only the Text component (created with text() function or Text::new()) supports styling. Labels (&str, String, Str) are rendered without styling.

Font Properties

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui_text::text;

fn font_styling_demo() -> impl View {
    vstack((
        // Labels - no styling available
        "Default label text",

        // Text components - styling available
        text("Large text")
            .size(24.0),

        text("Small text")
            .size(12.0),

        // Bold/weight not yet available
        text("Styleable text").size(18.0),
    ))
}
}

When to Use Labels vs Text Components

Choose the right approach based on your needs:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui_text::text;

fn choosing_text_type() -> impl View {
    vstack((
        // Use labels for simple, static text
        "Static heading",
        "Simple description text",

        // Use Text component for styled text
        text("Styled heading").size(20.0),

        // Use text! macro for reactive content
        {
            let count = binding(42);
            text!("Dynamic count: {}", count)
        },
    ))
}
}

Reactive Text with the text! Macro

The text! macro creates reactive Text components that automatically update when underlying data changes:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::reactive::{binding, s};
use waterui::component::layout::stack::{vstack, hstack};
use waterui::component::button::button;
use waterui_text::text;

fn reactive_text_demo() -> impl View {
    let count = binding(0);
    let name = binding(String::from("Alice"));
    let temperature = binding(22.5);

    vstack((
        // Reactive formatted text
        text!("Count: {}", count),
        text!("Hello, {}!", name),
        text!("Temperature: {:.1}Β°C", temperature),

        // Reactive with computed expressions using s! macro
        text!("Status: {}", s!(if count > 5 { "High" } else { "Low" })),

        hstack((
            button("Increment").action({
                let count = count.clone();
                move |_| count.update(|c| c + 1)
            }),
            button("Reset").action({
                let count = count.clone();
                move |_| count.set(0)
            }),
        )),
    ))
}
}

Formatting Best Practices

Always use text! macro for reactive text, never format! macro which loses reactivity:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui::reactive::binding;
use waterui_text::text;

fn formatting_best_practices() -> impl View {
    let user_count = binding(42);
    let status = binding(String::from("Active"));

    vstack((
        // βœ… CORRECT: Use text! for reactive content
        text!("Users: {} ({})", user_count, status),

        // ❌ WRONG: .get() breaks reactivity!
        // text(format!("Users: {} ({})", user_count.get(), status.get()))
        // This creates static text that won't update when signals change!

        // βœ… CORRECT: Use labels for static text
        "Status Dashboard",

        // βœ… CORRECT: Use text() for static styleable content
        text("Styleable heading").size(18.0),
    ))
}
}

Advanced Text Component Features

The Text component provides additional capabilities beyond basic labels:

Text Display Options

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui::reactive::binding;
use waterui_text::{text, Text};

fn text_display_demo() -> impl View {
    vstack((
        // Display formatting with different value types
        Text::display(binding(42)),
        Text::display(binding(3.14159)),
        Text::display(binding(true)),

        // Custom formatting with formatters
        {
            let price = binding(29.99);
            Text::format(price, |value| format!("${:.2}", value))
        },
    ))
}
}

// Rich text (spans, bold/italic) is planned but not yet available.

Performance Considerations

Efficient Reactive Updates

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui::component::button::button;
use waterui::reactive::{binding, s};
use waterui_text::text;

fn efficient_text_updates() -> impl View {
    let counter = binding(0);

    vstack((
        // βœ… GOOD: Reactive text with text! macro
        text!("Counter: {}", counter),

        // βœ… GOOD: Computed reactive text
        text!("Status: {}", s!(match counter {
            0..=10 => "Low",
            11..=50 => "Medium",
            _ => "High"
        })),

        button("Increment").action({
            let counter = counter.clone();
            move |_| counter.update(|c| c + 1)
        }),
    ))
}
}

Summary

WaterUI's text system provides:

  • Labels (&str, String, Str): Simple, unstyled text for static content
  • Text Component: Reactive, styleable text with font customization
  • text! macro: Reactive formatted text that updates automatically
  • Font API: Comprehensive typography control for Text components

Key Guidelines

  • Use labels for simple, static text without styling
  • Use Text component when you need styling or reactive updates
  • Use text! macro for formatted reactive content
  • Never use format! with reactive values - it breaks reactivity
  • Choose the simplest approach that meets your needs

Quick Reference

#![allow(unused)]
fn main() {
use waterui::View;
use waterui::component::layout::stack::vstack;
use waterui::reactive::binding;
use waterui_text::{text, Text};

fn text_reference() -> impl View {
    let count = binding(42);

    vstack((
        // Label: static, no styling
        "Simple text",

        // Text: static, with styling
        text("Styled text").size(18.0),

        // Text: reactive, formatted
        text!("Count: {}", count),

        // Text: reactive, custom formatting
        Text::display(count),
        Text::format(count, |n| format!("#{:03}", n)),
    ))
}
}

Next: Forms

Form Controls

WaterUI provides a comprehensive form system that makes creating interactive forms both simple and powerful. The centerpiece of this system is the FormBuilder derive macro, which automatically generates form UIs from your data structures.

Quick Start with FormBuilder

The easiest way to create forms in WaterUI is using the #[derive(FormBuilder)] macro:

#![allow(unused)]
fn main() {
use waterui_form::{FormBuilder, form};
use waterui::reactive::Binding;

#[derive(Default, Clone, Debug, FormBuilder)]
pub struct LoginForm {
    /// The user's username
    pub username: String,
    /// The user's password  
    pub password: String,
    /// Whether to remember the user
    pub remember_me: bool,
    /// The user's age
    pub age: i32,
}

fn login_view() -> impl View {
    let form_binding = LoginForm::binding();
    form(&form_binding)
}
}

That's it! WaterUI automatically creates appropriate form controls for each field type:

  • String β†’ Text field
  • bool β†’ Toggle switch
  • i32 β†’ Number stepper
  • f32 β†’ Slider
  • And many more...

Type-to-Component Mapping

The FormBuilder macro automatically maps Rust types to appropriate form components:

Rust TypeForm ComponentDescription
String, &strTextFieldSingle-line text input
boolToggleOn/off switch
i32, i64, etc.StepperNumeric input with +/- buttons
f32, f64SliderSlider with 0.0-1.0 range
ColorColorPickerColor selection widget

Complete Example: User Registration Form

Let's build a more comprehensive form:

#![allow(unused)]
fn main() {
use waterui_form::{FormBuilder, form};
use waterui::reactive::Binding;
use waterui::core::Color;
use waterui::component::layout::stack::vstack;
use waterui_text::{text};

#[derive(Default, Clone, Debug, FormBuilder)]
struct RegistrationForm {
    /// Full name (2-50 characters)
    full_name: String,
    /// Email address
    email: String,
    /// Age (must be 18+)
    age: i32,
    /// Subscribe to newsletter
    newsletter: bool,
    /// Account type
    is_premium: bool,
    /// Profile completion (0.0 to 1.0)
    profile_completion: f32,
    /// Theme color preference
    theme_color: Color,
}

fn registration_view() -> impl View {
    let form_binding = RegistrationForm::binding();
    
    vstack((
        "User Registration",
        form(&form_binding),
        // Real-time validation feedback
        validation_feedback(&form_binding),
    ))
}

fn validation_feedback(form: &Binding<RegistrationForm>) -> impl View {
    text!(
        validate_registration(&form.get())
    )
}

fn validate_registration(data: &RegistrationForm) -> &'static str {
    if data.full_name.len() < 2 {
        "Name too short"
    } else if data.age < 18 {
        "Must be 18 or older"
    } else if !data.email.contains('@') {
        "Invalid email"
    } else {
        "Form is valid βœ“"
    }
}
}

Individual Form Controls

You can also use form controls individually:

Text Fields

#![allow(unused)]
fn main() {
use waterui_form::{TextField, field};
use waterui::reactive::binding;

fn text_field_example() -> impl View {
    let name = binding("".to_string());
    field("Name:", &name)
}
}

Toggle Switches

#![allow(unused)]
fn main() {
use waterui_form::{Toggle, toggle};

fn toggle_example() -> impl View {
    let enabled = binding(false);
    toggle("Enable notifications", &enabled)
}
}

Number Steppers

#![allow(unused)]
fn main() {
use waterui_form::{Stepper, stepper};

fn stepper_example() -> impl View {
    let count = binding(0);
    stepper(&count)
}
}

Sliders

#![allow(unused)]
fn main() {
use waterui_form::Slider;

fn slider_example() -> impl View {
    let volume = binding(0.5);
    Slider::new(0.0..=1.0, &volume)
}
}

Advanced Form Patterns

Multi-Step Forms

#![allow(unused)]
fn main() {
#[derive(Default, Clone, FormBuilder)]
struct PersonalInfo {
    first_name: String,
    last_name: String,
    birth_year: i32,
}

#[derive(Default, Clone, FormBuilder)]
struct ContactInfo {
    email: String,
    phone: String,
    preferred_contact: bool, // true = email, false = phone
}

#[derive(Default, Clone)]
struct RegistrationWizard {
    personal: PersonalInfo,
    contact: ContactInfo,
    current_step: usize,
}

fn registration_wizard() -> impl View {
    let wizard = binding(RegistrationWizard::default());
    
    vstack((
        text!(format!("Step {} of 2", wizard.current_step.get() + 1)),
        
        match wizard.current_step.get() {
            0 => vstack((
                "Personal Information",
                form(&wizard.personal),
            )),
            1 => vstack((
                "Contact Information", 
                form(&wizard.contact),
            )),
            _ => "Registration Complete!",
        },
        
        navigation_buttons(&wizard),
    ))
}
}

Custom Form Layouts

For complete control over form layout, implement FormBuilder manually:

#![allow(unused)]
fn main() {
use waterui_form::{FormBuilder, TextField, Toggle};
use waterui::{
    core::Binding,
    component::layout::stack::{vstack, hstack},
};

struct CustomForm {
    title: String,
    active: bool,
}

impl FormBuilder for CustomForm {
    type View = VStack;

    fn view(binding: &Binding<Self>) -> Self::View {
        vstack((
            hstack((
                "Title:",
                TextField::new(&binding.title),
            )),
            hstack((
                "Active:",
                Toggle::new(&binding.active),
            )),
        ))
    }
}
}

Secure Fields

For sensitive data like passwords:

#![allow(unused)]
fn main() {
use waterui_form::{SecureField, secure};

fn password_form() -> impl View {
    let password = binding(String::new());
    let confirm_password = binding(String::new());
    
    vstack((
        secure("Password:", &password),
        secure("Confirm Password:", &confirm_password),
        password_validation(&password, &confirm_password),
    ))
}

fn password_validation(pwd: &Binding<String>, confirm: &Binding<String>) -> impl View {
    text!(
        if pwd.get() == confirm.get() && !pwd.get().is_empty() {
            "Passwords match βœ“"
        } else {
            "Passwords don't match"
        }
    )
}
}

Form Validation Best Practices

Real-time Validation

#![allow(unused)]
fn main() {
#[derive(Default, Clone, FormBuilder)]
struct ValidatedForm {
    email: String,
    password: String,
    age: i32,
}

fn validated_form_view() -> impl View {
    let form = ValidatedForm::binding();
    
    vstack((
        form(&form),
        
        // Email validation
        text!(
            if form.email.get().contains('@') && form.email.get().contains('.') {
                "βœ“ Valid email"
            } else {
                "βœ— Please enter a valid email"
            }
        ),
        
        // Password validation
        text!(
            if form.password.get().len() >= 8 {
                "βœ“ Password is strong enough"
            } else {
                "βœ— Password must be at least 8 characters"
            }
        ),
        
        // Age validation
        text!(
            if form.age.get() >= 18 {
                "βœ“ Age requirement met"
            } else {
                "βœ— Must be 18 or older"
            }
        ),
        
        // Submit button - only enabled when form is valid
        button("Submit")
            .disabled(s!(
                !form.email.get().contains('@') ||
                form.password.get().len() < 8 ||
                form.age.get() < 18
            ))
            .action(|| {
                // Handle form submission
                println!("Form submitted!");
            }),
    ))
}
}

Integration with State Management

Forms integrate seamlessly with WaterUI's reactive state system:

#![allow(unused)]
fn main() {
use nami::s;

#[derive(Default, Clone, FormBuilder)]
struct UserSettings {
    name: String,
    theme: String,
    notifications: bool,
}

fn settings_panel() -> impl View {
    let settings = UserSettings::binding();
    
    // Computed values based on form state
    let has_changes = s!(
        settings.name.get() != "Default Name" ||
        settings.theme.get() != "Light" ||
        settings.notifications.get()
    );
    
    let settings_summary = s!(
        format!("User: {} | Theme: {} | Notifications: {}",
            settings.name.get(),
            settings.theme.get(), 
            if settings.notifications.get() { "On" } else { "Off" }
        )
    );
    
    vstack((
        "Settings",
        form(&settings),
        
        // Live preview
        "Preview:",
        text!(settings_summary),
        
        // Save button
        button("Save Changes")
            .disabled(s!(!has_changes))
            .action({
                let settings = settings.clone();
                move |_| {
                    save_settings(&settings.get());
                }
            }),
    ))
}

fn save_settings(settings: &UserSettings) {
    println!("Saving settings: {settings:?}");
    // Save to database, file, etc.
}
}

[WIP]

Media Components

WaterUI provides a comprehensive set of media components for displaying images, videos, and Live Photos in your applications. These components are designed to be reactive, configurable, and type-safe.

Overview

The media components in WaterUI include:

  • Photo: Display static images with customizable placeholders
  • Video: Video sources and players with reactive controls
  • VideoPlayer: Advanced video playback with volume control
  • LivePhoto: Apple Live Photo display with image and video components
  • MediaPicker: Platform-native media selection (when available)
  • Media: Unified enum for different media types

Photo Component

The Photo component displays images from URLs with support for placeholder views while loading.

Basic Usage

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_media::Photo;
use waterui_text::text;

pub fn photo_example() -> impl View {
    Photo::new("https://example.com/image.jpg")
        .placeholder(text!("Loading image..."))
}
}

Local Images

#![allow(unused)]
fn main() {
pub fn local_photo() -> impl View {
    Photo::new("assets/photo.jpg")
        .placeholder(
            waterui_layout::stack::vstack((text!("πŸ“·"), text!("Loading...")))
        )
}
}

Responsive Placeholders

You can create sophisticated placeholder views that match your app's design:

#![allow(unused)]
fn main() {
use waterui::{View, ViewExt, background::Background};
use waterui_layout::stack::{vstack, zstack};
use waterui_text::text;
use waterui::component::layout::{Edge, Frame};

pub fn styled_photo() -> impl View {
    Photo::new("https://example.com/large-image.jpg")
        .placeholder(
            zstack((
                // Background color layer
                ().background(Background::color((0.1, 0.1, 0.1))),
                // Loading indicator
                vstack((
                    text!("πŸ“Έ").size(48.0),
                    text!("Loading image...").foreground((0.6, 0.6, 0.6)),
                )),
            ))
            .frame(Frame::new().width(300.0).height(200.0))
        )
}
}

Video Components

WaterUI provides both Video sources and VideoPlayer components for video playback.

Basic Video

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_media::Video;

pub fn basic_video() -> impl View {
    Video::new("https://example.com/video.mp4")
}
}

When a Video is used as a view, it automatically creates a VideoPlayer.

Video Player with Controls

For more control over video playback, use VideoPlayer directly:

#![allow(unused)]
fn main() {
use waterui::{View, ViewExt};
use waterui_media::{Video, VideoPlayer};
use waterui_text::text;
use waterui::reactive::binding;
use waterui_layout::stack::{vstack, hstack};

pub fn video_with_controls() -> impl View {
    let video = Video::new("assets/demo.mp4");
    let muted = binding(false);
    
    vstack((
        VideoPlayer::new(video).muted(&muted),
        
        // Mute toggle button
        waterui::component::button::button("Toggle Mute")
            .action_with(&muted, |muted| muted.update(|m| !m))
    ))
}
}

Volume Control System

The video player uses a unique volume system where:

  • Positive values (> 0): Audible volume level
  • Negative values (< 0): Muted state that preserves the original volume level
  • When unmuting, the absolute value is restored
#![allow(unused)]
fn main() {
use waterui::{View};
use waterui_media::{Video, VideoPlayer};
use waterui::reactive::binding;
use waterui_layout::stack::{vstack, hstack};
use waterui_text::text;

pub fn volume_control_example() -> impl View {
    let video = Video::new("video.mp4");
    let muted = binding(false);
    let volume = binding(0.7); // 70% volume
    
    vstack((
        VideoPlayer::new(video).muted(&muted),
            
        // Volume controls
        hstack((
            waterui::component::button::button("πŸ”‡")
                .action_with(&muted, |muted| muted.set(true)),
            waterui::component::button::button("πŸ”‰")
                .action_with(&muted, |muted| muted.set(false)),
            // Volume internally stored as -0.7 when muted, +0.7 when unmuted
        ))
    ))
}
}

Live Photos

Live Photos combine a still image with a short video, similar to Apple's Live Photos feature.

Basic Live Photo

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_media::{LivePhoto, LivePhotoSource};

pub fn live_photo_example() -> impl View {
    let source = LivePhotoSource::new(
        "photo.jpg".into(),
        "photo_video.mov".into()
    );
    
    LivePhoto::new(source)
}
}

Reactive Live Photo

#![allow(unused)]
fn main() {
use waterui::{View};
use waterui_media::{LivePhoto, LivePhotoSource};
use waterui_text::text;
use waterui_layout::stack::{vstack, hstack};
use waterui::reactive::binding;

pub fn reactive_live_photo() -> impl View {
    let photo_index = binding(0);
    
    let live_source = s!({
        LivePhotoSource::new(
            format!("photo_{}.jpg", photo_index).into(),
            format!("photo_{}.mov", photo_index).into()
        )
    });
    
    vstack((
        LivePhoto::new(live_source),
        
        hstack((
            waterui::component::button::button("Previous")
                .action_with(&photo_index, |photo_index| {
                    photo_index.update(|idx| if idx > 0 { idx - 1 } else { idx })
                }),
            waterui::component::button::button("Next")
                .action_with(&photo_index, |photo_index| photo_index.update(|idx| idx + 1)),
        ))
    ))
}
}

Unified Media Type

The Media enum provides a unified way to handle different types of media content:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_media::{Media, LivePhotoSource};
use waterui_layout::stack::vstack;

pub fn media_gallery() -> impl View {
    let media_items = vec![
        Media::Image("photo1.jpg".into()),
        Media::Video("video1.mp4".into()),
        Media::LivePhoto(LivePhotoSource::new(
            "live1.jpg".into(),
            "live1.mov".into()
        )),
    ];
    
    // Each Media automatically renders as the appropriate component
    vstack(
        media_items
            .into_iter()
            .map(|media| media.frame(waterui::component::layout::Frame::new().width(300.0).height(200.0)))
            .collect::<Vec<_>>()
    )
}
}

Media Picker

The MediaPicker component provides platform-native media selection when available:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_media::{MediaPicker, MediaFilter};
use waterui::widget::condition::when;
use waterui_layout::stack::vstack;
use waterui_text::text;
use waterui::reactive::binding;

pub fn photo_picker_example() -> impl View {
    let selected_media = binding(None);
    
    vstack((
        waterui::component::button::button("Select Photo")
            .action(|| { /* open picker */ }),
            
        // Display selected media
        when(waterui::reactive::s!(selected_media.is_some()), || {
            // Simplest approach: show placeholder text when none is selected
            text!("Media selected")
        })
        .or(|| text!("No media selected"))
    ))
}
}

Media Filters

You can filter the types of media shown in the picker:

#![allow(unused)]
fn main() {
use waterui::media::MediaFilter;

// Only show images
let image_filter = MediaFilter::Image;

// Only show videos
let video_filter = MediaFilter::Video;

// Show images and live photos
let mixed_filter = MediaFilter::Any(vec![
    MediaFilter::Image,
    MediaFilter::LivePhoto
]);

// Show everything except videos
let no_videos = MediaFilter::Not(vec![MediaFilter::Video]);
}

Here's a complete example of a media gallery with different types of content:

#![allow(unused)]
fn main() {
use waterui::{View, ViewExt};
use waterui_media::*;
use waterui_layout::stack::{vstack, hstack};
use waterui_text::text;
use waterui::reactive::binding;
use waterui::component::layout::{Edge, Frame};

pub fn media_gallery_app() -> impl View {
    let selected_index = binding(0);
    
    let media_items = vec![
        Media::Image("gallery/sunset.jpg".into()),
        Media::Video("gallery/timelapse.mp4".into()),
        Media::LivePhoto(LivePhotoSource::new(
            "gallery/action.jpg".into(),
            "gallery/action.mov".into()
        )),
        Media::Image("gallery/landscape.jpg".into()),
    ];
    
    let current_media = s!(media_items.get(selected_index).cloned());
    
    vstack((
        // Header
        text!("Media Gallery")
            .size(24.0)
            .frame(Frame::new().margin(Edge::round(16.0))),
            
        // Main media display
        waterui::component::Dynamic::watch(current_media, |media| {
            if let Some(media) = media {
                media
                    .frame(Frame::new().width(600.0).height(400.0))
                    .frame(Frame::new().margin(Edge::round(16.0)))
                    .anyview()
            } else {
                text!("No media available").anyview()
            }
        }),
        
        // Navigation controls
        hstack((
            waterui::component::button::button("β—€ Previous")
                .action_with(&selected_index, |selected_index| {
                    selected_index.update(|idx| if idx > 0 { idx - 1 } else { idx })
                }),
                
            text!("{} / {}", 
                s!(selected_index + 1), 
                media_items.len()
            )
            .frame(Frame::new().margin(Edge::horizontal(16.0))),
            
            waterui::component::button::button("Next β–Ά")
                .action_with(&selected_index, |selected_index| {
                    selected_index.update(|idx| if idx + 1 < media_items.len() { idx + 1 } else { idx })
                })
        ))
        .frame(Frame::new().margin(Edge::round(16.0))),
        
        // Thumbnail strip
        hstack(
            media_items
                .iter()
                .enumerate()
                .map(|(index, media)| {
                    waterui::component::button::button( media.clone().frame(Frame::new().width(80.0).height(60.0)) )
                        .action_with(&selected_index, move |selected_index| selected_index.set(index))
                })
                .collect::<Vec<_>>()
        )
        .frame(Frame::new().margin(Edge::round(16.0)))
    ))
}
}

Best Practices

Loading States

Always provide meaningful placeholder views for images:

#![allow(unused)]
fn main() {
// Good: Informative placeholder
Photo::new("large-image.jpg")
    .placeholder(
        vstack((
            text!("πŸ“Έ"),
            text!("Loading high-resolution image...")
        ))
    )

// Avoid: No placeholder (jarring loading experience)
Photo::new("large-image.jpg")
}

Performance

For large media galleries, consider lazy loading:

#![allow(unused)]
fn main() {
use waterui::View;
use waterui_text::text;
use waterui_layout::stack::vstack;

pub fn efficient_gallery() -> impl View {
    let visible_items = waterui::reactive::Computed::new({
        // Only load visible items based on scroll position
        // Placeholder for example
        Vec::<Media>::new()
    });
    
    // Lazy stacks are experimental; use a regular vstack for now
    vstack(
        visible_items.get().into_iter()
            .map(|media| media.frame(Frame::new().width(300.0).height(200.0)))
            .collect::<Vec<_>>()
    )
}
}

Responsive Design

Make media components responsive to different screen sizes:

#![allow(unused)]
fn main() {
// Example placeholder: responsive sizing requires reading screen metrics from the backend
}

Animation and Transitions

WaterUI provides a revolutionary reactive animation system that leverages nami's metadata capabilities to create smooth, performant animations with zero boilerplate. Unlike traditional animation systems that require explicit animation setup, WaterUI's animations work through reactive value metadata that automatically flows to the renderer.

The Reactive Animation System

At its core, WaterUI's animation system is built on metadata propagation through the reactive system. When you use .animated() on a reactive value, you're attaching animation metadata that travels with every value change. The upstream renderer receives this metadata and handles all the complex interpolation, timing, and rendering automatically.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     .animated()     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Reactive Value  β”‚ ──────────────────> β”‚ Value + Animation        β”‚
β”‚                 β”‚                     β”‚ Metadata Wrapper         β”‚
β”‚ s!(0.0)         β”‚                     β”‚                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                          .set(100.0)                 β”‚
                               β”‚                      β”‚
                               β–Ό                      β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚   nami Context  β”‚ ──> β”‚    Renderer     β”‚ <β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚                 β”‚     β”‚                 β”‚
β”‚ value: 100.0    β”‚     β”‚ - Sees new      β”‚
β”‚ metadata:       β”‚     β”‚   target value  β”‚
β”‚ Animation::     β”‚     β”‚ - Gets animationβ”‚
β”‚ ease_in_out     β”‚     β”‚   config        β”‚
β”‚ (250ms)         β”‚     β”‚ - Creates       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚   interpolator  β”‚
                        β”‚ - Animates      β”‚
                        β”‚   0.0 β†’ 100.0   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Secret: Metadata Flow

The fundamental innovation is that animation metadata flows through nami's reactive system alongside value changes:

use waterui::*;
use nami::s;

fn reactive_animation_demo() -> impl View {
    let position = s!(0.0);
    
    // The .animated() method attaches Animation metadata to the binding
    let animated_position = position.animated();
    
    vstack((
        rectangle()
            .width(50.0)
            .height(50.0)
            .offset_x(animated_position), // Renderer receives value + animation metadata
            .fill(color::BLUE),
        
        button("Move")
            .on_press(move || {
                // When we change the value, the metadata flows to the renderer
                position.set(200.0); // Renderer sees: new_value=200.0 + Animation::default()
            })
    ))
}

Using .animated() - The Core Method

The .animated() method is your primary interface to WaterUI's animation system. It creates a binding wrapper that attaches default animation metadata:

use waterui::*;
use nami::s;

fn animated_examples() -> impl View {
    let opacity = s!(1.0);
    let scale = s!(1.0);
    let color = s!(color::RED);
    
    // Each .animated() call attaches metadata that the renderer will use
    let animated_opacity = opacity.animated();     // Default ease-in-out animation
    let animated_scale = scale.animated();         // Default ease-in-out animation  
    let animated_color = color.animated();         // Default ease-in-out animation
    
    rectangle()
        .width(100.0)
        .height(100.0)
        .opacity(animated_opacity)
        .scale(animated_scale)
        .fill(animated_color)
        .on_tap(move || {
            // All three changes will be animated by the renderer
            opacity.set(0.5);
            scale.set(1.2);  
            color.set(color::BLUE);
        })
}

Practical Examples (Current API)

While some sections below use conceptual shape APIs (marked as rust,ignore), you can use .animated() today with existing view modifiers like .frame(...), .foreground(...), and normal components. Here are two minimal, working examples:

#![allow(unused)]
fn main() {
use waterui::{View, ViewExt, Color};
use waterui::reactive::{binding};
use nami::SignalExt; // map/computed helpers
use waterui::component::layout::stack::{vstack, hstack};
use waterui::component::layout::{Edge, Frame};
use waterui::component::button::button;
use waterui_text::text;

// Animate padding by mapping an animated number to Frame margins
fn animated_padding_demo() -> impl View {
    let pad = binding(8.0);
    let animated_pad = pad.animated();
    let frame = animated_pad.map(|p| Frame::new().margin(Edge::round(p))).computed();

    vstack((
        text!("Animated padding"),
        hstack((
            button("Less").action_with(&pad, |pad| pad.update(|p| (p - 4.0).max(0.0))),
            button("More").action_with(&pad, |pad| pad.update(|p| p + 4.0)),
        )),
    ))
    .frame(frame)
}

// Animate text alpha by mapping an animated number to a Color
fn animated_color_demo() -> impl View {
    let alpha = binding(1.0);
    let animated_alpha = alpha.animated();
    let color = animated_alpha
        .map(|a| Color::from_rgba(0.0, 0.5, 1.0, a))
        .computed();

    vstack((
        text!("Fade me").foreground(color.clone()),
        hstack((
            button("Hide").action_with(&alpha, |a| a.set(0.0)),
            button("Show").action_with(&alpha, |a| a.set(1.0)),
        )),
    ))
}
}

Custom Animation Configurations

While .animated() provides sensible defaults, you can customize animations using the .with_animation() method:

Animation Types Available

WaterUI supports several animation curves, each creating different visual effects:

use waterui::*;
use nami::s;
use std::time::Duration;

fn animation_types_demo() -> impl View {
    let position = s!(0.0);
    
    vstack((
        // Default ease-in-out (250ms) - most common
        rectangle()
            .width(40.0).height(40.0)
            .offset_x(position.animated())  // Uses default animation
            .fill(color::BLUE),
        
        // Linear - constant speed
        rectangle()
            .width(40.0).height(40.0)
            .offset_x(position.with_animation(Animation::linear(Duration::from_millis(500))))
            .fill(color::GREEN),
        
        // Ease-in - starts slow, accelerates
        rectangle()
            .width(40.0).height(40.0)  
            .offset_x(position.with_animation(Animation::ease_in(Duration::from_millis(400))))
            .fill(color::RED),
            
        // Ease-out - starts fast, decelerates
        rectangle()
            .width(40.0).height(40.0)
            .offset_x(position.with_animation(Animation::ease_out(Duration::from_millis(400))))
            .fill(color::ORANGE),
            
        // Spring - physics-based bouncing
        rectangle()
            .width(40.0).height(40.0)
            .offset_x(position.with_animation(Animation::spring(300.0, 20.0)))
            .fill(color::PURPLE),
        
        button("Move All")
            .on_press(move || {
                position.set(if position.get() > 100.0 { 0.0 } else { 200.0 });
            })
    ))
    .spacing(20.0)
}

The Metadata Attachment Process

Here's what happens under the hood when you use .animated():

use waterui::*;
use nami::s;

fn metadata_flow_explanation() -> impl View {
    let opacity = s!(0.5);
    
    // Step 1: Raw reactive value
    let raw_value = opacity.clone();  // Just the binding
    
    // Step 2: Attach animation metadata
    let animated_value = opacity.animated();  // Now wrapped with Animation metadata
    
    // Step 3: When used in a view property, both value + metadata flow to renderer
    text!("Fade me")
        .opacity(animated_value)  // Renderer receives: value=0.5 + metadata=Animation::default()
        .on_tap(move || {
            raw_value.set(1.0);  // Change triggers: value=1.0 + metadata flows through
        })
}

Understanding Metadata Propagation

Animation metadata propagates through reactive computations automatically:

use waterui::*;
use nami::s;

fn metadata_propagation() -> impl View {
    let count = s!(0);
    let animated_count = count.animated();
    
    // Metadata flows through map operations
    let opacity = animated_count.map(|n| (n as f32 / 10.0).clamp(0.0, 1.0));
    let scale = animated_count.map(|n| 1.0 + (n as f32 * 0.1));
    
    // Both opacity and scale inherit the animation metadata from count
    rectangle()
        .width(100.0).height(100.0)
        .opacity(opacity)    // Animated because count is animated
        .scale(scale)        // Also animated because count is animated
        .fill(color::BLUE)
        .on_tap(move || {
            count.set((count.get() + 1) % 11);  // 0-10 range
        })
}

How Renderers Consume Animation Metadata

When a renderer (like GTK4 or Web backend) receives a reactive value change with animation metadata, it automatically handles the interpolation process:

The Rendering Pipeline

User Code                nami System              Renderer                 UI Output
─────────                ───────────              ────────                 ─────────

position.set(100.0) ──> Context {        ──────> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                         value: 100.0             β”‚ Current: 0.0    β”‚    β”‚ β–’           β”‚
                         metadata:                β”‚ Target:  100.0  β”‚    β”‚ β–’           β”‚
                         Animation::              β”‚                 β”‚    β”‚ β–’           β”‚
                         spring(300,20)           β”‚ Creates Spring  β”‚    β”‚ β–’           β”‚
                        }                         β”‚ Interpolator    β”‚    β”‚ β–’           β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚               frame 1 (t=0ms)
                                                           β”‚
                                                    Timer triggers 60fps
                                                           β”‚
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚ Spring calc:    β”‚    β”‚  β–’          β”‚
                                                  β”‚ t=0.1 β†’ 15.0   β”‚    β”‚  β–’          β”‚
                                                  β”‚ Apply to UI     β”‚    β”‚  β–’          β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚               frame 6 (t=100ms)
                                                           β”‚
                                                    Continue until
                                                    target reached
                                                           β”‚
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚ Final position  β”‚    β”‚            β–’β”‚
                                                  β”‚ reached: 100.0  β”‚    β”‚            β–’β”‚
                                                  β”‚ Remove timer    β”‚    β”‚            β–’β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                         final position

Renderer Implementation Details

Here's a simplified view of how renderers handle animation metadata:

// Pseudocode showing renderer animation handling
impl Renderer {
    fn handle_property_change<T>(&mut self, context: Context<T>) 
    where 
        T: Interpolatable + Clone 
    {
        let Context { value: new_value, metadata } = context;
        
        // Check if animation metadata is present
        if let Some(animation) = metadata.get::<Animation>() {
            // Create interpolator based on animation type
            let interpolator = match animation {
                Animation::Linear(duration) => LinearInterpolator::new(duration),
                Animation::EaseInOut(duration) => EaseInterpolator::new(duration),
                Animation::Spring { stiffness, damping } => SpringInterpolator::new(stiffness, damping),
                Animation::Default => EaseInterpolator::new(Duration::from_millis(250)),
            };
            
            // Start animation from current_value to new_value
            self.start_animation(interpolator, self.current_value, new_value);
        } else {
            // No animation metadata - update immediately
            self.current_value = new_value;
            self.render_immediately();
        }
    }
}

Animation Interpolation Types

Different animation types use different mathematical functions for interpolation:

// How different animation curves work internally
fn interpolate_value(animation: &Animation, progress: f32) -> f32 {
    match animation {
        Animation::Linear(_) => progress,  // t
        
        Animation::EaseIn(_) => progress * progress,  // tΒ²
        
        Animation::EaseOut(_) => 1.0 - (1.0 - progress).powi(2),  // 1-(1-t)Β²
        
        Animation::EaseInOut(_) => {
            if progress < 0.5 {
                2.0 * progress * progress  // 2tΒ² for first half
            } else {
                1.0 - (-2.0 * progress + 2.0).powi(2) / 2.0  // Smooth transition for second half
            }
        },
        
        Animation::Spring { stiffness, damping } => {
            // Complex physics simulation using spring equations
            spring_interpolation(progress, stiffness, damping)
        },
    }
}

Zero-Cost Abstractions

The beauty of WaterUI's animation system is that it provides zero-cost abstractions:

  • No Animation Metadata: If a value has no animation metadata, it updates immediately with no overhead
  • With Animation Metadata: The renderer automatically creates the appropriate interpolator
  • Type Safety: Animation metadata is type-erased but type-safe through nami's metadata system
  • Composability: Multiple animated properties work independently without interference
use waterui::*;
use nami::s;

fn zero_cost_demo() -> impl View {
    let animated_opacity = s!(1.0).animated();      // Gets interpolator 
    let instant_opacity = s!(1.0);                  // Updates immediately
    let animated_position = s!(0.0).with_animation( // Gets custom interpolator
        Animation::spring(200.0, 15.0)
    );
    
    rectangle()
        .opacity(animated_opacity)     // Renderer creates opacity interpolator
        .width(instant_opacity)        // Renderer updates width immediately  
        .offset_x(animated_position)   // Renderer creates position interpolator
        .height(100.0)                 // Static value - no computation
        .fill(color::BLUE)
}

Complete Animation Flow Example

Here's a practical example showing the complete metadata flow in a button component:

use waterui::*;
use nami::s;

fn animated_button_demo() -> impl View {
    let is_pressed = s!(false);
    let is_hovered = s!(false);
    
    // Create animated reactive values
    let scale = is_pressed.map(|pressed| if *pressed { 0.95 } else { 1.0 }).animated();
    let bg_color = is_hovered.map(|hovered| {
        if *hovered { color::BLUE } else { color::GRAY }  
    }).with_animation(Animation::ease_out(Duration::from_millis(150)));
    
    /*
    Metadata Flow Visualization:
    
    User hovers ──> is_hovered.set(true) ──> map() ──> bg_color gets new value + Animation metadata
                                             β”‚
                                             β–Ό
                                     Renderer receives:
                                     Context {
                                         value: BLUE,
                                         metadata: Animation::ease_out(150ms)
                                     }
                                             β”‚  
                                             β–Ό
                                     Creates color interpolator GRAY β†’ BLUE over 150ms
    */
    
    rectangle()
        .width(120.0)
        .height(40.0)
        .scale(scale)              // Gets scale animation metadata 
        .fill(bg_color)            // Gets color animation metadata
        .corner_radius(8.0)
        .overlay(
            text!("Animated Button")
                .color(color::WHITE)
        )
        .on_press(move || {
            is_pressed.set(true);
            // Animation automatically triggered by metadata flow
        })
        .on_release(move || {
            is_pressed.set(false); 
        })
        .on_hover(move || {
            is_hovered.set(true);
        })
        .on_hover_end(move || {
            is_hovered.set(false);
        })
}

Metadata Flow Through Complex Computations

Animation metadata propagates through reactive computations, creating sophisticated animations with simple code:

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Base Signal     β”‚
                    β”‚ count.animated()β”‚
                    β”‚                 β”‚
                    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚ Animation metadata attached
                          β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                     β”‚                     β”‚
    β–Ό                     β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ .map()  β”‚         β”‚ .map()  β”‚         β”‚ .map()  β”‚
β”‚ opacity β”‚         β”‚ scale   β”‚         β”‚ rotationβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚                     β”‚                     β”‚
    β”‚ metadata flows      β”‚ metadata flows      β”‚ metadata flows
    β”‚                     β”‚                     β”‚
    β–Ό                     β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Renderer β”‚         β”‚Renderer β”‚         β”‚Renderer β”‚
β”‚creates  β”‚         β”‚creates  β”‚         β”‚creates  β”‚
β”‚opacity  β”‚         β”‚scale    β”‚         β”‚rotation β”‚
β”‚animator β”‚         β”‚animator β”‚         β”‚animator β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
fn complex_metadata_flow() -> impl View {
    let count = s!(0).animated();  // Single source of animation metadata
    
    // All these computed values inherit the animation metadata from count
    let opacity = count.map(|n| (*n as f32 / 10.0).clamp(0.0, 1.0));
    let scale = count.map(|n| 1.0 + (*n as f32 * 0.05));  
    let rotation = count.map(|n| *n as f32 * 36.0); // 36Β° per increment
    let bg_color = count.map(|n| {
        let hue = (*n as f32 * 30.0) % 360.0;
        Color::from_hsv(hue, 1.0, 1.0)
    });
    
    rectangle()
        .width(100.0).height(100.0)
        .opacity(opacity)     // Animated - renderer gets metadata from count
        .scale(scale)         // Animated - renderer gets metadata from count  
        .rotation(rotation)   // Animated - renderer gets metadata from count
        .fill(bg_color)       // Animated - renderer gets metadata from count
        .on_tap(move || {
            count.set((count.get() + 1) % 11);  // Single change animates everything
        })
}

Animatable Properties

WaterUI can animate most visual properties smoothly:

Transform Properties

Transform properties are ideal for animations as they're GPU-accelerated and don't trigger layout recalculations:

use waterui::*;
use nami::s;

fn transform_animations() -> impl View {
    let offset_x = s!(0.0);
    let offset_y = s!(0.0); 
    let scale = s!(1.0);
    let rotation = s!(0.0);
    
    // Apply animations to transform properties using reactive values
    rectangle()
        .width(100.0).height(100.0)
        .offset_x(offset_x.animated())           // Spring animation for smooth movement
        .offset_y(offset_y.with_animation(Animation::ease_out(Duration::from_millis(400))))
        .scale(scale.with_animation(Animation::spring(200.0, 15.0)))  // Bouncy scaling
        .rotation(rotation.animated())           // Default ease for rotation
        .fill(color::RED)
        .overlay(
            vstack((
                hstack((
                    button("β†’")
                        .on_press({
                            let x = offset_x.clone();
                            move || x.set(if x.get() > 50.0 { 0.0 } else { 100.0 })
                        }),
                    button("↓")  
                        .on_press({
                            let y = offset_y.clone();
                            move || y.set(if y.get() > 30.0 { 0.0 } else { 60.0 })
                        }),
                    button("⚑")
                        .on_press({
                            let s = scale.clone();
                            move || s.set(if s.get() > 1.1 { 1.0 } else { 1.5 })
                        }),
                    button("↻")
                        .on_press(move || {
                            rotation.update(|r| r + 90.0);
                        }),
                ))
                .spacing(5.0),
                
                button("Reset All")
                    .on_press({
                        let x = offset_x.clone();
                        let y = offset_y.clone();
                        let s = scale.clone();
                        let r = rotation.clone();
                        move || {
                            x.set(0.0);
                            y.set(0.0);
                            s.set(1.0);
                            r.set(0.0);
                        }
                    })
            ))
            .spacing(10.0)
            .frame(waterui::component::layout::Frame::new().margin(waterui::component::layout::Edge::round(10.0)))
        )
}

Color Animations

Color transitions create smooth theme changes and visual feedback:

use waterui::*;
use nami::s;

fn color_animations() -> impl View {
    let theme_index = s!(0);
    
    // Define color themes
    let themes = [
        (color::BLUE, color::WHITE, "Ocean"),
        (color::RED, color::YELLOW, "Sunset"), 
        (color::GREEN, color::BLACK, "Forest"),
        (color::PURPLE, color::WHITE, "Royal"),
    ];
    
    // Map theme index to colors with animation
    let bg_color = theme_index.map(|&idx| themes[idx % 4].0).animated();
    let text_color = theme_index.map(|&idx| themes[idx % 4].1).with_animation(
        Animation::ease_in_out(Duration::from_millis(300))
    );
    let theme_name = theme_index.map(|&idx| themes[idx % 4].2);
    
    vstack((
        // Animated color display
        rectangle()
            .width(200.0).height(100.0)
            .fill(bg_color)
            .corner_radius(12.0)
            .overlay(
                text!(theme_name)
                    .size(24.0)
                    .color(text_color)
            ),
        
        // Theme selection buttons  
        hstack((
            button("← Prev")
                .on_press({
                    let idx = theme_index.clone();
                    move || idx.update(|i| (*i + 3) % 4)  // Wrap around backwards
                }),
            button("Next β†’")
                .on_press(move || {
                    theme_index.update(|i| (*i + 1) % 4)  // Cycle forward
                }),
        ))
        .spacing(20.0),
        
        // Random theme button
        button("🎲 Random Theme")
            .on_press({
                let idx = theme_index.clone();
                move || {
                    use std::collections::hash_map::DefaultHasher;
                    use std::hash::{Hash, Hasher};
                    let mut hasher = DefaultHasher::new();
                    std::time::SystemTime::now().hash(&mut hasher);
                    idx.set((hasher.finish() as usize) % 4);
                }
            })
    ))
    .spacing(30.0)
}

Size and Layout Animations

Size animations can create expand/collapse effects and responsive layouts:

use waterui::*;
use nami::s;

fn size_animations() -> impl View {
    let is_expanded = s!(false);
    
    // Map boolean state to size values with different animations
    let width = is_expanded.map(|&expanded| if expanded { 250.0 } else { 100.0 })
        .with_animation(Animation::spring(180.0, 12.0));  // Bouncy width
    let height = is_expanded.map(|&expanded| if expanded { 180.0 } else { 100.0 })
        .with_animation(Animation::ease_out(Duration::from_millis(400)));  // Smooth height
    let corner_radius = is_expanded.map(|&expanded| if expanded { 20.0 } else { 8.0 }).animated();
    let padding = is_expanded.map(|&expanded| if expanded { 20.0 } else { 10.0 }).animated();
    
    vstack((
        // Animated container
        rectangle()
            .width(width)
            .height(height) 
            .corner_radius(corner_radius)
            .fill(color::PURPLE)
            .overlay(
                vstack((
                    text!("πŸ“¦")
                        .size(32.0),
                    text!(is_expanded.map(|&exp| if exp { "Expanded!" } else { "Compact" }))
                        .color(color::WHITE)
                        .size(16.0),
                ))
                .spacing(8.0)
                .padding(padding)
            ),
        
        // Control buttons
        hstack((
            button(is_expanded.map(|&exp| if exp { "πŸ“¦ Collapse" } else { "πŸ“‚ Expand" }))
                .on_press({
                    let expanded = is_expanded.clone();
                    move || expanded.update(|e| !e)
                }),
                
            button("πŸ”„ Quick Toggle")
                .on_press(move || {
                    let exp = is_expanded.clone();
                    // Rapid toggle demonstration
                    tokio::spawn(async move {
                        for _ in 0..3 {
                            exp.update(|e| !e);
                            tokio::time::sleep(Duration::from_millis(300)).await;
                        }
                    });
                })
        ))
        .spacing(15.0),
        
        // Status indicator
        text!(is_expanded.map(|&exp| {
            format!("State: {} | Size: {}x{}", 
                if exp { "EXPANDED" } else { "COMPACT" },
                if exp { "250" } else { "100" },
                if exp { "180" } else { "100" }
            )
        }))
        .size(12.0)
        .color(color::GRAY)
    ))
    .spacing(25.0)
}

Complex Animation Sequences

For more complex animations, you can chain and combine multiple animations:

Sequential Animations

use std::time::Duration;
use tokio::time::sleep;

fn sequential_animation() -> impl View {
    let position = s!(Offset::zero());
    let scale = s!(1.0);
    let opacity = s!(1.0);
    
    let animated_circle = circle()
        .size(50.0)
        .offset(position.clone())
        .scale(scale.clone())
        .opacity(opacity.clone())
        .animation(Animation::ease_in_out(Duration::from_millis(500)))
        .fill(Color::orange());
    
    vstack((
        animated_circle,
        
        button("Animate Sequence")
            .on_press({
                let position = position.clone();
                let scale = scale.clone();
                let opacity = opacity.clone();
                move || {
                    let pos = position.clone();
                    let sc = scale.clone();
                    let op = opacity.clone();
                    
                    tokio::spawn(async move {
                        // Step 1: Move right
                        pos.set(Offset::new(100.0, 0.0));
                        sleep(Duration::from_millis(500)).await;
                        
                        // Step 2: Scale up
                        sc.set(1.5);
                        sleep(Duration::from_millis(500)).await;
                        
                        // Step 3: Fade out
                        op.set(0.3);
                        sleep(Duration::from_millis(500)).await;
                        
                        // Step 4: Return to original state
                        pos.set(Offset::zero());
                        sc.set(1.0);
                        op.set(1.0);
                    });
                }
            }),
    ))
    .spacing(30.0)
}

Parallel Animations

fn parallel_animations() -> impl View {
    let transform = s!(Transform::identity());
    let color = s!(Color::blue());
    
    rectangle()
        .size(100.0)
        .transform(transform.clone())
        .fill(color.clone())
        .animation(Animation::spring())
        .on_tap(move || {
            // Both animations happen simultaneously
            transform.set(Transform::identity()
                .scaled(1.5)
                .rotated(45.0)
                .translated(50.0, 25.0));
            color.set(Color::red());
        })
}

Gesture-Driven Animations

WaterUI animations work seamlessly with gesture recognizers:

Drag Animations

fn draggable_animation() -> impl View {
    let position = s!(Offset::zero());
    let is_dragging = s!(false);
    
    circle()
        .size(80.0)
        .offset(position.clone())
        .scale(s!(if is_dragging { 1.1 } else { 1.0 }))
        .animation(Animation::spring())
        .fill(Color::green())
        .gesture(
            DragGesture::new()
                .on_started(move |_| is_dragging.set(true))
                .on_changed({
                    let position = position.clone();
                    move |delta| position.update(|pos| pos + delta.translation)
                })
                .on_ended(move |_| is_dragging.set(false))
        )
}

Swipe Animations

fn swipe_cards() -> impl View {
    let cards = s!(vec!["Card 1", "Card 2", "Card 3", "Card 4"));
    let current_offset = s!(0.0);
    
    zstack(
        cards.iter().enumerate().map(|(index, card)| {
            let offset = s!(current_offset + (index as f32 * 300.0));
            
            rectangle()
                .width(250.0)
                .height(150.0)
                .offset_x(offset)
                .corner_radius(10.0)
                .fill(Color::blue())
                .animation(Animation::spring())
                .overlay(
                    text(card)
                        .color(Color::white())
                        .size(18.0)
                )
                .gesture(
                    DragGesture::new()
                        .on_ended({
                            let current_offset = current_offset.clone();
                            move |details| {
                                if details.velocity.x.abs() > 500.0 {
                                    // Snap to next card
                                    current_offset.update(|offset| {
                                        let new_offset = if details.velocity.x < 0.0 {
                                            offset - 300.0
                                        } else {
                                            offset + 300.0
                                        };
                                        new_offset.max(-600.0).min(0.0)
                                    });
                                }
                            }
                        })
                )
        }).collect::<Vec<_>>()
    )
}

Performance Optimization

Animation Performance Tips

  1. Use Transform Properties: Transform properties (translate, scale, rotate) are GPU-accelerated:
// βœ… Efficient - uses transforms
fn efficient_animation() -> impl View {
    let scale = s!(1.0);
    
    circle()
        .width(100.0).height(100.0)
        .scale(scale.animated())  // GPU-accelerated, metadata-driven animation
        .fill(color::BLUE)
        .on_tap(move || scale.update(|s| if *s > 1.1 { 1.0 } else { 1.3 }))
}

// ❌ Less efficient - changes layout
fn less_efficient_animation() -> impl View {
    let size = s!(100.0);
    
    circle()
        .width(size.animated())  // May trigger layout recalculation
        .height(size.clone())    // Better to use transform scale instead
        .fill(color::BLUE)
        .on_tap(move || size.update(|s| if *s > 110.0 { 100.0 } else { 150.0 }))
}
  1. Leverage Reactive Computations: Use reactive patterns to batch logical updates:
use waterui::*;
use nami::s;

fn efficient_batched_updates() -> impl View {
    let interaction_state = s!(0); // Single source of truth
    
    // All animations derive from one state change - efficient!
    let scale = interaction_state.map(|&state| match state {
        0 => 1.0,      // Normal
        1 => 1.1,      // Hover
        2 => 0.95,     // Pressed
        _ => 1.0,
    }).animated();
    
    let color = interaction_state.map(|&state| match state {
        0 => color::BLUE,
        1 => color::CYAN, 
        2 => color::NAVY,
        _ => color::BLUE,
    }).with_animation(Animation::ease_in_out(Duration::from_millis(150)));
    
    let rotation = interaction_state.map(|&state| (state as f32) * 15.0).animated();
    
    rectangle()
        .width(100.0).height(100.0)
        .scale(scale)       // All three properties animate from single state change
        .fill(color)        // Efficient: one reactive update β†’ three animations
        .rotation(rotation)
        .on_hover(move || interaction_state.set(1))
        .on_press(move || interaction_state.set(2)) 
        .on_release(move || interaction_state.set(0))
}
  1. Use Appropriate Animation Curves: Choose the right animation type for your use case:
fn optimized_animations() -> impl View {
    vstack((
        // For UI feedback - use spring for natural feel
        button("Spring Animation")
            .animation(Animation::spring()),
        
        // For loading indicators - use linear for consistency  
        progress_bar()
            .animation(Animation::linear(Duration::from_millis(1000))),
        
        // For page transitions - use ease curves
        page_transition()
            .animation(Animation::ease_in_out(Duration::from_millis(300))),
    ))
}

Custom Animation Curves

You can create custom animation curves for unique effects:

fn custom_curve_animation() -> impl View {
    let progress = s!(0.0);
    
    // Custom bounce curve
    let bounce_curve = |t: f32| -> f32 {
        if t < 1.0 / 2.75 {
            7.5625 * t * t
        } else if t < 2.0 / 2.75 {
            let t = t - 1.5 / 2.75;
            7.5625 * t * t + 0.75
        } else if t < 2.5 / 2.75 {
            let t = t - 2.25 / 2.75;
            7.5625 * t * t + 0.9375
        } else {
            let t = t - 2.625 / 2.75;
            7.5625 * t * t + 0.984375
        }
    };
    
    circle()
        .size(60.0)
        .offset_y(s!(progress * 200.0))
        .animation(Animation::custom(
            Duration::from_millis(1000),
            bounce_curve
        ))
        .fill(Color::red())
        .on_tap(move || {
            progress.update(|p| if p > 0.5 { 0.0 } else { 1.0 });
        })
}

Animation State Management

Animation Controllers

For complex animation sequences, use animation controllers:

struct AnimationController {
    is_playing: Binding<bool>,
    progress: Binding<f32>,
    direction: Binding<i32>, // 1 for forward, -1 for reverse
}

impl AnimationController {
    fn new() -> Self {
        Self {
            is_playing: s!(false),
            progress: s!(0.0),
            direction: s!(1),
        }
    }
    
    fn play(&self) {
        self.is_playing.set(true);
        self.animate_to_end();
    }
    
    fn reverse(&self) {
        self.direction.set(-1);
        self.is_playing.set(true);
        self.animate_to_start();
    }
    
    fn animate_to_end(&self) {
        let progress = self.progress.clone();
        let is_playing = self.is_playing.clone();
        
        tokio::spawn(async move {
            while progress.with(|p| *p < 1.0) {
                progress.update(|p| (p + 0.02).min(1.0));
                sleep(Duration::from_millis(16)).await; // ~60 FPS
            }
            is_playing.set(false);
        });
    }
    
    fn animate_to_start(&self) {
        let progress = self.progress.clone();
        let is_playing = self.is_playing.clone();
        
        tokio::spawn(async move {
            while progress.with(|p| *p > 0.0) {
                progress.update(|p| (p - 0.02).max(0.0));
                sleep(Duration::from_millis(16)).await;
            }
            is_playing.set(false);
        });
    }
}

fn controlled_animation() -> impl View {
    let controller = AnimationController::new();
    let scale = controller.progress.map(|p| 1.0 + p * 0.5);
    let rotation = controller.progress.map(|p| p * 360.0);
    
    vstack((
        rectangle()
            .size(100.0)
            .scale(scale.clone())
            .rotation(rotation.clone())
            .fill(Color::blue()),
        
        hstack((
            button("Play")
                .on_press({
                    let controller = controller.clone();
                    move || controller.play()
                }),
            
            button("Reverse")
                .on_press(move || controller.reverse()),
        ))
        .spacing(10.0),
    ))
    .spacing(20.0)
}

Real-World Animation Examples

Loading Animations

fn loading_animations() -> impl View {
    let rotation = s!(0.0);
    let pulse_scale = s!(1.0);
    
    // Start continuous animations
    let rotation_clone = rotation.clone();
    tokio::spawn(async move {
        loop {
            rotation_clone.update(|r| r + 2.0);
            sleep(Duration::from_millis(16)).await;
        }
    });
    
    let pulse_clone = pulse_scale.clone();
    tokio::spawn(async move {
        let growing = binding(true);
        loop {
            let current_val = pulse_clone.with(|p| *p);
            let is_growing = growing.with(|g| *g);
            
            if is_growing {
                let new_val = current_val + 0.01;
                pulse_clone.set(new_val);
                if new_val >= 1.2 { growing.set(false); }
            } else {
                let new_val = current_val - 0.01;
                pulse_clone.set(new_val);
                if new_val <= 0.8 { growing.set(true); }
            }
            sleep(Duration::from_millis(16)).await;
        }
    });
    
    vstack((
        // Spinning loader
        circle()
            .size(40.0)
            .stroke(Color::blue(), 4.0)
            .stroke_dash([10.0, 5.0))
            .rotation(rotation.clone())
            .animation(Animation::linear(Duration::from_millis(16))),
        
        // Pulsing dot
        circle()
            .size(20.0)
            .scale(pulse_scale.clone())
            .animation(Animation::ease_in_out(Duration::from_millis(16)))
            .fill(Color::green()),
        
        text("Loading...")
            .color(Color::gray()),
    ))
    .spacing(20.0)
}

Page Transitions

fn page_transition_example() -> impl View {
    let current_page = s!(0);
    let transition_offset = s!(0.0);
    
    let pages = vec!["Home", "Profile", "Settings"];
    
    vstack((
        // Page content with slide transition
        zstack(
            pages.iter().enumerate().map(|(index, page)| {
                let offset = s!((index as f32 - current_page as f32) * 300.0 + transition_offset);
                
                rectangle()
                    .width(300.0)
                    .height(200.0)
                    .offset_x(offset)
                    .fill(Color::white())
                    .border(Color::gray(), 1.0)
                    .animation(Animation::ease_out(Duration::from_millis(300)))
                    .overlay(
                        text(page)
                            .size(24.0)
                            .color(Color::black())
                    )
            }).collect::<Vec<_>>()
        ),
        
        // Navigation
        hstack(
            pages.iter().enumerate().map(|(index, page)| {
                button(page)
                    .on_press({
                        let current_page = current_page.clone();
                        move || current_page.set(index)
                    })
                    .background(s!(if current_page == index { 
                        Color::blue() 
                    } else { 
                        Color::gray() 
                    }))
            }).collect::<Vec<_>>()
        )
        .spacing(10.0),
    ))
    .spacing(20.0)
}

Animation Testing and Debugging

Animation Inspector

fn animation_debug_view(animated_view: impl View) -> impl View {
    let show_debug = s!(false);
    
    vstack((
        animated_view,
        
        when(show_debug.clone(), |debug| if debug {
            vstack((
                text("Animation Debug Info"),
                text("Frame Rate: 60 FPS"),
                text("GPU Accelerated: Yes"),
                text("Active Animations: 2"),
            ))
            .background(waterui::background::Background::color((0.0, 0.0, 0.0, 0.8)))
            .color(Color::white())
            .frame(waterui::component::layout::Frame::new().margin(waterui::component::layout::Edge::round(10.0)))
            .into_view()
        } else {
            empty().into_view()
        }),
        
        button("Toggle Debug")
            .on_press(move || show_debug.update(|d| !d)),
    ))
    .spacing(10.0)
}

Best Practices

  1. Keep Animations Purposeful: Every animation should serve a purpose - providing feedback, guiding attention, or enhancing understanding.

  2. Follow Platform Conventions: Respect platform-specific animation durations and curves.

  3. Test Performance: Profile your animations on target devices to ensure smooth performance.

  4. Provide Accessibility Options: Allow users to disable animations if needed for accessibility.

  5. Use Appropriate Durations:

    • Micro-interactions: 100-200ms
    • Page transitions: 300-500ms
    • Loading animations: Continuous
    • Attention-seeking: 500-800ms
  6. Optimize for Battery Life: Avoid unnecessary continuous animations on mobile devices.

By following these patterns and best practices, you can create smooth, performant animations that enhance your WaterUI applications' user experience while maintaining good performance across all target platforms.

Suspense and Asynchronous Loading

Modern applications often need to load data asynchronously from APIs, databases, or other sources. The Suspense component in WaterUI provides an elegant way to handle async content loading while maintaining a responsive user interface. This chapter covers everything you need to know about implementing suspense in your applications.

Basic Suspense Usage

The simplest way to use Suspense is with async functions that return views:

#![allow(unused)]
fn main() {
use waterui::{View};
use waterui_text::text;
use waterui::component::layout::stack::{vstack};
use waterui::widget::suspense::Suspense;

// Async function that loads data
async fn load_user_profile(user_id: u32) -> impl View {
    // Simulate API call
    tokio::time::sleep(Duration::from_secs(2)).await;
    
    let user_data = fetch_user_data(user_id).await;
    
    vstack((
        text!("Name: {}", user_data.name),
        text!("Email: {}", user_data.email),
        text!("Joined: {}", user_data.joined_date),
    ))
}

fn user_profile_view(user_id: u32) -> impl View {
    // Basic suspense with custom loading view
    Suspense::new(load_user_profile(user_id))
        .loading(text!("Loading user profile..."))
}
}

Using Default Loading Views

You can set up default loading views in your application's environment:

#![allow(unused)]
fn main() {
use waterui::{Environment};
use waterui::view::AnyViewBuilder;
use waterui::widget::suspense::{Suspense, DefaultLoadingView};
use waterui_text::text;
use waterui::component::layout::stack::vstack;
use waterui::component::layout::{Edge, Frame};

// Set up default loading view in your app
fn setup_app_environment() -> Environment {
    let loading_view = AnyViewBuilder::new(|_| {
        vstack((
            text!("Loading..."),
        ))
        .frame(Frame::new().margin(Edge::round(20.0)))
    });

    Environment::new().with(DefaultLoadingView(loading_view))
}

// Components can now use the default loading view
fn simple_async_view() -> impl View {
    Suspense::new(load_data) // Uses default loading view from environment
}
}

The SuspendedView Trait

Any type can be used with Suspense by implementing the SuspendedView trait. The trait is automatically implemented for any Future that resolves to a View.

Automatic Implementation for Futures

#![allow(unused)]
fn main() {
use waterui::widget::suspense::{Suspense, SuspendedView};
use waterui_text::text;

// These all work with Suspense automatically:

// 1. Async functions
async fn fetch_weather() -> impl View {
    let weather = get_weather_data().await;
    text!("Temperature: {}Β°F", weather.temperature)
}

// 2. Async closures
let load_news = async move || {
    let articles = fetch_news_articles().await;
    news_list_view(articles)
};

// 3. Future types
use std::future::Future;
use std::pin::Pin;

type BoxedFuture = Pin<Box<dyn Future<Output = impl View>>>;

fn get_async_content() -> BoxedFuture {
    Box::pin(async {
        text!("Async content loaded!")
    })
}

// All work with Suspense:
let weather_view = Suspense::new(fetch_weather);
let news_view = Suspense::new(load_news);
let content_view = Suspense::new(get_async_content());
}

Custom SuspendedView Implementation

For more complex scenarios, you can implement SuspendedView manually:

#![allow(unused)]
fn main() {
use waterui::{Environment, View};
use waterui::widget::suspense::SuspendedView;
use waterui_text::text;
use waterui::component::layout::stack::vstack;

struct DataLoader {
    user_id: u32,
    include_posts: bool,
}

impl SuspendedView for DataLoader {
    async fn body(self, _env: Environment) -> impl View {
        // Custom loading logic with environment access
        let user = fetch_user(self.user_id).await;
        
        if self.include_posts {
            let posts = fetch_user_posts(self.user_id).await;
            vstack((
                user_profile_view(user),
                posts_list_view(posts),
            ))
        } else {
            user_profile_view(user)
        }
    }
}

// Usage
fn user_dashboard(user_id: u32, show_posts: bool) -> impl View {
    Suspense::new(DataLoader {
        user_id,
        include_posts: show_posts,
    })
    .loading(text!("Loading dashboard..."))
}
}

[WIP]