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