Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Blinc is a GPU-accelerated, reactive UI framework for Rust. It provides a declarative, component-based approach to building high-performance user interfaces with smooth animations and modern visual effects.

Why Blinc?

  • GPU-Accelerated Rendering - All rendering is done on the GPU via wgpu, enabling smooth 60fps animations and complex visual effects like glass materials and shadows.

  • Declarative UI - Build interfaces using a fluent, composable API inspired by SwiftUI and modern web frameworks. No manual DOM manipulation.

  • Reactive State - Automatic UI updates when state changes, with fine-grained reactivity for optimal performance.

  • Spring Physics - Natural, physics-based animations using spring dynamics instead of fixed durations.

  • Cross-Platform - Runs on macOS, Windows, Linux, and Android (iOS coming soon).

Key Features

Flexbox Layout

All layout is powered by Taffy, a high-performance flexbox implementation. Use familiar CSS-like properties:

#![allow(unused)]
fn main() {
div()
    .flex_col()
    .gap(16.0)
    .p(24.0)
    .child(text("Hello"))
    .child(text("World"))
}

Material Effects

Built-in support for glass, metallic, and other material effects:

#![allow(unused)]
fn main() {
div()
    .glass()
    .rounded(16.0)
    .p(24.0)
    .child(text("Frosted Glass"))
}

Type-Safe Animations

The BlincComponent derive macro generates type-safe animation hooks:

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct MyCard {
    #[animation]
    scale: f32,
    #[animation]
    opacity: f32,
}

// Usage
let scale = MyCard::use_scale(ctx, 1.0, SpringConfig::snappy());
let opacity = MyCard::use_opacity(ctx, 0.0, SpringConfig::gentle());
}

Event Handling

Intuitive event handling with closures:

#![allow(unused)]
fn main() {
div()
    .on_click(|_| println!("Clicked!"))
    .on_hover_enter(|_| println!("Hovered"))
}

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                   Your Application                   │
├─────────────────────────────────────────────────────┤
│  blinc_app   │  WindowedApp, Context, State Hooks   │
├──────────────┼──────────────────────────────────────┤
│  blinc_layout│  Elements, Flexbox, Event Routing    │
├──────────────┼──────────────────────────────────────┤
│  blinc_animation │  Springs, Timelines, Motion      │
├──────────────┼──────────────────────────────────────┤
│  blinc_gpu   │  Render Pipeline, Materials          │
├──────────────┼──────────────────────────────────────┤
│  wgpu        │  GPU Abstraction Layer               │
└─────────────────────────────────────────────────────┘

Quick Example

Here’s a minimal Blinc application:

use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

fn main() -> Result<()> {
    WindowedApp::run(WindowConfig::default(), |ctx| {
        div()
            .w(ctx.width)
            .h(ctx.height)
            .bg(Color::rgba(0.1, 0.1, 0.15, 1.0))
            .flex_center()
            .child(
                div()
                    .glass()
                    .rounded(16.0)
                    .p(32.0)
                    .child(text("Hello, Blinc!").size(24.0).color(Color::WHITE))
            )
    })
}

Next Steps

Installation

Prerequisites

Blinc requires:

  • Rust 1.70+ (for stable async and other features)
  • A GPU with Vulkan, Metal, or DX12 support

Adding Blinc to Your Project

Add blinc_app to your Cargo.toml:

[dependencies]
blinc_app = { version = "0.1", features = ["windowed"] }

The windowed feature enables desktop windowing support. For headless rendering (e.g., server-side), omit this feature.

Feature Flags

FeatureDescription
windowedDesktop window support via winit (default)
androidAndroid platform support

Verifying Installation

Create a simple test application:

// src/main.rs
use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

fn main() -> Result<()> {
    WindowedApp::run(WindowConfig::default(), |ctx| {
        div()
            .w(ctx.width)
            .h(ctx.height)
            .bg(Color::rgba(0.1, 0.1, 0.15, 1.0))
            .flex_center()
            .child(text("Blinc is working!").size(32.0).color(Color::WHITE))
    })
}

Run with:

cargo run

You should see a window with “Blinc is working!” displayed in the center.

For a better development experience, add these to your Cargo.toml:

[dev-dependencies]
tracing-subscriber = "0.3"

Then initialize logging in your app:

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    WindowedApp::run(/* ... */)
}

Platform-Specific Notes

macOS

No additional setup required. Blinc uses Metal for GPU rendering.

Windows

Ensure you have up-to-date GPU drivers. Blinc uses DX12 by default, falling back to Vulkan.

Linux

Install Vulkan development libraries:

# Ubuntu/Debian
sudo apt install libvulkan-dev

# Fedora
sudo dnf install vulkan-devel

# Arch
sudo pacman -S vulkan-icd-loader

Android

See the Android platform guide for cross-compilation setup.

Your First App

Let’s build a simple counter application to learn Blinc fundamentals.

The Basic Structure

Every Blinc windowed application follows this pattern:

use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

fn main() -> Result<()> {
    WindowedApp::run(WindowConfig::default(), |ctx| {
        // Your UI goes here
        build_ui(ctx)
    })
}

fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .w(ctx.width)
        .h(ctx.height)
        // ... children
}

The WindowedApp::run function:

  1. Creates a window with the given configuration
  2. Sets up the GPU renderer
  3. Calls your UI builder function when needed
  4. Handles events and animations automatically

Building a Counter

Let’s create a counter with increment and decrement buttons.

Step 1: Window Configuration

use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

fn main() -> Result<()> {
    let config = WindowConfig {
        title: "Counter App".to_string(),
        width: 400,
        height: 300,
        resizable: true,
        ..Default::default()
    };

    WindowedApp::run(config, |ctx| build_ui(ctx))
}

Step 2: Creating State

Use use_state_keyed to create reactive state that persists across UI rebuilds:

#![allow(unused)]
fn main() {
fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    // Create keyed state for the count - persists across rebuilds
    let count = ctx.use_state_keyed("counter", || 0i32);

    // State will be read inside stateful elements via .deps()
    // ... rest of UI
}
}

Step 3: Building the Layout with Stateful Elements

The key insight in Blinc is that UI doesn’t rebuild on every state change. Instead, we use stateful(handle) with .deps() to react to state changes:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let count = ctx.use_state_keyed("counter", || 0i32);
    let container_handle = ctx.use_state(ButtonState::Idle);

    div()
        .w(ctx.width)
        .h(ctx.height)
        .bg(Color::rgba(0.08, 0.08, 0.12, 1.0))
        .flex_col()
        .justify_center()
        .items_center()
        .gap(24.0)
        // Title
        .child(
            text("Counter")
                .size(32.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
        // Count display - uses stateful with deps to update when count changes
        .child(count_display(ctx, count.clone()))
        // Buttons row
        .child(
            div()
                .flex_row()
                .gap(16.0)
                .child(counter_button(ctx, count.clone(), "-", -1))
                .child(counter_button(ctx, count.clone(), "+", 1))
        )
}
}

Step 4: Creating the Count Display

The count display needs to update when the count changes. We use stateful(handle) with .deps():

#![allow(unused)]
fn main() {
fn count_display(ctx: &WindowedContext, count: State<i32>) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .deps(&[count.signal_id()])
        .on_state(move |_state, container| {
            let current = count.get();
            container.merge(
                div()
                    .child(
                        text(&format!("{}", current))
                            .size(64.0)
                            .weight(FontWeight::Bold)
                            .color(Color::rgba(0.4, 0.6, 1.0, 1.0))
                    )
            );
        })
}
}

Step 5: Creating Interactive Buttons

For interactive buttons with hover and press states, use stateful(handle):

#![allow(unused)]
fn main() {
fn counter_button(
    ctx: &WindowedContext,
    count: State<i32>,
    label: &'static str,
    delta: i32,
) -> impl ElementBuilder {
    // Use use_state_for for reusable components with a unique key
    let handle = ctx.use_state_for(label, ButtonState::Idle);

    stateful(handle)
        .w(60.0)
        .h(60.0)
        .rounded(12.0)
        .flex_center()
        .on_state(|state, div| {
            // Apply different styles based on current state
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.2, 0.2, 0.25, 1.0),
                ButtonState::Hovered => Color::rgba(0.3, 0.3, 0.35, 1.0),
                ButtonState::Pressed => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Disabled => Color::rgba(0.1, 0.1, 0.12, 0.5),
            };
            div.set_bg(bg);
        })
        .on_click(move |_| {
            count.update(|v| v + delta);
        })
        .child(
            text(label)
                .size(28.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
}
}

Complete Example

Here’s the full counter application:

use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};
use blinc_layout::stateful::stateful;

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    let config = WindowConfig {
        title: "Counter App".to_string(),
        width: 400,
        height: 300,
        resizable: true,
        ..Default::default()
    };

    WindowedApp::run(config, |ctx| build_ui(ctx))
}

fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let count = ctx.use_state_keyed("counter", || 0i32);

    div()
        .w(ctx.width)
        .h(ctx.height)
        .bg(Color::rgba(0.08, 0.08, 0.12, 1.0))
        .flex_col()
        .justify_center()
        .items_center()
        .gap(24.0)
        .child(
            text("Counter")
                .size(32.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
        .child(count_display(ctx, count.clone()))
        .child(
            div()
                .flex_row()
                .gap(16.0)
                .child(counter_button(ctx, count.clone(), "-", -1))
                .child(counter_button(ctx, count.clone(), "+", 1))
        )
}

fn count_display(ctx: &WindowedContext, count: State<i32>) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .deps(&[count.signal_id()])
        .on_state(move |_state, container| {
            let current = count.get();
            container.merge(
                div()
                    .child(
                        text(&format!("{}", current))
                            .size(64.0)
                            .weight(FontWeight::Bold)
                            .color(Color::rgba(0.4, 0.6, 1.0, 1.0))
                    )
            );
        })
}

fn counter_button(
    ctx: &WindowedContext,
    count: State<i32>,
    label: &'static str,
    delta: i32,
) -> impl ElementBuilder {
    let handle = ctx.use_state_for(label, ButtonState::Idle);

    stateful(handle)
        .w(60.0)
        .h(60.0)
        .rounded(12.0)
        .flex_center()
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.2, 0.2, 0.25, 1.0),
                ButtonState::Hovered => Color::rgba(0.3, 0.3, 0.35, 1.0),
                ButtonState::Pressed => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Disabled => Color::rgba(0.1, 0.1, 0.12, 0.5),
            };
            div.set_bg(bg);
        })
        .on_click(move |_| {
            count.update(|v| v + delta);
        })
        .child(
            text(label)
                .size(28.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
}

Tip: For more examples, explore the crates/blinc_app/examples/ directory which includes windowed.rs, canvas_demo.rs, motion_demo.rs, and more.

Key Concepts Learned

  1. WindowedApp::run - Entry point for desktop applications
  2. WindowedContext - Provides window dimensions and state hooks
  3. use_state_keyed - Creates reactive state with a string key
  4. stateful(handle) - Creates elements that react to state changes
  5. deps() - Declares signal dependencies for reactive updates
  6. on_state - Callback that runs when state or dependencies change
  7. Fluent Builder API - Chain methods like .w(), .h(), .child()
  8. Flexbox Layout - Use .flex_col(), .flex_center(), .gap()

Next Steps

Project Structure

For a typical Blinc application:

my-app/
├── Cargo.toml
├── src/
│   ├── main.rs           # Application entry point
│   ├── app.rs            # Main UI builder
│   ├── components/       # Reusable UI components
│   │   ├── mod.rs
│   │   ├── header.rs
│   │   ├── sidebar.rs
│   │   └── card.rs
│   ├── screens/          # Full-page views
│   │   ├── mod.rs
│   │   ├── home.rs
│   │   └── settings.rs
│   └── state/            # Application state
│       ├── mod.rs
│       └── app_state.rs
└── assets/               # Static assets
    ├── fonts/
    ├── images/
    └── icons/

Entry Point Pattern

// src/main.rs
use blinc_app::prelude::*;
use blinc_app::windowed::{WindowedApp, WindowedContext};

mod app;
mod components;
mod screens;
mod state;

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive(tracing::Level::INFO.into()),
        )
        .init();

    let config = WindowConfig {
        title: "My App".to_string(),
        width: 1200,
        height: 800,
        resizable: true,
        ..Default::default()
    };

    WindowedApp::run(config, |ctx| app::build(ctx))
}

Component Organization

Simple Component

#![allow(unused)]
fn main() {
// src/components/card.rs
use blinc_app::prelude::*;

pub fn card(title: &str) -> Div {
    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .flex_col()
        .gap(8.0)
        .child(
            text(title)
                .size(18.0)
                .weight(FontWeight::SemiBold)
                .color(Color::WHITE)
        )
}
}

Component with Children

#![allow(unused)]
fn main() {
// src/components/card.rs
pub fn card_with_content<E: ElementBuilder>(title: &str, content: E) -> Div {
    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .flex_col()
        .gap(8.0)
        .child(
            text(title)
                .size(18.0)
                .weight(FontWeight::SemiBold)
                .color(Color::WHITE)
        )
        .child(content)
}
}

Stateful Component with BlincComponent

#![allow(unused)]
fn main() {
// src/components/animated_card.rs
use blinc_app::prelude::*;
use blinc_app::windowed::WindowedContext;
use blinc_animation::SpringConfig;
use std::sync::Arc;

#[derive(BlincComponent)]
pub struct AnimatedCard {
    #[animation]
    scale: f32,
    #[animation]
    opacity: f32,
}

pub fn animated_card(ctx: &WindowedContext, title: &str) -> Div {
    let scale = AnimatedCard::use_scale(ctx, 1.0, SpringConfig::snappy());
    let opacity = AnimatedCard::use_opacity(ctx, 1.0, SpringConfig::gentle());

    let hover_scale = Arc::clone(&scale);
    let leave_scale = Arc::clone(&scale);

    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .transform(Transform::scale(scale.lock().unwrap().get()))
        .opacity(opacity.lock().unwrap().get())
        .on_hover_enter(move |_| {
            hover_scale.lock().unwrap().set_target(1.05);
        })
        .on_hover_leave(move |_| {
            leave_scale.lock().unwrap().set_target(1.0);
        })
        .child(text(title).size(18.0).color(Color::WHITE))
}
}

Screen Organization

#![allow(unused)]
fn main() {
// src/screens/home.rs
use blinc_app::prelude::*;
use blinc_app::windowed::WindowedContext;
use crate::components::{header, card};

pub fn home_screen(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .w(ctx.width)
        .h(ctx.height)
        .bg(Color::rgba(0.08, 0.08, 0.12, 1.0))
        .flex_col()
        .child(header::header(ctx))
        .child(
            div()
                .flex_1()
                .p(24.0)
                .flex_col()
                .gap(16.0)
                .child(card("Welcome"))
                .child(card("Getting Started"))
        )
}
}

State Management Patterns

Global App State

#![allow(unused)]
fn main() {
// src/state/app_state.rs
use blinc_core::reactive::Signal;
use blinc_app::windowed::WindowedContext;

pub struct AppState {
    pub user_name: Signal<String>,
    pub theme: Signal<Theme>,
    pub sidebar_open: Signal<bool>,
}

impl AppState {
    pub fn new(ctx: &WindowedContext) -> Self {
        Self {
            user_name: ctx.use_signal(String::new()),
            theme: ctx.use_signal(Theme::Dark),
            sidebar_open: ctx.use_signal(true),
        }
    }
}

#[derive(Clone, Copy, PartialEq)]
pub enum Theme {
    Light,
    Dark,
}
}

Using App State

#![allow(unused)]
fn main() {
// src/app.rs
use blinc_app::prelude::*;
use blinc_app::windowed::WindowedContext;
use crate::state::AppState;
use crate::screens;

pub fn build(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = AppState::new(ctx);

    div()
        .w(ctx.width)
        .h(ctx.height)
        .flex_row()
        .child(sidebar(ctx, &state))
        .child(main_content(ctx, &state))
}

fn sidebar(ctx: &WindowedContext, state: &AppState) -> Div {
    let is_open = ctx.get(state.sidebar_open).unwrap_or(true);

    if is_open {
        div().w(250.0).h_full().bg(Color::rgba(0.1, 0.1, 0.15, 1.0))
            // ... sidebar content
    } else {
        div().w(0.0).h(0.0)
    }
}
}

Module Re-exports

#![allow(unused)]
fn main() {
// src/components/mod.rs
mod card;
mod header;
mod sidebar;
mod animated_card;

pub use card::*;
pub use header::*;
pub use sidebar::*;
pub use animated_card::*;
}
#![allow(unused)]
fn main() {
// src/screens/mod.rs
mod home;
mod settings;

pub use home::*;
pub use settings::*;
}

Asset Loading

For images and other assets, use relative paths from your project root:

#![allow(unused)]
fn main() {
// Load an image
image("assets/images/logo.png")
    .w(100.0)
    .h(100.0)
    .contain()

// Load an SVG icon
svg("assets/icons/menu.svg")
    .w(24.0)
    .h(24.0)
    .tint(Color::WHITE)
}

Tips

  1. Keep components small - Each component should do one thing well
  2. Use BlincComponent - For any component with animations or complex state
  3. Separate concerns - UI building, state management, and business logic
  4. Use the prelude - use blinc_app::prelude::* imports common items
  5. Consistent naming - Use _screen suffix for full-page views, no suffix for components

Mobile Development

Blinc supports building native mobile applications for both Android and iOS platforms. The same Rust UI code runs on mobile with platform-specific rendering backends (Vulkan for Android, Metal for iOS).

Cross-Platform Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Your Blinc App                          │
│         (Shared Rust UI code, state, animations)             │
└─────────────────────────────┬───────────────────────────────┘
                              │
         ┌────────────────────┼────────────────────┐
         │                    │                    │
    ┌────▼────┐         ┌─────▼─────┐        ┌────▼────┐
    │ Desktop │         │  Android  │        │   iOS   │
    │ (wgpu)  │         │ (Vulkan)  │        │ (Metal) │
    └─────────┘         └───────────┘        └─────────┘

Key Features

  • Shared UI Code: Write your UI once in Rust, deploy everywhere
  • Native Performance: GPU-accelerated rendering via Vulkan/Metal
  • Touch Support: Full multi-touch gesture handling
  • Reactive State: Same reactive state system as desktop
  • Animations: Spring physics and keyframe animations work seamlessly

Supported Platforms

PlatformBackendMin VersionStatus
AndroidVulkanAPI 24 (7.0)Stable
iOSMetaliOS 15+Stable

Project Structure

A typical Blinc mobile project looks like this:

my-app/
├── Cargo.toml           # Rust dependencies
├── blinc.toml           # Blinc project config
├── src/
│   └── main.rs          # Shared UI code
├── platforms/
│   ├── android/         # Android-specific files
│   │   ├── app/
│   │   │   └── src/main/
│   │   │       ├── AndroidManifest.xml
│   │   │       └── kotlin/.../MainActivity.kt
│   │   └── build.gradle.kts
│   └── ios/             # iOS-specific files
│       ├── BlincApp/
│       │   ├── AppDelegate.swift
│       │   ├── BlincViewController.swift
│       │   └── Info.plist
│       └── BlincApp.xcodeproj/
└── build-android.sh     # Build scripts

Quick Start

1. Create a new mobile project

blinc new my-app --template rust
cd my-app

2. Write your UI

#![allow(unused)]
fn main() {
use blinc_app::prelude::*;

fn app(ctx: &mut WindowedContext) -> impl ElementBuilder {
    let count = ctx.use_state_keyed("count", || 0i32);

    div()
        .w(ctx.width)
        .h(ctx.height)
        .bg(Color::from_hex(0x1a1a2e))
        .flex_col()
        .items_center()
        .justify_center()
        .gap(20.0)
        .child(counter_display(count.clone()))
        .child(counter_button("+", count.clone(), 1))
}

fn counter_display(count: State<i32>) -> impl ElementBuilder {
    // Stateful elements with deps update incrementally when dependencies change
    stateful::<NoState>()
        .deps([count.signal_id()])
        .on_state(move |_ctx| {
            text(format!("Count: {}", count.get()))
                .size(48.0)
                .color(Color::WHITE)
        })
}

fn counter_button(label: &str, count: State<i32>, delta: i32) -> impl ElementBuilder {
    let label = label.to_string();
    stateful::<ButtonState>()
        .on_state(move |ctx| {
            let bg = match ctx.state() {
                ButtonState::Idle => Color::from_hex(0x4a4a5a),
                ButtonState::Hovered => Color::from_hex(0x5a5a6a),
                ButtonState::Pressed => Color::from_hex(0x3a3a4a),
                ButtonState::Disabled => Color::from_hex(0x2a2a2a),
            };
            div()
                .w(80.0).h(50.0)
                .rounded(8.0)
                .bg(bg)
                .items_center()
                .justify_center()
                .child(text(&label).size(24.0).color(Color::WHITE))
        })
        .on_click(move |_| count.set(count.get() + delta))
}
}

3. Build and run

# Android
blinc run android

# iOS
blinc run ios

Next Steps

Android Development

This guide covers setting up your environment and building Blinc apps for Android.

Prerequisites

1. Android SDK & NDK

Install Android Studio or the standalone SDK:

# macOS (via Homebrew)
brew install --cask android-studio

# Or download from https://developer.android.com/studio

Set up environment variables:

export ANDROID_HOME=$HOME/Library/Android/sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125
export PATH=$PATH:$ANDROID_HOME/platform-tools

2. Rust Android Targets

rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
rustup target add i686-linux-android

3. cargo-ndk

cargo install cargo-ndk

Building

Debug Build

# Build for arm64 (most modern devices)
cargo ndk -t arm64-v8a build

# Build for multiple architectures
cargo ndk -t arm64-v8a -t armeabi-v7a build

Release Build

cargo ndk -t arm64-v8a build --release

Using Gradle

From the platforms/android directory:

./gradlew assembleDebug

The APK will be at app/build/outputs/apk/debug/app-debug.apk.

Project Configuration

Cargo.toml

[lib]
name = "my_app"
crate-type = ["cdylib", "staticlib"]

[target.'cfg(target_os = "android")'.dependencies]
blinc_app = { version = "0.1", features = ["android"] }
blinc_platform_android = "0.1"
android-activity = { version = "0.6", features = ["native-activity"] }
log = "0.4"
android_logger = "0.14"

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature android:glEsVersion="0x00030000" android:required="true" />

    <application
        android:label="My App"
        android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
        android:hardwareAccelerated="true">

        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:exported="true">

            <meta-data
                android:name="android.app.lib_name"
                android:value="my_app" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Touch Event Handling

Android touch events are automatically routed to your UI. The touch phases map as follows:

Android ActionBlinc Event
ACTION_DOWNpointer_down
ACTION_MOVEpointer_move
ACTION_UPpointer_up + pointer_leave
ACTION_CANCELpointer_leave

Debugging

View Logs

adb logcat | grep -E "(blinc|BlincApp)"

Common Issues

“Library not found”

Ensure the native library is built and copied to app/src/main/jniLibs/:

cargo ndk -t arm64-v8a build
cp target/aarch64-linux-android/debug/libmy_app.so \
   platforms/android/app/src/main/jniLibs/arm64-v8a/

“Vulkan not supported”

Check device compatibility:

adb shell getprop ro.hardware.vulkan

Most devices with API 24+ support Vulkan, but some older devices may not.

Touch events not working

  1. Verify the render context is created successfully
  2. Check that android.app.lib_name in manifest matches your library name
  3. Look for errors in logcat

Performance Tips

  1. Use release builds for performance testing
  2. Enable LTO in Cargo.toml:
    [profile.release]
    lto = "thin"
    opt-level = "z"
    
  3. Test on real devices - emulators have different GPU characteristics

iOS Development

This guide covers setting up your environment and building Blinc apps for iOS.

Prerequisites

1. Xcode

Install Xcode 15+ from the Mac App Store or Apple Developer website.

# Verify installation
xcode-select -p

2. Rust iOS Targets

rustup target add aarch64-apple-ios        # Device (arm64)
rustup target add aarch64-apple-ios-sim    # Simulator (Apple Silicon)
rustup target add x86_64-apple-ios         # Simulator (Intel)

Building

Build Script

Create a build script build-ios.sh:

#!/bin/bash
set -e

MODE=${1:-debug}
PROJECT_NAME="my_app"

if [ "$MODE" = "release" ]; then
    CARGO_FLAGS="--release"
    TARGET_DIR="release"
else
    CARGO_FLAGS=""
    TARGET_DIR="debug"
fi

# Build for device
cargo build --target aarch64-apple-ios $CARGO_FLAGS

# Build for simulator (Apple Silicon)
cargo build --target aarch64-apple-ios-sim $CARGO_FLAGS

# Copy to libs directory
mkdir -p platforms/ios/libs/device
mkdir -p platforms/ios/libs/simulator

cp target/aarch64-apple-ios/$TARGET_DIR/lib${PROJECT_NAME}.a \
   platforms/ios/libs/device/

cp target/aarch64-apple-ios-sim/$TARGET_DIR/lib${PROJECT_NAME}.a \
   platforms/ios/libs/simulator/

Building

# Debug build
./build-ios.sh

# Release build
./build-ios.sh release

Xcode

  1. Open platforms/ios/BlincApp.xcodeproj
  2. Select your target (device or simulator)
  3. Press Cmd+R to build and run

Project Configuration

Cargo.toml

[lib]
name = "my_app"
crate-type = ["cdylib", "staticlib"]

[target.'cfg(target_os = "ios")'.dependencies]
blinc_app = { version = "0.1", features = ["ios"] }
blinc_platform_ios = "0.1"

Xcode Build Settings

In your Xcode project:

  1. Link the static library:

    • Build Phases → Link Binary With Libraries
    • Add libmy_app.a from libs/device/ or libs/simulator/
  2. Set the bridging header:

    • Build Settings → Swift Compiler - General
    • Objective-C Bridging Header: BlincApp/Blinc-Bridging-Header.h
  3. Add required frameworks:

    • Metal.framework
    • MetalKit.framework
    • QuartzCore.framework

Swift Integration

Bridging Header

The bridging header (Blinc-Bridging-Header.h) declares the C FFI functions:

// Context lifecycle
IOSRenderContext* blinc_create_context(uint32_t width, uint32_t height, double scale);
void blinc_destroy_context(IOSRenderContext* ctx);

// Rendering
bool blinc_needs_render(IOSRenderContext* ctx);
void blinc_build_frame(IOSRenderContext* ctx);
bool blinc_render_frame(IOSGpuRenderer* gpu);

// Input
void blinc_handle_touch(IOSRenderContext* ctx, uint64_t id, float x, float y, int32_t phase);

View Controller

The BlincViewController manages:

  • CADisplayLink for 60fps frame timing
  • Metal layer for GPU rendering
  • Touch event forwarding to Rust

Touch Event Handling

iOS touch events are routed through the view controller:

iOS PhaseBlinc Event
touchesBeganpointer_down
touchesMovedpointer_move
touchesEndedpointer_up + pointer_leave
touchesCancelledpointer_leave

The pointer_leave after pointer_up is important for proper button state transitions on touch devices.

Debugging

Console Logs

View Rust logs in Xcode’s console or use Console.app with a filter:

subsystem:com.blinc.my_app

Common Issues

“Library not found: -lmy_app”

Run the build script first:

./build-ios.sh

Black screen on simulator

  1. Ensure you built for the correct simulator target (aarch64-apple-ios-sim)
  2. Verify the library is in libs/simulator/
  3. Check Xcode console for Metal initialization errors

Touch events not working

  1. Verify blinc_create_context succeeds (check console logs)
  2. Ensure ios_app_init() is called before creating the context
  3. Check that touch coordinates are in logical points, not pixels

Performance Tips

  1. Use release builds for performance testing:

    ./build-ios.sh release
    
  2. Enable LTO in Cargo.toml:

    [profile.release]
    lto = "thin"
    opt-level = "z"
    strip = true
    
  3. Test on real devices - simulators use software rendering for some operations

  4. Profile with Instruments - use Xcode’s Metal debugger for GPU analysis

CLI Reference

The Blinc CLI simplifies creating and building mobile projects.

Creating a Project

New Project

blinc new my-app --template rust

This creates a new Blinc project with:

  • Cargo.toml configured for mobile targets
  • Platform directories for Android and iOS
  • Build scripts for each platform
  • Example UI code

Options

blinc new <name> [options]

Options:
  --template <type>   Project template (rust, swift, kotlin)
  --platforms <list>  Target platforms (desktop,android,ios)
  --no-git            Skip git initialization

Building

Build for Android

blinc build android

Options:

blinc build android [options]

Options:
  --release           Build in release mode
  --target <arch>     Target architecture (arm64-v8a, armeabi-v7a, x86_64, x86)
  --all-targets       Build for all architectures

Build for iOS

blinc build ios

Options:

blinc build ios [options]

Options:
  --release           Build in release mode
  --device            Build for physical device only
  --simulator         Build for simulator only

Running

Run on Android

blinc run android

This will:

  1. Build the native library
  2. Build the APK with Gradle
  3. Install on connected device/emulator
  4. Launch the app

Options:

blinc run android [options]

Options:
  --release           Run release build
  --device <id>       Target specific device (from adb devices)
  --no-install        Build only, don't install

Run on iOS

blinc run ios

This will:

  1. Build the static library
  2. Open Xcode project
  3. Build and run on selected target

Options:

blinc run ios [options]

Options:
  --release           Run release build
  --simulator <name>  Target specific simulator
  --device            Run on physical device

Project Configuration

blinc.toml

The project configuration file:

[project]
name = "my-app"
version = "0.1.0"
template = "rust"
entry = "Cargo.toml"

[targets]
default = "desktop"
supported = ["desktop", "android", "ios"]

[targets.desktop]
enabled = true
command = "cargo run --features desktop"

[targets.android]
enabled = true
platform_dir = "platforms/android"

[targets.ios]
enabled = true
platform_dir = "platforms/ios"

[build]
blinc_path = "../.."  # Path to Blinc framework

Configuration Options

KeyDescriptionDefault
project.nameProject nameRequired
project.versionVersion string“0.1.0”
project.templateTemplate type“rust”
targets.defaultDefault build target“desktop”
targets.supportedList of supported platforms[“desktop”]
build.blinc_pathPath to Blinc framework“../..”

Cleaning

# Clean all build artifacts
blinc clean

# Clean specific platform
blinc clean android
blinc clean ios

Checking Configuration

# Validate project configuration
blinc check

# Check specific platform setup
blinc check android
blinc check ios

This verifies:

  • Required tools are installed
  • Environment variables are set
  • Project configuration is valid

Elements & Layout

Blinc provides a set of core elements that can be composed to build any UI. All elements implement the ElementBuilder trait and use a fluent builder pattern.

Core Elements

Div - The Universal Container

div() is the primary building block. It’s a flexible container that supports:

  • Flexbox layout
  • Background colors and materials
  • Borders and shadows
  • Event handling
  • Child elements
#![allow(unused)]
fn main() {
div()
    .w(200.0)
    .h(100.0)
    .bg(Color::rgba(0.2, 0.2, 0.3, 1.0))
    .rounded(8.0)
    .flex_center()
    .child(text("Hello"))
}

Text - Typography

text(content) renders text with customizable typography:

#![allow(unused)]
fn main() {
text("Hello, World!")
    .size(24.0)
    .weight(FontWeight::Bold)
    .color(Color::WHITE)
    .family("Inter")
}

Text Properties:

  • .size(px) - Font size in pixels
  • .weight(FontWeight) - Bold, SemiBold, Medium, Regular, Light
  • .color(Color) - Text color
  • .family(name) - Font family
  • .italic() - Italic style
  • .underline() - Underline decoration
  • .line_height(multiplier) - Line height as multiplier of font size
  • .letter_spacing(px) - Space between characters
  • .align(TextAlign) - Left, Center, Right, Justify

Typography Helpers:

#![allow(unused)]
fn main() {
h1("Heading 1")      // 32px bold
h2("Heading 2")      // 28px bold
h3("Heading 3")      // 24px bold
h4("Heading 4")      // 20px semibold
h5("Heading 5")      // 16px semibold
h6("Heading 6")      // 14px semibold
p("Paragraph")       // 14px regular
caption("Caption")   // 12px regular
label("Label")       // 14px medium
muted("Muted text")  // Reduced opacity
b("Bold text")       // Bold weight
small("Small")       // 12px
}

Stack - Overlapping Layers

stack() positions children on top of each other, useful for overlays and layered designs:

#![allow(unused)]
fn main() {
stack()
    .w(200.0)
    .h(200.0)
    // Background layer
    .child(
        div().w_full().h_full().bg(Color::BLUE)
    )
    // Foreground layer
    .child(
        div()
            .absolute()
            .right(10.0)
            .bottom(10.0)
            .w(50.0)
            .h(50.0)
            .bg(Color::RED)
    )
}

Canvas - Custom Drawing

canvas(render_fn) provides direct GPU drawing access:

#![allow(unused)]
fn main() {
canvas(|ctx: &mut dyn DrawContext, bounds| {
    ctx.fill_rect(
        Rect::new(0.0, 0.0, bounds.width, bounds.height),
        CornerRadius::uniform(8.0),
        Brush::Solid(Color::RED),
    );
})
.w(200.0)
.h(100.0)
}

See Canvas Drawing for more details.

Image & SVG

#![allow(unused)]
fn main() {
// Raster images
image("path/to/image.png")
    .w(200.0)
    .h(150.0)
    .cover()  // Object-fit: cover

// SVG with tint
svg("path/to/icon.svg")
    .w(24.0)
    .h(24.0)
    .tint(Color::WHITE)
}

See Images & SVG for more details.


Layout System

Blinc uses Flexbox for layout, powered by Taffy.

Sizing

#![allow(unused)]
fn main() {
div()
    .w(200.0)           // Fixed width in pixels
    .h(100.0)           // Fixed height in pixels
    .w_full()           // 100% width
    .h_full()           // 100% height
    .w_auto()           // Auto width (content-based)
    .h_auto()           // Auto height (content-based)
    .w_fit()            // Shrink-wrap to content
    .size(200.0, 100.0) // Set both dimensions
    .square(100.0)      // Square element
    .min_w(50.0)        // Minimum width
    .max_w(500.0)       // Maximum width
    .min_h(50.0)        // Minimum height
    .max_h(300.0)       // Maximum height
    .aspect_ratio(16.0 / 9.0)  // Maintain aspect ratio
}

Flex Container

#![allow(unused)]
fn main() {
div()
    .flex()             // Enable flexbox
    .flex_row()         // Horizontal layout (default)
    .flex_col()         // Vertical layout
    .flex_row_reverse() // Right to left
    .flex_col_reverse() // Bottom to top
    .flex_wrap()        // Wrap children
}

Flex Items

#![allow(unused)]
fn main() {
div()
    .flex_grow()        // Grow to fill space (flex-grow: 1)
    .flex_shrink()      // Allow shrinking (flex-shrink: 1)
    .flex_shrink_0()    // Don't shrink (flex-shrink: 0)
    .flex_1()           // flex: 1 1 0% (grow and shrink)
    .flex_auto()        // flex: 1 1 auto
}

Alignment

Align Items (cross-axis alignment):

#![allow(unused)]
fn main() {
div()
    .items_start()      // Align to start
    .items_center()     // Center alignment
    .items_end()        // Align to end
    .items_stretch()    // Stretch to fill
    .items_baseline()   // Align baselines
}

Justify Content (main-axis distribution):

#![allow(unused)]
fn main() {
div()
    .justify_start()    // Pack at start
    .justify_center()   // Center items
    .justify_end()      // Pack at end
    .justify_between()  // Space between items
    .justify_around()   // Space around items
    .justify_evenly()   // Equal spacing
}

Convenience Methods:

#![allow(unused)]
fn main() {
div().flex_center()     // Center both axes
div().flex_col().justify_center().items_center()  // Same as above
}

Gap (Spacing Between Children)

#![allow(unused)]
fn main() {
div()
    .gap(16.0)          // Gap in pixels
    .gap_x(8.0)         // Horizontal gap only
    .gap_y(12.0)        // Vertical gap only
}

Padding

Padding uses a 4px unit system (like Tailwind CSS):

#![allow(unused)]
fn main() {
div()
    .p(4.0)             // 16px padding all sides (4 * 4px)
    .px(2.0)            // 8px horizontal padding
    .py(3.0)            // 12px vertical padding
    .pt(1.0)            // 4px top padding
    .pr(2.0)            // 8px right padding
    .pb(3.0)            // 12px bottom padding
    .pl(4.0)            // 16px left padding
    .p_px(20.0)         // 20px (exact pixels, not units)
}

Margin

Same unit system as padding:

#![allow(unused)]
fn main() {
div()
    .m(4.0)             // 16px margin all sides
    .mx(2.0)            // 8px horizontal margin
    .my(3.0)            // 12px vertical margin
    .mt(1.0)            // 4px top margin
    .mr(2.0)            // 8px right margin
    .mb(3.0)            // 12px bottom margin
    .ml(4.0)            // 16px left margin
    .mx_auto()          // Auto horizontal margins (centering)
}

Positioning

#![allow(unused)]
fn main() {
div()
    .relative()         // Position relative
    .absolute()         // Position absolute
    .inset(10.0)        // 10px from all edges
    .top(20.0)          // 20px from top
    .right(20.0)        // 20px from right
    .bottom(20.0)       // 20px from bottom
    .left(20.0)         // 20px from left
}

Overflow

#![allow(unused)]
fn main() {
div()
    .overflow_clip()    // Clip overflowing content
    .overflow_visible() // Allow overflow
    .overflow_scroll()  // Enable scrolling
}

Common Layout Patterns

Centered Content

#![allow(unused)]
fn main() {
div()
    .w_full()
    .h_full()
    .flex_center()
    .child(content)
}
#![allow(unused)]
fn main() {
div()
    .w_full()
    .h_full()
    .flex_row()
    .child(
        div().w(250.0).h_full()  // Sidebar
    )
    .child(
        div().flex_1().h_full()  // Main content
    )
}

Card Grid

#![allow(unused)]
fn main() {
div()
    .w_full()
    .flex_row()
    .flex_wrap()
    .gap(16.0)
    .child(card().w(300.0))
    .child(card().w(300.0))
    .child(card().w(300.0))
}

Header/Content/Footer

#![allow(unused)]
fn main() {
div()
    .w_full()
    .h_full()
    .flex_col()
    .child(
        div().h(60.0).w_full()  // Header
    )
    .child(
        div().flex_1().w_full() // Content (fills remaining)
    )
    .child(
        div().h(40.0).w_full()  // Footer
    )
}

Horizontal Navigation

#![allow(unused)]
fn main() {
div()
    .w_full()
    .h(60.0)
    .flex_row()
    .items_center()
    .justify_between()
    .px(4.0)
    .child(logo())
    .child(
        div()
            .flex_row()
            .gap(24.0)
            .child(nav_item("Home"))
            .child(nav_item("About"))
            .child(nav_item("Contact"))
    )
}

The .child() Pattern

Add children with .child(). For multiple children of the same type, use iterators:

#![allow(unused)]
fn main() {
// Single child
div().child(text("Hello"))

// Multiple children
div()
    .child(text("First"))
    .child(text("Second"))
    .child(text("Third"))

// From iterator
let items = vec!["Apple", "Banana", "Cherry"];
div().child(
    items.into_iter().map(|item| text(item))
)
}

ElementBuilder Trait

All elements implement ElementBuilder:

#![allow(unused)]
fn main() {
pub trait ElementBuilder {
    fn build(self, tree: &mut LayoutTree) -> LayoutNodeId;
}
}

This allows composing any element type:

#![allow(unused)]
fn main() {
fn my_component() -> impl ElementBuilder {
    div().child(text("Hello"))
}

// Use it
div().child(my_component())
}

Styling & Materials

Blinc provides comprehensive styling options from simple colors to advanced GPU-accelerated material effects.

Colors

Basic Colors

Colors are RGBA with values from 0.0 to 1.0:

#![allow(unused)]
fn main() {
// RGBA constructor
Color::rgba(0.2, 0.4, 0.8, 1.0)  // Blue, fully opaque
Color::rgba(1.0, 0.0, 0.0, 0.5)  // Red, 50% transparent

// From array (common pattern)
Color::from([0.2, 0.4, 0.8, 1.0])

// Predefined colors
Color::WHITE
Color::BLACK
Color::RED
Color::GREEN
Color::BLUE
Color::TRANSPARENT
}

Background Colors

#![allow(unused)]
fn main() {
div()
    .bg(Color::rgba(0.1, 0.1, 0.15, 1.0))

// From array shorthand
div().bg([0.1, 0.1, 0.15, 1.0])
}

Gradients

For gradients, use the .background() method with a Brush:

#![allow(unused)]
fn main() {
use blinc_core::{Brush, Gradient, GradientStop, Point};

div()
    .w(200.0)
    .h(100.0)
    .background(Brush::Gradient(Gradient::linear_with_stops(
        Point::new(0.0, 0.0),       // Start point
        Point::new(200.0, 0.0),     // End point
        vec![
            GradientStop::new(0.0, Color::rgba(0.9, 0.2, 0.5, 1.0)),
            GradientStop::new(0.5, Color::rgba(0.9, 0.5, 0.2, 1.0)),
            GradientStop::new(1.0, Color::rgba(0.2, 0.8, 0.6, 1.0)),
        ],
    )))
}

Borders & Corners

Corner Radius

#![allow(unused)]
fn main() {
div()
    .rounded(8.0)           // Uniform radius
    .rounded_full()         // Pill shape (50% of smallest dimension)
    .rounded_corners(
        16.0,  // Top-left
        16.0,  // Top-right
        8.0,   // Bottom-right
        8.0,   // Bottom-left
    )
}

Shadows

Preset Shadows

#![allow(unused)]
fn main() {
div()
    .shadow_sm()    // Small shadow
    .shadow_md()    // Medium shadow
    .shadow_lg()    // Large shadow
    .shadow_xl()    // Extra large shadow
}

Custom Shadows

#![allow(unused)]
fn main() {
div().shadow_params(
    2.0,   // Offset X
    4.0,   // Offset Y
    12.0,  // Blur radius
    Color::rgba(0.0, 0.0, 0.0, 0.3)
)
}

Opacity

#![allow(unused)]
fn main() {
div()
    .opacity(0.5)       // 50% opacity
    .opaque()           // opacity: 1.0
    .translucent()      // opacity: 0.5
    .invisible()        // opacity: 0.0
}

Transforms

Apply 2D transforms to any element:

#![allow(unused)]
fn main() {
div()
    .translate(10.0, 20.0)    // Move by (x, y)
    .scale(1.5)               // Uniform scale
    .scale_xy(1.5, 0.8)       // Non-uniform scale
    .rotate(45.0_f32.to_radians())  // Rotate (radians)
    .rotate_deg(45.0)         // Rotate (degrees)
}

For combined transforms:

#![allow(unused)]
fn main() {
use blinc_core::Transform;

div().transform(
    Transform::translate(100.0, 50.0)
        .then_scale(1.2, 1.2)
        .then_rotate(0.1)
)
}

Materials

Blinc includes GPU-accelerated material effects for modern, polished UIs.

Glass Material

Creates a frosted glass effect with background blur:

#![allow(unused)]
fn main() {
// Quick glass
div().glass()

// Customized glass
use blinc_core::GlassMaterial;

div().material(Material::Glass(
    GlassMaterial::new()
        .blur(20.0)           // Blur intensity (0-50)
        .tint(Color::rgba(1.0, 1.0, 1.0, 0.1))
        .saturation(1.2)      // Color saturation
        .brightness(1.0)      // Brightness adjustment
        .noise(0.03)          // Frosted texture
        .border(0.8)          // Border highlight intensity
))
}

Glass Presets:

#![allow(unused)]
fn main() {
GlassMaterial::ultra_thin()  // Very subtle
GlassMaterial::thin()        // Light blur
GlassMaterial::regular()     // Standard (default)
GlassMaterial::thick()       // Heavy blur
GlassMaterial::frosted()     // Frosted window style
GlassMaterial::card()        // Card-like appearance
}

Metallic Material

Creates reflective metallic surfaces:

#![allow(unused)]
fn main() {
use blinc_core::MetallicMaterial;

div().material(Material::Metallic(
    MetallicMaterial::new()
        .color(Color::WHITE)
        .roughness(0.3)       // 0 = mirror, 1 = matte
        .metallic(1.0)        // Metal intensity
        .reflection(0.5)      // Reflection strength
))
}

Metallic Presets:

#![allow(unused)]
fn main() {
MetallicMaterial::chrome()   // Polished chrome
MetallicMaterial::brushed()  // Brushed metal
MetallicMaterial::gold()     // Gold finish
MetallicMaterial::silver()   // Silver finish
MetallicMaterial::copper()   // Copper finish
}

Quick Material Methods

#![allow(unused)]
fn main() {
div().glass()       // Default glass material
div().metallic()    // Default metallic material
div().chrome()      // Chrome preset
div().gold()        // Gold preset
}

Render Layers

Control rendering order with layers:

#![allow(unused)]
fn main() {
use blinc_core::RenderLayer;

div()
    .layer(RenderLayer::Background)  // Rendered first
    .child(background_content())

div()
    .layer(RenderLayer::Foreground)  // Rendered on top
    .child(overlay_content())
}

For glass effects, content behind glass should be on .background() layer:

#![allow(unused)]
fn main() {
stack()
    .child(
        div().background()  // Behind glass
            .child(colorful_background())
    )
    .child(
        div().glass()       // Glass overlay
            .foreground()   // On top
            .child(content())
    )
}

Common Styling Patterns

Card Style

#![allow(unused)]
fn main() {
fn card() -> Div {
    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .shadow_md()
}
}

Glass Card

#![allow(unused)]
fn main() {
fn glass_card() -> Div {
    div()
        .p(16.0)
        .rounded(16.0)
        .glass()
        .shadow_lg()
}
}

Button Styles

#![allow(unused)]
fn main() {
fn primary_button() -> Div {
    div()
        .px(4.0)
        .py(2.0)
        .rounded(8.0)
        .bg(Color::rgba(0.3, 0.5, 0.9, 1.0))
}

fn secondary_button() -> Div {
    div()
        .px(4.0)
        .py(2.0)
        .rounded(8.0)
        .bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
}

fn ghost_button() -> Div {
    div()
        .px(4.0)
        .py(2.0)
        .rounded(8.0)
        .bg(Color::TRANSPARENT)
}
}

Hover Effects with State

Use stateful(handle) to create elements with automatic hover/press state transitions:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn hoverable_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .p(16.0)
        .rounded(12.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Hovered => Color::rgba(0.18, 0.18, 0.24, 1.0),
                ButtonState::Pressed => Color::rgba(0.12, 0.12, 0.16, 1.0),
                _ => Color::rgba(0.15, 0.15, 0.2, 1.0),
            };
            div.set_bg(bg);
        })
        .child(text("Hover me").color(Color::WHITE))
}
}

Dark Theme Color Palette

Common colors for dark-themed UIs:

#![allow(unused)]
fn main() {
// Backgrounds
let bg_primary = Color::rgba(0.08, 0.08, 0.12, 1.0);
let bg_secondary = Color::rgba(0.12, 0.12, 0.16, 1.0);
let bg_tertiary = Color::rgba(0.16, 0.16, 0.2, 1.0);

// Surfaces
let surface = Color::rgba(0.15, 0.15, 0.2, 1.0);
let surface_hover = Color::rgba(0.18, 0.18, 0.24, 1.0);

// Text
let text_primary = Color::WHITE;
let text_secondary = Color::rgba(0.7, 0.7, 0.8, 1.0);
let text_muted = Color::rgba(0.5, 0.5, 0.6, 1.0);

// Accent
let accent = Color::rgba(0.4, 0.6, 1.0, 1.0);
let accent_hover = Color::rgba(0.5, 0.7, 1.0, 1.0);

// Status
let success = Color::rgba(0.2, 0.8, 0.4, 1.0);
let warning = Color::rgba(0.9, 0.7, 0.2, 1.0);
let error = Color::rgba(0.9, 0.3, 0.3, 1.0);
}

CSS Styling

Blinc includes a powerful CSS parser that allows you to define styles using familiar CSS syntax. This enables separation of concerns between layout code and visual styling.

Overview

The CSS system supports:

  • ID-based selectors (#element-id)
  • State modifiers (:hover, :active, :focus, :disabled)
  • CSS custom properties (:root and var())
  • Keyframe animations (@keyframes)
  • Automatic animation application via the animation: property
  • Theme integration (theme() function)
  • Length units (px, sp, %)
  • Gradients (linear-gradient, radial-gradient, conic-gradient)

Basic Usage

Parsing CSS

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

let css = r#"
    #card {
        background: #3498db;
        border-radius: 12px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    }
"#;

let result = Stylesheet::parse_with_errors(css);

// Check for errors
if result.has_errors() {
    result.print_colored_diagnostics();
}

let stylesheet = result.stylesheet;
}

Applying Styles to Elements

Attach the stylesheet to the RenderTree:

#![allow(unused)]
fn main() {
use std::sync::Arc;

// In your render tree setup
render_tree.set_stylesheet(Some(Arc::new(stylesheet)));

// Then use IDs on elements
div()
    .id("card")
    .child(text("Styled with CSS!"))
}

Supported Properties

Background

#element {
    background: #ff5733;                    /* Hex color */
    background: rgb(255, 87, 51);           /* RGB */
    background: rgba(255, 87, 51, 0.8);     /* RGBA */
    background: theme(primary);             /* Theme token */
}

Gradients

CSS gradients are fully supported for the background property:

Linear Gradients

#element {
    /* Angle-based (0deg = up, 90deg = right, 180deg = down) */
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

    /* Direction keywords */
    background: linear-gradient(to right, red, blue);
    background: linear-gradient(to bottom right, #fff, #000);

    /* Multiple color stops */
    background: linear-gradient(90deg, red 0%, yellow 50%, green 100%);

    /* Implied positions (evenly distributed) */
    background: linear-gradient(to bottom, red, yellow, green);

    /* Different angle units */
    background: linear-gradient(0.25turn, red, blue);  /* 90deg */
    background: linear-gradient(1.5708rad, red, blue); /* ~90deg */
}

Radial Gradients

#element {
    /* Simple circle from center */
    background: radial-gradient(circle, red, blue);

    /* With position */
    background: radial-gradient(circle at center, red, blue);
    background: radial-gradient(circle at 25% 75%, red, blue);

    /* Ellipse shape */
    background: radial-gradient(ellipse at center, red, blue);

    /* Multiple color stops */
    background: radial-gradient(circle, red 0%, yellow 50%, green 100%);
}

Conic Gradients

#element {
    /* Simple color wheel */
    background: conic-gradient(red, yellow, green, blue, red);

    /* With starting angle */
    background: conic-gradient(from 45deg, red, blue);

    /* With position */
    background: conic-gradient(at 25% 75%, red, blue);

    /* Combined angle and position */
    background: conic-gradient(from 90deg at center, red, blue);
}

Gradient Color Stops

Color stops can use any supported color format:

#element {
    /* Hex colors with positions */
    background: linear-gradient(to right, #667eea 0%, #764ba2 100%);

    /* RGBA colors */
    background: linear-gradient(45deg, rgba(255, 0, 0, 0.5), rgba(0, 0, 255, 0.8));

    /* Named colors */
    background: linear-gradient(to right, red, orange, yellow, green, blue);

    /* Mixed formats */
    background: linear-gradient(135deg, #ff0000, rgba(0, 255, 0, 0.5) 50%, blue);
}

Border Radius

#element {
    border-radius: 8px;                     /* Uniform */
    border-radius: theme(radius-lg);        /* Theme token */
}

Box Shadow

#element {
    box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.3);  /* x y blur color */
    box-shadow: theme(shadow-md);                 /* Theme token */
    box-shadow: none;                             /* Remove shadow */
}

Transform

#element {
    transform: scale(1.02);                 /* Uniform scale */
    transform: scale(1.5, 0.8);             /* Non-uniform */
    transform: translate(10px, 20px);       /* Translation */
    transform: translateX(10px);            /* X only */
    transform: translateY(20px);            /* Y only */
    transform: rotate(45deg);               /* Rotation */
}

Opacity

#element {
    opacity: 0.8;
}

Render Layer

#element {
    render-layer: foreground;               /* On top */
    render-layer: background;               /* Behind */
    render-layer: glass;                    /* Glass layer */
}

Length Units

Blinc CSS supports three types of length units:

Pixels (px)

Raw pixel values. These are the default when no unit is specified.

#element {
    border-radius: 8px;
    box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.3);
    transform: translate(10px, 20px);
}

Spacing Units (sp)

Spacing units follow a 4px grid system, where 1sp = 4px. This helps maintain consistent spacing throughout your application.

#card {
    border-radius: 2sp;                    /* 2 * 4 = 8px */
    box-shadow: 1sp 2sp 4sp rgba(0,0,0,0.2); /* 4px 8px 16px */
    transform: translate(4sp, 2sp);         /* 16px, 8px */
}

Common sp values:

  • 1sp = 4px
  • 2sp = 8px
  • 4sp = 16px
  • 6sp = 24px
  • 8sp = 32px

Percentages (%)

Percentages are supported in gradient color stops and position values.

#element {
    /* Gradient color stops use percentages */
    background: linear-gradient(to right, red 0%, blue 100%);

    /* Radial/conic gradient positions */
    background: radial-gradient(circle at 25% 75%, red, blue);
}

State Modifiers

Define different styles for interactive states:

#button {
    background: theme(primary);
    transform: scale(1.0);
}

#button:hover {
    background: theme(primary-hover);
    transform: scale(1.02);
}

#button:active {
    transform: scale(0.98);
}

#button:focus {
    box-shadow: 0 0 0 3px theme(primary);
}

#button:disabled {
    opacity: 0.5;
}

Querying State Styles

#![allow(unused)]
fn main() {
// Get base style
let base = stylesheet.get("button");

// Get state-specific style
let hover = stylesheet.get_with_state("button", CssElementState::Hover);
let active = stylesheet.get_with_state("button", CssElementState::Active);

// Get all states at once
let (base, states) = stylesheet.get_all_states("button");
for (state, style) in states {
    println!(":{} => {:?}", state, style.opacity);
}
}

CSS Variables

Define reusable values with custom properties:

:root {
    --brand-color: #3498db;
    --hover-opacity: 0.85;
    --card-radius: 12px;
    --spacing-md: 16px;
}

#card {
    background: var(--brand-color);
    border-radius: var(--card-radius);
    opacity: 1.0;
}

#card:hover {
    opacity: var(--hover-opacity);
}

Fallback Values

#element {
    background: var(--undefined-color, #333);  /* Uses fallback */
}

Accessing Variables Programmatically

#![allow(unused)]
fn main() {
// Get a variable value
if let Some(value) = stylesheet.get_variable("brand-color") {
    println!("Brand color: {}", value);
}

// Iterate all variables
for name in stylesheet.variable_names() {
    let value = stylesheet.get_variable(name).unwrap();
    println!("--{}: {}", name, value);
}
}

Theme Integration

Use the theme() function to reference theme tokens:

#card {
    background: theme(surface);
    border-radius: theme(radius-lg);
    box-shadow: theme(shadow-md);
}

#button {
    background: theme(primary);
}

#button:hover {
    background: theme(primary-hover);
}

Available Theme Tokens

Colors:

  • primary, primary-hover, primary-active
  • secondary, secondary-hover, secondary-active
  • success, success-bg
  • warning, warning-bg
  • error, error-bg
  • info, info-bg
  • foreground, foreground-muted
  • background, surface, surface-hover
  • border, border-muted

Radii:

  • radius-sm, radius-default, radius-md
  • radius-lg, radius-xl, radius-2xl

Shadows:

  • shadow-sm, shadow-default, shadow-md
  • shadow-lg, shadow-xl

Keyframe Animations

Define complex animations with @keyframes:

@keyframes fade-in {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes pulse {
    0%, 100% {
        opacity: 1;
        transform: scale(1);
    }
    50% {
        opacity: 0.8;
        transform: scale(1.05);
    }
}

Percentage Positions

@keyframes complex-animation {
    0% { opacity: 0; }
    25% { opacity: 0.5; transform: scale(1.1); }
    50% { opacity: 1; }
    75% { opacity: 0.5; transform: scale(0.9); }
    100% { opacity: 1; transform: scale(1); }
}

Accessing Keyframes

#![allow(unused)]
fn main() {
// Get keyframes by name
if let Some(keyframes) = stylesheet.get_keyframes("fade-in") {
    println!("Animation has {} stops", keyframes.keyframes.len());

    for kf in &keyframes.keyframes {
        println!("  {}%: opacity={:?}",
            (kf.position * 100.0) as i32,
            kf.style.opacity
        );
    }
}
}

Converting to Motion Animation

#![allow(unused)]
fn main() {
// Convert to MotionAnimation (for simple from/to animations)
let motion = keyframes.to_motion_animation(300, 200);  // enter_ms, exit_ms

// Convert to MultiKeyframeAnimation (for complex multi-step animations)
let animation = keyframes.to_multi_keyframe_animation(1000, Easing::EaseInOut);
}

Animation Property

Apply animations to elements with the animation: property:

@keyframes slide-in {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}

#modal {
    animation: slide-in 300ms ease-out;
}

Animation Shorthand

#element {
    /* animation: name duration timing-function delay iteration-count direction fill-mode */
    animation: pulse 2s ease-in-out 100ms infinite alternate forwards;
}

Individual Properties

#element {
    animation-name: pulse;
    animation-duration: 2s;
    animation-timing-function: ease-in-out;
    animation-delay: 100ms;
    animation-iteration-count: infinite;  /* or a number */
    animation-direction: alternate;        /* normal | reverse | alternate | alternate-reverse */
    animation-fill-mode: forwards;         /* none | forwards | backwards | both */
}

Automatic Application

When a stylesheet is attached to the RenderTree, elements with IDs automatically receive animations:

#![allow(unused)]
fn main() {
let css = r#"
    @keyframes card-enter {
        from { opacity: 0; transform: scale(0.95); }
        to { opacity: 1; transform: scale(1); }
    }

    #card {
        animation: card-enter 300ms ease-out;
    }
"#;

let stylesheet = Stylesheet::parse_with_errors(css).stylesheet;
render_tree.set_stylesheet(Some(Arc::new(stylesheet)));

// This element will automatically animate on render!
div()
    .id("card")
    .child(content())
}

Error Handling

The CSS parser collects errors without failing:

#![allow(unused)]
fn main() {
let css = r#"
    #card {
        background: red;
        opacity: invalid;        /* Error: invalid value */
        unknown-prop: foo;       /* Warning: unknown property */
    }
"#;

let result = Stylesheet::parse_with_errors(css);

// Check for issues
if result.has_errors() {
    println!("Has {} error(s)", result.errors_only().count());
}
if result.has_warnings() {
    println!("Has {} warning(s)", result.warnings_only().count());
}

// Print colored diagnostics to console
result.print_colored_diagnostics();
result.print_summary();

// The valid properties are still parsed!
let style = result.stylesheet.get("card").unwrap();
assert!(style.background.is_some());  // "red" was parsed
}

Error Information

#![allow(unused)]
fn main() {
for error in &result.errors {
    println!("Line {}, Column {}: {}",
        error.line,
        error.column,
        error.message
    );

    if let Some(ref prop) = error.property {
        println!("  Property: {}", prop);
    }
    if let Some(ref val) = error.value {
        println!("  Value: {}", val);
    }
}
}

Motion Container Integration

Use CSS keyframes with the Motion container:

#![allow(unused)]
fn main() {
let css = r#"
    @keyframes modal-enter {
        from { opacity: 0; transform: scale(0.9) translateY(20px); }
        to { opacity: 1; transform: scale(1) translateY(0); }
    }
"#;

let stylesheet = Stylesheet::parse_with_errors(css).stylesheet;

// Method 1: Using from_stylesheet
motion()
    .from_stylesheet(&stylesheet, "modal-enter", 300, 200)
    .child(modal_content())

// Method 2: Using keyframes_from_stylesheet for multi-step animations
motion()
    .keyframes_from_stylesheet(&stylesheet, "pulse", 1000, Easing::EaseInOut)
    .child(pulsing_element())
}

Complete Example

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
use std::sync::Arc;

fn styled_app() -> impl ElementBuilder {
    // Define styles
    let css = r#"
        :root {
            --card-bg: theme(surface);
            --card-radius: theme(radius-lg);
            --brand-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }

        @keyframes fade-in {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        #app-container {
            background: theme(background);
        }

        #card {
            background: var(--card-bg);
            border-radius: var(--card-radius);
            box-shadow: theme(shadow-md);
            animation: fade-in 300ms ease-out;
        }

        #card:hover {
            box-shadow: theme(shadow-lg);
            transform: translateY(-2px);
        }

        #gradient-card {
            background: var(--brand-gradient);
            border-radius: theme(radius-lg);
            box-shadow: theme(shadow-md);
        }

        #gradient-card:hover {
            background: linear-gradient(135deg, #7c8ff0 0%, #8b5cb8 100%);
            transform: translateY(-2px);
        }

        #primary-button {
            background: theme(primary);
            border-radius: theme(radius-default);
        }

        #primary-button:hover {
            background: theme(primary-hover);
            transform: scale(1.02);
        }

        #primary-button:active {
            transform: scale(0.98);
        }
    "#;

    let result = Stylesheet::parse_with_errors(css);
    if result.has_errors() {
        result.print_colored_diagnostics();
    }

    // In real usage, attach to render_tree
    // render_tree.set_stylesheet(Some(Arc::new(result.stylesheet)));

    div()
        .id("app-container")
        .flex_col()
        .p(24.0)
        .gap(16.0)
        .child(
            div()
                .id("card")
                .p(16.0)
                .child(text("Styled with CSS!"))
        )
        .child(
            div()
                .id("gradient-card")
                .p(16.0)
                .child(text("Gradient background!"))
        )
        .child(
            button("Click me")
                .id("primary-button")
        )
}
}

Best Practices

  1. Use CSS variables for values you want to reuse or override
  2. Use theme tokens for colors that should respect the app’s theme
  3. Check for errors after parsing to catch typos and invalid values
  4. Keep animations short for UI transitions (150-400ms)
  5. Use state modifiers for hover/active effects instead of manual callbacks
  6. Prefer ID selectors (#id) for precise targeting

Comparison with Builder API

CSSBuilder API
background: #3498db;.bg(Color::hex("#3498db"))
border-radius: 8px;.rounded(8.0)
transform: scale(1.02);.scale(1.02)
opacity: 0.8;.opacity(0.8)
box-shadow: theme(shadow-md);.shadow_md()

Both approaches can be combined - use CSS for base styles and the builder API for dynamic values.

Theming

Blinc provides a comprehensive theming system with design tokens, light/dark mode support, animated theme transitions, and platform-native color scheme detection.

Overview

The theming system is built around these core concepts:

  • Design Tokens: Semantic color, typography, spacing, and radius values
  • ThemeState: Global singleton for theme access and switching
  • Animated Transitions: Smooth spring-based color interpolation between themes
  • Platform Detection: Automatic system dark/light mode detection

Quick Start

Accessing Theme Tokens

#![allow(unused)]
fn main() {
use blinc_theme::{ThemeState, ColorToken};

fn my_component() -> impl ElementBuilder {
    let theme = ThemeState::get();

    // Get semantic colors
    let bg = theme.color(ColorToken::Background);
    let text = theme.color(ColorToken::TextPrimary);
    let primary = theme.color(ColorToken::Primary);

    // Get spacing values
    let padding = theme.spacing().space_4;

    // Get typography
    let font_size = theme.typography().text_base;

    // Get border radius
    let radius = theme.radii().radius_lg;

    div()
        .bg(bg)
        .p(padding)
        .rounded(radius)
        .child(
            text("Hello, themed world!")
                .size(font_size)
                .color(text)
        )
}
}

Toggling Color Scheme

⚠️ Known Limitation: Dynamic Theme Toggle

Dynamic theme switching at runtime (e.g., toggling between light/dark mode while the app is running) currently has significant limitations:

  • Full UI rebuild required: Theme changes trigger a complete UI tree rebuild, which is expensive and can cause visual glitches
  • on_ready callbacks fire multiple times: During theme animation, on_ready may fire repeatedly instead of once
  • Animation ticks cause rebuilds: Each frame of the theme transition animation triggers another rebuild

Recommendation: For production apps, set the theme once at startup based on user preference or system settings. Theme changes should require an app restart.

This limitation will be addressed in a future release with token-based color resolution that allows visual-only repaints without tree rebuilds.

#![allow(unused)]
fn main() {
// Toggle between light and dark mode
ThemeState::get().toggle_scheme();

// Or set explicitly
use blinc_theme::ColorScheme;
ThemeState::get().set_scheme(ColorScheme::Dark);
ThemeState::get().set_scheme(ColorScheme::Light);

// Check current scheme
let scheme = ThemeState::get().scheme();
match scheme {
    ColorScheme::Light => { /* ... */ }
    ColorScheme::Dark => { /* ... */ }
}
}

Color Tokens

Color tokens provide semantic meaning to colors, making it easy to build consistent UIs that adapt to theme changes.

Token Categories

CategoryTokensDescription
BrandPrimary, PrimaryHover, PrimaryActive, Secondary, SecondaryHover, SecondaryActiveMain brand colors
SemanticSuccess, Warning, Error, Info + *Bg variantsStatus/feedback colors
SurfaceBackground, Surface, SurfaceElevated, SurfaceOverlayBackground layers
TextTextPrimary, TextSecondary, TextTertiary, TextInverse, TextLinkText colors
BorderBorder, BorderHover, BorderFocus, BorderErrorBorder states
InputInputBg, InputBgHover, InputBgFocus, InputBgDisabledForm input backgrounds
SelectionSelection, SelectionTextText selection colors
AccentAccent, AccentSubtleAccent highlights

Usage Example

#![allow(unused)]
fn main() {
use blinc_theme::{ThemeState, ColorToken};

fn themed_card() -> impl ElementBuilder {
    let theme = ThemeState::get();

    div()
        .bg(theme.color(ColorToken::Surface))
        .border(1.0, theme.color(ColorToken::Border))
        .rounded(theme.radii().radius_lg)
        .p(theme.spacing().space_4)
        .child(
            text("Card Title")
                .size(theme.typography().text_lg)
                .color(theme.color(ColorToken::TextPrimary))
        )
        .child(
            text("Card description text")
                .size(theme.typography().text_sm)
                .color(theme.color(ColorToken::TextSecondary))
        )
}
}

Typography Tokens

Typography tokens define a consistent type scale:

TokenSizeUse Case
text_xs12pxCaptions, labels
text_sm14pxSecondary text, buttons
text_base16pxBody text
text_lg18pxLarge body text
text_xl20pxSmall headings
text_2xl24pxSection headings
text_3xl30pxPage headings
text_4xl36pxLarge headings
text_5xl48pxHero text
#![allow(unused)]
fn main() {
let theme = ThemeState::get();
let typo = theme.typography();

text("Heading").size(typo.text_2xl)
text("Body").size(typo.text_base)
text("Caption").size(typo.text_xs)
}

Spacing Tokens

Spacing follows a 4px base scale for consistent rhythm:

TokenValueUse Case
space_14pxMinimal spacing
space_28pxTight spacing
space_2_510pxBetween tight and standard
space_312pxStandard small
space_416pxStandard spacing
space_520pxMedium spacing
space_624pxLarge spacing
space_832pxSection spacing
space_1040pxLarge section spacing
space_1248pxExtra large spacing
#![allow(unused)]
fn main() {
let theme = ThemeState::get();
let spacing = theme.spacing();

div()
    .p(spacing.space_4)      // 16px padding
    .gap(spacing.space_3)    // 12px gap between children
    .my(spacing.space_6)     // 24px vertical margin
}

Radius Tokens

Border radius tokens for consistent rounded corners:

TokenValueUse Case
radius_none0pxSharp corners
radius_sm4pxSubtle rounding
radius_md6pxStandard rounding
radius_lg8pxPronounced rounding
radius_xl12pxLarge rounding
radius_2xl16pxExtra large rounding
radius_full9999pxPill shape
#![allow(unused)]
fn main() {
let theme = ThemeState::get();

div().rounded(theme.radii().radius_lg)   // 8px corners
div().rounded(theme.radii().radius_full) // Pill shape
}

Animated Theme Transitions

⚠️ Experimental Feature

Animated theme transitions are currently experimental and have known issues. See the Known Limitation above. For production use, disable animations and require app restart for theme changes.

When switching between light and dark mode, colors smoothly animate using spring physics. This happens automatically when you call toggle_scheme() or set_scheme().

How It Works

  1. Theme colors are stored as AnimatedValue in the global ThemeState
  2. When the scheme changes, target colors animate from current to new values
  3. The animation scheduler drives smooth interpolation
  4. UI rebuilds on each frame with interpolated colors (⚠️ this is the source of current performance issues)

Configuration

The transition uses a gentle spring configuration for smooth, natural motion:

#![allow(unused)]
fn main() {
// Internal spring config for theme transitions
SpringConfig::gentle()  // stiffness: 120, damping: 14
}

Reading Animated Colors

Colors are read during each render, automatically getting the interpolated value:

#![allow(unused)]
fn main() {
fn my_component() -> impl ElementBuilder {
    let theme = ThemeState::get();

    // This color will be interpolated during transitions
    let bg = theme.color(ColorToken::Background);

    div().bg(bg)
}
}

Important: Always read colors from ThemeState inside your component function, not captured in closures at initialization time. This ensures colors update during animations.


Reactive Theme Updates

For interactive elements that need to respond to theme changes within event handlers, fetch colors inside the callback:

#![allow(unused)]
fn main() {
fn themed_button(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state_for("btn", ButtonState::Idle);

    stateful(handle)
        .on_state(|state, div| {
            // Fetch colors inside callback for theme reactivity
            let theme = ThemeState::get();
            let primary = theme.color(ColorToken::Primary);
            let primary_hover = theme.color(ColorToken::PrimaryHover);

            match state {
                ButtonState::Idle => div.set_bg(primary),
                ButtonState::Hovered => div.set_bg(primary_hover),
                // ...
            }
        })
        .child(text("Click me"))
}
}

Default Theme: Catppuccin

Blinc’s default theme is derived from Catppuccin, a community-driven pastel theme:

  • Light mode: Catppuccin Latte
  • Dark mode: Catppuccin Mocha

Latte (Light) Palette

RoleColor
Background#EFF1F5
Surface#FFFFFF
Text Primary#4C4F69
Primary#1E66F5 (Blue)
Success#40A02B (Green)
Warning#DF8E1D (Yellow)
Error#D20F39 (Red)

Mocha (Dark) Palette

RoleColor
Background#1E1E2E
Surface#313244
Text Primary#CDD6F4
Primary#89B4FA (Blue)
Success#A6E3A1 (Green)
Warning#F9E2AF (Yellow)
Error#F38BA8 (Red)

Platform Color Scheme Detection

Blinc automatically detects the system’s preferred color scheme on supported platforms:

PlatformDetection Method
macOSAppleInterfaceStyle from UserDefaults
WindowsWindows.UI.ViewManagement API
LinuxXDG/GTK settings
iOSNative UITraitCollection
AndroidConfiguration.uiMode

Manual Detection

#![allow(unused)]
fn main() {
use blinc_theme::platform::detect_system_color_scheme;

// Get system preference
let scheme = detect_system_color_scheme();

// Initialize theme with system preference
ThemeState::init(BlincTheme::bundle(), scheme);
}

The WindowedApp automatically initializes the theme with system color scheme detection.


System Scheme Watcher (Optional)

⚠️ Not Recommended for Production

Due to the dynamic theme toggle limitations, the system scheme watcher is not recommended for production apps. When the system theme changes, it triggers the same problematic full UI rebuild. Consider detecting the system scheme once at startup instead.

For apps that need to automatically follow system theme changes (e.g., when the user toggles dark mode in system settings), Blinc provides an optional background watcher.

Enabling the Feature

Add the watcher feature to your Cargo.toml:

[dependencies]
blinc_theme = { version = "0.1", features = ["watcher"] }

Basic Usage

#![allow(unused)]
fn main() {
use blinc_theme::{SystemSchemeWatcher, WatcherConfig};
use std::time::Duration;

// Start watching with default interval (1 second)
let watcher = SystemSchemeWatcher::start();

// Or with a custom polling interval
let watcher = SystemSchemeWatcher::start_with_interval(Duration::from_secs(5));

// The watcher runs in a background thread and automatically updates
// ThemeState when the system color scheme changes.

// Stop watching when done (or let it drop)
// watcher.stop();
}

Using WatcherConfig

#![allow(unused)]
fn main() {
use blinc_theme::WatcherConfig;
use std::time::Duration;

// Builder pattern for configuration
let watcher = WatcherConfig::new()
    .poll_interval(Duration::from_secs(2))  // Check every 2 seconds
    .auto_start(true)                        // Start immediately
    .build();
}

How It Works

  1. The watcher runs in a background thread named blinc-scheme-watcher
  2. It polls the system color scheme at the configured interval
  3. When a change is detected, it calls ThemeState::set_scheme() automatically
  4. Theme transitions are animated smoothly using spring physics
  5. The watcher is thread-safe and cleans up when dropped

Use Cases

  • Desktop apps: Follow system dark/light mode preference
  • Long-running apps: Adapt to user changing system settings
  • Kiosk/display apps: Automatically switch themes based on time of day (if OS supports scheduling)

Performance Notes

  • The default 1-second interval is a good balance between responsiveness and CPU usage
  • For less critical apps, consider using 5-10 second intervals
  • The watcher thread sleeps between checks, consuming minimal resources

Dynamic Token Overrides

You can override individual tokens at runtime without changing the entire theme:

#![allow(unused)]
fn main() {
use blinc_theme::{ThemeState, ColorToken};
use blinc_core::Color;

// Override a specific color
ThemeState::get().set_color_override(
    ColorToken::Primary,
    Color::from_hex(0x6366F1)  // Custom brand color
);

// Remove override (revert to theme default)
ThemeState::get().remove_color_override(ColorToken::Primary);

// Clear all overrides
ThemeState::get().clear_overrides();
}

Override Types

MethodTriggers
set_color_override()Repaint only
set_spacing_override()Layout recompute
set_radius_override()Repaint only

Building Themed Components

Pattern 1: Direct Token Access

Best for simple components:

#![allow(unused)]
fn main() {
fn simple_badge(label: &str) -> impl ElementBuilder {
    let theme = ThemeState::get();

    div()
        .px(theme.spacing().space_2)
        .py(theme.spacing().space_1)
        .rounded(theme.radii().radius_md)
        .bg(theme.color(ColorToken::AccentSubtle))
        .child(
            text(label)
                .size(theme.typography().text_xs)
                .color(theme.color(ColorToken::Accent))
        )
}
}

Pattern 2: Themed Config Struct

Best for complex widgets with many options:

#![allow(unused)]
fn main() {
pub struct CardConfig {
    pub padding: f32,
    pub radius: f32,
    pub show_shadow: bool,
}

impl CardConfig {
    pub fn themed() -> Self {
        let theme = ThemeState::get();
        Self {
            padding: theme.spacing().space_4,
            radius: theme.radii().radius_lg,
            show_shadow: true,
        }
    }
}
}

Pattern 3: Color Token Parameters

For components that accept different color variants:

#![allow(unused)]
fn main() {
fn status_badge(label: &str, color_token: ColorToken) -> impl ElementBuilder {
    let theme = ThemeState::get();
    let color = theme.color(color_token);

    div()
        .px(theme.spacing().space_2)
        .py(theme.spacing().space_1)
        .rounded(theme.radii().radius_full)
        .bg(color.with_alpha(0.15))
        .child(
            text(label)
                .size(theme.typography().text_xs)
                .color(color)
        )
}

// Usage
status_badge("Success", ColorToken::Success)
status_badge("Warning", ColorToken::Warning)
status_badge("Error", ColorToken::Error)
}

Best Practices

  1. Always use semantic tokens - Use ColorToken::Primary instead of hardcoded colors for automatic theme support.

  2. Read colors at render time - Access ThemeState::get() inside your component function, not at module level.

  3. Fetch in callbacks - For on_state and other callbacks, fetch theme colors inside the callback to respond to theme changes.

  4. Use spacing scale - Use theme.spacing().space_* for consistent visual rhythm.

  5. Match radius to context - Use smaller radii for small elements, larger for cards and panels.

  6. Test both themes - Always verify your UI looks good in both light and dark modes.


Example: Complete Themed Component

#![allow(unused)]
fn main() {
use blinc_app::prelude::*;
use blinc_theme::{ThemeState, ColorToken};

fn notification_toast(
    ctx: &WindowedContext,
    message: &str,
    variant: ColorToken,
) -> impl ElementBuilder {
    let theme = ThemeState::get();
    let handle = ctx.use_state_for("toast", ButtonState::Idle);

    let bg_color = theme.color(variant);

    stateful(handle)
        .w(320.0)
        .p(theme.spacing().space_4)
        .rounded(theme.radii().radius_lg)
        .bg(bg_color.with_alpha(0.15))
        .border(1.0, bg_color.with_alpha(0.3))
        .shadow_md()
        .on_state(move |state, div| {
            let theme = ThemeState::get();
            let base = theme.color(variant);

            match state {
                ButtonState::Hovered => {
                    div.set_bg(base.with_alpha(0.2));
                }
                _ => {
                    div.set_bg(base.with_alpha(0.15));
                }
            }
        })
        .flex_row()
        .items_center()
        .gap(theme.spacing().space_3)
        .child(
            // Icon placeholder
            div()
                .w(24.0)
                .h(24.0)
                .rounded(theme.radii().radius_full)
                .bg(bg_color)
        )
        .child(
            text(message)
                .size(theme.typography().text_sm)
                .color(theme.color(ColorToken::TextPrimary))
        )
}

// Usage
notification_toast(ctx, "File saved successfully", ColorToken::Success)
notification_toast(ctx, "Network error occurred", ColorToken::Error)
notification_toast(ctx, "New update available", ColorToken::Info)
}

Event Handling

Blinc provides event handling through closures attached to elements. Events bubble up from child to parent elements.

Available Events

Pointer Events

#![allow(unused)]
fn main() {
div()
    .on_click(|ctx| {
        println!("Clicked at ({}, {})", ctx.local_x, ctx.local_y);
    })
    .on_mouse_down(|ctx| {
        println!("Mouse button pressed");
    })
    .on_mouse_up(|ctx| {
        println!("Mouse button released");
    })
}

Hover Events

#![allow(unused)]
fn main() {
div()
    .on_hover_enter(|ctx| {
        println!("Mouse entered element");
    })
    .on_hover_leave(|ctx| {
        println!("Mouse left element");
    })
}

Focus Events

#![allow(unused)]
fn main() {
div()
    .on_focus(|ctx| {
        println!("Element focused");
    })
    .on_blur(|ctx| {
        println!("Element lost focus");
    })
}

Keyboard Events

#![allow(unused)]
fn main() {
div()
    .on_key_down(|ctx| {
        println!("Key pressed: code={}", ctx.key_code);
        if ctx.ctrl && ctx.key_code == 83 {  // Ctrl+S
            println!("Save shortcut triggered!");
        }
    })
    .on_key_up(|ctx| {
        println!("Key released");
    })
    .on_text_input(|ctx| {
        if let Some(ch) = ctx.key_char {
            println!("Character typed: {}", ch);
        }
    })
}

Scroll Events

#![allow(unused)]
fn main() {
div()
    .on_scroll(|ctx| {
        println!("Scrolled: dx={}, dy={}", ctx.scroll_delta_x, ctx.scroll_delta_y);
    })
}

Drag Events

#![allow(unused)]
fn main() {
div()
    .on_drag(|ctx| {
        println!("Dragging: delta=({}, {})", ctx.drag_delta_x, ctx.drag_delta_y);
    })
    .on_drag_end(|ctx| {
        println!("Drag ended");
    })
}

Lifecycle Events

#![allow(unused)]
fn main() {
div()
    .on_mount(|ctx| {
        println!("Element added to tree");
    })
    .on_unmount(|ctx| {
        println!("Element removed from tree");
    })
    .on_resize(|ctx| {
        println!("Element resized");
    })
}

EventContext

All event handlers receive an EventContext with information about the event:

#![allow(unused)]
fn main() {
pub struct EventContext {
    pub event_type: EventType,       // Type of event
    pub node_id: LayoutNodeId,       // Element that received the event

    // Mouse position (global coordinates)
    pub mouse_x: f32,
    pub mouse_y: f32,

    // Mouse position (relative to element)
    pub local_x: f32,
    pub local_y: f32,

    // Scroll deltas (for SCROLL events)
    pub scroll_delta_x: f32,
    pub scroll_delta_y: f32,

    // Drag deltas (for DRAG events)
    pub drag_delta_x: f32,
    pub drag_delta_y: f32,

    // Keyboard (for KEY_DOWN, KEY_UP, TEXT_INPUT)
    pub key_char: Option<char>,      // Character for TEXT_INPUT
    pub key_code: u32,               // Virtual key code

    // Modifier keys
    pub shift: bool,
    pub ctrl: bool,
    pub alt: bool,
    pub meta: bool,                  // Cmd on macOS, Win on Windows
}
}

Event Patterns

Toggle on Click

Use ToggleState for toggle buttons - it handles click transitions automatically:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn toggle_button(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state(ToggleState::Off);

    stateful(handle)
        .w(100.0)
        .h(40.0)
        .rounded(8.0)
        .flex_center()
        .on_state(|state, div| {
            let bg = match state {
                ToggleState::Off => Color::rgba(0.3, 0.3, 0.35, 1.0),
                ToggleState::On => Color::rgba(0.2, 0.8, 0.4, 1.0),
            };
            div.set_bg(bg);
        })
        .on_click(|_| {
            println!("Toggled!");
            // ToggleState transitions automatically on click
        })
        .child(text("Toggle").color(Color::WHITE))
}
}

Drag to Move

#![allow(unused)]
fn main() {
use blinc_core::BlincContextState;

fn draggable_box(ctx: &WindowedContext) -> impl ElementBuilder {
    let pos_x = ctx.use_signal(100.0f32);
    let pos_y = ctx.use_signal(100.0f32);

    let x = ctx.get(pos_x).unwrap_or(100.0);
    let y = ctx.get(pos_y).unwrap_or(100.0);

    div()
        .absolute()
        .left(x)
        .top(y)
        .w(80.0)
        .h(80.0)
        .rounded(8.0)
        .bg(Color::rgba(0.4, 0.6, 1.0, 1.0))
        .on_drag(move |evt| {
            // Signal<T> is Copy, so it can be captured directly
            // Use BlincContextState to update signals from closures
            BlincContextState::get().update(pos_x, |v| v + evt.drag_delta_x);
            BlincContextState::get().update(pos_y, |v| v + evt.drag_delta_y);
        })
}
}

Keyboard Shortcuts

#![allow(unused)]
fn main() {
fn keyboard_handler(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .w_full()
        .h_full()
        .on_key_down(|evt| {
            // Ctrl+S or Cmd+S to save
            if (evt.ctrl || evt.meta) && evt.key_code == 83 {
                println!("Save triggered!");
            }
            // Escape to close
            if evt.key_code == 27 {
                println!("Escape pressed!");
            }
        })
}
}

Hover Preview

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn hover_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .w(200.0)
        .h(120.0)
        .rounded(12.0)
        .on_state(|state, div| {
            let (bg, scale) = match state {
                ButtonState::Hovered => (Color::rgba(0.2, 0.2, 0.3, 1.0), 1.02),
                _ => (Color::rgba(0.15, 0.15, 0.2, 1.0), 1.0),
            };
            div.set_bg(bg);
            div.set_transform(Transform::scale(scale, scale));
        })
        .child(text("Hover me!").color(Color::WHITE))
}
}

Capturing State in Closures

Event handlers are Fn closures. Signal<T> is Copy, so signals can be captured directly. Use BlincContextState to access signal operations from within closures:

#![allow(unused)]
fn main() {
use blinc_core::BlincContextState;

fn counter_buttons(ctx: &WindowedContext) -> impl ElementBuilder {
    let count = ctx.use_signal(0i32);

    div()
        .flex_row()
        .gap(16.0)
        .child(
            div()
                .on_click(move |_| {
                    // Signal is Copy - captured directly in the closure
                    BlincContextState::get().update(count, |v| v - 1);
                })
                .child(text("-"))
        )
        .child(text(&format!("{}", ctx.get(count).unwrap_or(0))))
        .child(
            div()
                .on_click(move |_| {
                    BlincContextState::get().update(count, |v| v + 1);
                })
                .child(text("+"))
        )
}
}

Thread Safety

BlincContextState is a thread-safe global singleton:

  • It uses Arc<Mutex<...>> for the reactive graph and hook state
  • All callbacks use RwLock for safe concurrent access
  • BlincContextState::get() returns &'static BlincContextState

This makes it safe to use in event handler closures:

#![allow(unused)]
fn main() {
div()
    .on_click(move |_| {
        // Safe: BlincContextState is thread-safe
        BlincContextState::get().update(my_signal, |v| v + 1);
        BlincContextState::get().set_focus(Some("my-input"));
        BlincContextState::get().request_rebuild();
    })
}

For shared mutable state, use Arc<Mutex<T>>:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

fn shared_state_example() -> impl ElementBuilder {
    let data = Arc::new(Mutex::new(Vec::<String>::new()));
    let data_click = Arc::clone(&data);

    div()
        .on_click(move |_| {
            data_click.lock().unwrap().push("clicked".to_string());
        })
}
}

Best Practices

  1. Keep handlers lightweight - Do minimal work in event handlers. For heavy operations, queue work or update state.

  2. Use stateful(handle) for hover/press - Instead of manually tracking hover state, use ctx.use_state() with stateful(handle) which handles state transitions automatically.

  3. Clone before closures - Clone Arc, signals, or context references before moving them into closures.

  4. Avoid nested event handlers - Events bubble up, so you rarely need deeply nested handlers.

  5. Use local coordinates - For hit testing within an element, use ctx.local_x and ctx.local_y.

State Management

Blinc uses Stateful elements as the primary way to manage UI state. Stateful elements handle state transitions automatically without rebuilding the entire UI tree.

Stateful Elements

Stateful is a wrapper element that manages visual states (hover, press, focus, etc.) efficiently. When state changes, only the affected element updates - not the entire UI.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

fn feature_card(label: &str, accent: Color) -> impl ElementBuilder {
    let label = label.to_string();

    stateful::<ButtonState>()
        .w_fit()
        .p(4.0)
        .rounded(14.0)
        .on_state(move |ctx| {
            let bg = match ctx.state() {
                ButtonState::Idle => accent,
                ButtonState::Hovered => Color::rgba(
                    (accent.r * 1.15).min(1.0),
                    (accent.g * 1.15).min(1.0),
                    (accent.b * 1.15).min(1.0),
                    accent.a,
                ),
                ButtonState::Pressed => Color::rgba(
                    accent.r * 0.85,
                    accent.g * 0.85,
                    accent.b * 0.85,
                    accent.a,
                ),
                ButtonState::Disabled => Color::GRAY,
            };

            div()
                .bg(bg)
                .on_click({
                    let label = label.clone();
                    move |_| println!("'{}' clicked!", label)
                })
                .child(text(&label).color(Color::WHITE))
        })
}
}

How It Works

  1. stateful::<S>() creates a StatefulBuilder for state type S
  2. .on_state(|ctx| ...) defines the callback that receives a StateContext
  3. Events (hover, click, etc.) trigger automatic state transitions
  4. ctx.state() returns the current state for pattern matching
  5. Return a Div from the callback - it’s merged onto the container

StateContext

The StateContext provides access to state and scoped utilities within your callback:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Get current state
        let state = ctx.state();

        // Create scoped signals (persist across rebuilds)
        let counter = ctx.use_signal("counter", || 0);

        // Create scoped animated values
        let opacity = ctx.use_animated_value("opacity", 1.0);

        // Access dependency values
        let value: i32 = ctx.dep(0).unwrap_or_default();

        // Dispatch events to trigger state transitions
        // ctx.dispatch(CUSTOM_EVENT);

        div().bg(color_for_state(state))
    })
}

StateContext Methods

MethodDescription
ctx.state()Get the current state value
ctx.event()Get the event that triggered this callback (if any)
ctx.use_signal(name, init)Create/retrieve a scoped signal
ctx.use_spring(name, target, config)Declarative spring animation (recommended)
ctx.spring(name, target)Declarative spring with default stiff config
ctx.use_animated_value(name, initial)Low-level animated value handle
ctx.use_timeline(name)Create/retrieve an animated timeline
ctx.dep::<T>(index)Get dependency value by index
ctx.dep_as_state::<T>(index)Get dependency as State handle
ctx.dispatch(event)Trigger a state transition

Event Access

Use ctx.event() to access the event that triggered the callback:

#![allow(unused)]
fn main() {
use blinc_core::events::event_types::*;

stateful::<ButtonState>()
    .on_state(|ctx| {
        // ctx.event() returns Some(EventContext) when triggered by user event
        // Returns None when triggered by dependency changes
        if let Some(event) = ctx.event() {
            match event.event_type {
                POINTER_UP => {
                    println!("Clicked at ({}, {})", event.local_x, event.local_y);
                }
                POINTER_ENTER => {
                    println!("Mouse entered!");
                }
                KEY_DOWN => {
                    if event.ctrl && event.key_code == 83 {  // Ctrl+S
                        println!("Save shortcut pressed!");
                    }
                }
                _ => {}
            }
        }

        let bg = match ctx.state() {
            ButtonState::Idle => Color::BLUE,
            ButtonState::Hovered => Color::CYAN,
            ButtonState::Pressed => Color::DARK_BLUE,
            _ => Color::GRAY,
        };

        div().bg(bg)
    })
}

EventContext Fields

FieldTypeDescription
event_typeu32Event type (POINTER_UP, POINTER_ENTER, etc.)
node_idLayoutNodeIdThe node that received the event
mouse_x, mouse_yf32Absolute mouse position
local_x, local_yf32Position relative to element bounds
bounds_x, bounds_yf32Element position (top-left corner)
bounds_width, bounds_heightf32Element dimensions
scroll_delta_x, scroll_delta_yf32Scroll delta (for SCROLL events)
drag_delta_x, drag_delta_yf32Drag offset (for DRAG events)
key_charOption<char>Character (for TEXT_INPUT events)
key_codeu32Key code (for KEY_DOWN/KEY_UP events)
shift, ctrl, alt, metaboolModifier key states

Setting Initial State

Use .initial() to set the initial state:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .initial(if disabled { ButtonState::Disabled } else { ButtonState::Idle })
    .on_state(|ctx| {
        // ...
        div()
    })
}

Signal Dependencies with .deps()

When a Stateful element needs to react to external signal changes (not just hover/press events), use .deps() to declare dependencies:

#![allow(unused)]
fn main() {
fn direction_toggle() -> impl ElementBuilder {
    // External state that affects the element's appearance
    let direction = use_state_keyed("direction", || Direction::Horizontal);

    stateful::<ButtonState>()
        .w(120.0)
        .h(40.0)
        .rounded(8.0)
        // Declare dependency - on_state re-runs when this signal changes
        .deps([direction.signal_id()])
        .on_state(move |ctx| {
            // Read the current direction value
            let dir = direction.get();
            let label = match dir {
                Direction::Horizontal => "Horizontal",
                Direction::Vertical => "Vertical",
            };

            let bg = match ctx.state() {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };

            div()
                .bg(bg)
                .on_click(move |_| {
                    // Toggle direction
                    direction.update(|d| match d {
                        Direction::Horizontal => Direction::Vertical,
                        Direction::Vertical => Direction::Horizontal,
                    });
                })
                .child(text(label).color(Color::WHITE))
        })
}
}

Accessing Dependencies via StateContext

You can access dependency values directly from the context using ctx.dep():

#![allow(unused)]
fn main() {
let count_signal: State<i32> = use_state(|| 0);
let name_signal: State<String> = use_state(|| "".to_string());

stateful::<ButtonState>()
    .deps([count_signal.signal_id(), name_signal.signal_id()])
    .on_state(|ctx| {
        // Access by index (matches order in .deps())
        let count: i32 = ctx.dep(0).unwrap_or_default();
        let name: String = ctx.dep(1).unwrap_or_default();

        // Or get a full State<T> handle for reading and writing
        if let Some(count_state) = ctx.dep_as_state::<i32>(0) {
            let value = count_state.get();
            // count_state.set(value + 1);
        }

        div().child(text(&format!("{}: {}", name, count)))
    })
}

When to Use .deps()

Use .deps() when your on_state callback reads values from signals that can change independently of the element’s internal state transitions.

Without .deps(), the on_state callback only runs when:

  • The element’s state changes (Idle → Hovered, etc.)

With .deps(), it also runs when:

  • Any of the declared signal dependencies change

Scoped Signals

Use ctx.use_signal() for state that’s scoped to the stateful container:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // This signal is keyed to this specific stateful container
        // Format: "{stateful_key}:signal:click_count"
        let click_count = ctx.use_signal("click_count", || 0);

        div()
            .child(text(&format!("Clicks: {}", click_count.get())))
            .on_click(move |_| {
                click_count.update(|n| n + 1);
            })
    })
}

Animated Values

Use ctx.use_spring() for declarative spring animations - specify the target and get the current animated value:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Declarative: specify target, get current value
        let target_scale = match ctx.state() {
            ButtonState::Hovered => 1.1,
            _ => 1.0,
        };
        let current_scale = ctx.use_spring("scale", target_scale, SpringConfig::wobbly());

        // For default stiff spring, use ctx.spring()
        let opacity = ctx.spring("opacity", if ctx.state() == ButtonState::Idle { 0.8 } else { 1.0 });

        div()
            .transform(Transform::scale(current_scale, current_scale))
            .opacity(opacity)
    })
}

Low-Level API

For more control, use ctx.use_animated_value() which returns a SharedAnimatedValue:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Get the animated value handle
        let scale = ctx.use_animated_value("scale", 1.0);

        // With custom spring config
        let opacity = ctx.use_animated_value_with_config(
            "opacity",
            1.0,
            SpringConfig::bouncy(),
        );

        // Manually set target and get value
        match ctx.state() {
            ButtonState::Hovered => {
                scale.lock().unwrap().set_target(1.1);
            }
            _ => {
                scale.lock().unwrap().set_target(1.0);
            }
        }

        let current_scale = scale.lock().unwrap().get();
        div().transform(Transform::scale(current_scale, current_scale))
    })
}

Animated Timelines

Use ctx.use_timeline() for complex multi-property animations with keyframes:

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Persisted timeline scoped to this stateful
        let timeline = ctx.use_timeline("pulse");

        // Configure on first use, get existing entry IDs on subsequent calls
        let opacity_id = timeline.lock().unwrap().configure(|t| {
            let id = t.add(0, 1000, 0.5, 1.0);  // 0ms offset, 1000ms duration
            t.set_loop(-1);  // Loop forever
            t.start();
            id
        });

        let opacity = timeline.lock().unwrap().get(opacity_id);
        div().opacity(opacity)
    })
}

The configure() method is idempotent - it only runs the configuration closure on the first call and returns existing entry IDs on subsequent calls.


Built-in State Types

Blinc provides common state types with automatic transitions:

ButtonState

#![allow(unused)]
fn main() {
ButtonState::Idle      // Default state
ButtonState::Hovered   // Mouse over element
ButtonState::Pressed   // Mouse button down
ButtonState::Disabled  // Non-interactive
}

Transitions:

  • IdleHovered (on pointer enter)
  • HoveredIdle (on pointer leave)
  • HoveredPressed (on pointer down)
  • PressedHovered (on pointer up)

NoState

For containers that only need dependency tracking without state transitions:

#![allow(unused)]
fn main() {
stateful::<NoState>()
    .deps([some_signal.signal_id()])
    .on_state(|_ctx| {
        // Rebuilds when dependencies change
        div().child(text("Content"))
    })
}

Custom State Types

Define your own state enum for complex interactions:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::StateTransitions;
use blinc_core::events::event_types::*;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum DragState {
    #[default]
    Idle,
    Hovering,
    Dragging,
}

impl StateTransitions for DragState {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            (DragState::Idle, POINTER_ENTER) => Some(DragState::Hovering),
            (DragState::Hovering, POINTER_LEAVE) => Some(DragState::Idle),
            (DragState::Hovering, POINTER_DOWN) => Some(DragState::Dragging),
            (DragState::Dragging, POINTER_UP) => Some(DragState::Idle),
            _ => None,
        }
    }
}

fn draggable_item() -> impl ElementBuilder {
    stateful::<DragState>()
        .w(100.0)
        .h(100.0)
        .rounded(8.0)
        .on_state(|ctx| {
            let bg = match ctx.state() {
                DragState::Idle => Color::BLUE,
                DragState::Hovering => Color::CYAN,
                DragState::Dragging => Color::GREEN,
            };
            div().bg(bg)
        })
}
}

Keyed State (Global Signals)

For state persisted across UI rebuilds with a string key:

#![allow(unused)]
fn main() {
let is_expanded = use_state_keyed("sidebar_expanded", || false);

// Read
let expanded = is_expanded.get();

// Update
is_expanded.set(true);
is_expanded.update(|v| !v);

// Get signal ID for use with .deps()
let signal_id = is_expanded.signal_id();
}

Best Practices

  1. Use stateful::<S>() builder - This is the primary pattern for stateful UI elements.

  2. Return Div from callbacks - The new API expects you to return a Div, not mutate a container.

  3. Use .initial() for non-default states - Set initial state explicitly when needed.

  4. Use ctx.use_signal() for local state - Scoped signals are automatically keyed.

  5. Use ctx.dep() for dependency access - Cleaner than capturing signals in closures.

  6. Prefer built-in state types - They have correct transitions already defined.

  7. Custom states for complex flows - Define your own when built-in types don’t fit.

  8. Use .deps() for external dependencies - When on_state needs to react to signal changes.

Spring Physics

Blinc uses spring physics for natural, responsive animations. Springs provide smooth motion that feels organic compared to fixed-duration easing.

SpringConfig

All spring animations are configured with SpringConfig:

#![allow(unused)]
fn main() {
use blinc_animation::SpringConfig;

// Custom spring
let config = SpringConfig {
    stiffness: 180.0,    // How "tight" the spring is
    damping: 12.0,       // How quickly oscillation settles
    mass: 1.0,           // Virtual mass of the object
    ..Default::default()
};
}

Presets

Blinc provides common spring presets:

#![allow(unused)]
fn main() {
SpringConfig::stiff()    // Fast, minimal overshoot (stiffness: 400, damping: 30)
SpringConfig::snappy()   // Quick with slight bounce (stiffness: 300, damping: 20)
SpringConfig::gentle()   // Soft, slower motion (stiffness: 120, damping: 14)
SpringConfig::wobbly()   // Bouncy, playful (stiffness: 180, damping: 12)
}

Choosing a Spring

Use CasePresetFeel
Button press feedbackstiff()Immediate, snappy
Menu/panel transitionssnappy()Quick with character
Drag releasegentle()Smooth, natural
Playful interactionswobbly()Fun, bouncy

AnimatedValue

AnimatedValue wraps a single f32 value with spring physics:

Creating AnimatedValues

#![allow(unused)]
fn main() {
fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
    // Create a persisted animated value
    let scale = ctx.use_animated_value(1.0, SpringConfig::snappy());

    // With a custom key
    let x_pos = ctx.use_animated_value_for("card_x", 0.0, SpringConfig::gentle());

    // ...
}
}

Reading Values

#![allow(unused)]
fn main() {
// Get current animated value
let current = scale.lock().unwrap().get();

// Use in transforms
div().scale(current)
}

Setting Targets

#![allow(unused)]
fn main() {
// Animate to new target
scale.lock().unwrap().set_target(1.2);

// Immediate set (no animation)
scale.lock().unwrap().set(1.0);
}

Example: Hover Scale with Spring Animation

For smooth spring-animated hover effects, use motion() with animated values:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use blinc_layout::motion::motion;

fn hover_scale_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let scale = ctx.use_animated_value(1.0, SpringConfig::snappy());

    let hover_scale = Arc::clone(&scale);
    let leave_scale = Arc::clone(&scale);

    // motion() is a container - apply transforms to it, style the child
    motion()
        .scale(scale.lock().unwrap().get())
        .on_hover_enter(move |_| {
            hover_scale.lock().unwrap().set_target(1.05);
        })
        .on_hover_leave(move |_| {
            leave_scale.lock().unwrap().set_target(1.0);
        })
        .child(
            div()
                .w(200.0)
                .h(120.0)
                .rounded(12.0)
                .bg(Color::rgba(0.2, 0.2, 0.3, 1.0))
                .flex_center()
                .child(text("Hover me").color(Color::WHITE))
        )
}
}

Note: For simple hover state changes without spring physics (e.g., just color changes), prefer stateful(handle) which is more efficient. Use motion() when you specifically need spring-animated values.

Example: Drag Position

Use motion() for elements with animated position:

#![allow(unused)]
fn main() {
use blinc_layout::motion::motion;

fn draggable_element(ctx: &WindowedContext) -> impl ElementBuilder {
    let x = ctx.use_animated_value(100.0, SpringConfig::wobbly());
    let y = ctx.use_animated_value(100.0, SpringConfig::wobbly());

    let drag_x = Arc::clone(&x);
    let drag_y = Arc::clone(&y);

    // motion() handles the animated position, child has the styling
    motion()
        .absolute()
        .left(x.lock().unwrap().get())
        .top(y.lock().unwrap().get())
        .on_drag(move |evt| {
            let mut x = drag_x.lock().unwrap();
            let mut y = drag_y.lock().unwrap();
            x.set_target(x.target() + evt.drag_delta_x);
            y.set_target(y.target() + evt.drag_delta_y);
        })
        .child(
            div()
                .w(80.0)
                .h(80.0)
                .rounded(8.0)
                .bg(Color::rgba(0.4, 0.6, 1.0, 1.0))
        )
}
}

Motion Containers

For declarative enter/exit animations, use motion():

#![allow(unused)]
fn main() {
use blinc_layout::motion::motion;

motion()
    .fade_in(300)      // Fade in over 300ms
    .child(my_content())

motion()
    .scale_in(300)     // Scale from 0 to 1
    .child(my_content())

motion()
    .slide_in(SlideDirection::Left, 300)
    .child(my_content())
}

See Motion Containers for full details.


Best Practices

  1. Match spring to interaction - Use stiffer springs for immediate feedback, gentler for ambient motion.

  2. Persist animated values - Use ctx.use_animated_value() so animations survive UI rebuilds.

  3. Clone Arc before closures - Always Arc::clone() before moving into event handlers.

  4. Don’t fight the spring - Let animations complete naturally. Interrupting with new targets is fine.

  5. Use BlincComponent - For complex components with multiple animations, use the derive macro for type-safe hooks.

Keyframe Timelines

For time-based animations with precise control, use AnimatedTimeline. Timelines support multiple animation entries, looping, alternate (ping-pong) mode, and coordinated playback.

Creating Timelines

In WindowedContext

#![allow(unused)]
fn main() {
fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
    // Create a persisted timeline
    let timeline = ctx.use_animated_timeline();

    // With a custom key
    let loader_timeline = ctx.use_animated_timeline_for("loader");

    // ...
}
}

In StateContext (Stateful Elements)

#![allow(unused)]
fn main() {
stateful::<ButtonState>().on_state(|ctx| {
    // use_timeline returns (entry_ids, TimelineHandle)
    let ((entry1, entry2), timeline) = ctx.use_timeline("fade", |t| {
        let e1 = t.add(0, 500, 0.0, 1.0);
        let e2 = t.add(250, 500, 0.0, 100.0);
        t.set_loop(-1);
        t.start();
        (e1, e2)
    });

    let value1 = timeline.get(entry1).unwrap_or(0.0);
    let value2 = timeline.get(entry2).unwrap_or(0.0);

    div()
})
}

Configuring Timelines

Use the configure() method to set up animations once:

#![allow(unused)]
fn main() {
let timeline = ctx.use_animated_timeline();

// Configure returns entry IDs for accessing values later
let entry_id = timeline.lock().unwrap().configure(|t| {
    let id = t.add(0, 1000, 0.0, 360.0);  // 0ms start, 1000ms duration, 0° to 360°
    t.set_loop(-1);  // Loop forever (-1 = infinite)
    t.start();
    id
});
}

The closure only runs on first call. Subsequent calls return existing entry IDs.

Adding Animations

Basic Entry

#![allow(unused)]
fn main() {
timeline.lock().unwrap().configure(|t| {
    // add(offset_ms, duration_ms, start_value, end_value)
    let rotation = t.add(0, 1000, 0.0, 360.0);
    let scale = t.add(0, 500, 1.0, 1.5);       // Same start, shorter duration
    let opacity = t.add(500, 500, 1.0, 0.0);   // Starts at 500ms

    (rotation, scale, opacity)  // Return tuple of IDs
});
}

With Easing

#![allow(unused)]
fn main() {
use blinc_animation::Easing;

timeline.lock().unwrap().configure(|t| {
    // add_with_easing(offset_ms, duration_ms, start, end, easing)
    let smooth = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
    let bouncy = t.add_with_easing(0, 500, 0.0, 1.0, Easing::EaseOutQuad);

    (smooth, bouncy)
});
}

Using StaggerBuilder

For multiple entries with automatic offset calculation:

#![allow(unused)]
fn main() {
timeline.lock().unwrap().configure(|t| {
    // stagger(base_offset, stagger_amount)
    let mut stagger = t.stagger(0, 100);  // 0ms base, 100ms between each

    let bar1 = stagger.add(500, 0.0, 60.0);  // offset: 0ms
    let bar2 = stagger.add(500, 0.0, 60.0);  // offset: 100ms
    let bar3 = stagger.add(500, 0.0, 60.0);  // offset: 200ms

    // With easing
    let bar4 = stagger.add_with_easing(500, 0.0, 60.0, Easing::EaseInOut);

    (bar1, bar2, bar3, bar4)
});
}

Reading Values

#![allow(unused)]
fn main() {
let value = timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);

// Get entry progress (0.0 to 1.0)
let progress = timeline.lock().unwrap().entry_progress(entry_id);

// Get overall timeline progress
let total_progress = timeline.lock().unwrap().progress();
}

Playback Control

#![allow(unused)]
fn main() {
let mut t = timeline.lock().unwrap();

t.start();              // Start playing
t.pause();              // Pause (can resume)
t.resume();             // Resume from pause
t.stop();               // Stop and reset
t.restart();            // Start from beginning
t.reverse();            // Toggle playback direction
t.seek(500.0);          // Jump to 500ms position

t.set_loop(3);          // Loop 3 times
t.set_loop(-1);         // Loop forever
t.set_alternate(true);  // Ping-pong mode
t.set_playback_rate(2.0); // 2x speed

t.is_playing();         // Check if playing
t.progress();           // Overall progress (0.0 to 1.0)
}

Alternate (Ping-Pong) Mode

Enable alternate mode for back-and-forth animations that maintain stagger across loops:

#![allow(unused)]
fn main() {
let ((bar1, bar2, bar3), timeline) = ctx.use_timeline("bars", |t| {
    // Three staggered entries
    let b1 = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
    let b2 = t.add_with_easing(100, 500, 0.0, 60.0, Easing::EaseInOut);
    let b3 = t.add_with_easing(200, 500, 0.0, 60.0, Easing::EaseInOut);

    t.set_alternate(true);  // Reverse on each loop
    t.set_loop(-1);         // Loop forever
    t.start();

    (b1, b2, b3)
});
}

With alternate mode:

  • Timeline plays forward (0 → duration)
  • On completion, reverses direction (duration → 0)
  • Stagger offsets maintain their relative timing
  • No jump back to start - smooth continuous motion

Example: Staggered Wave Animation

#![allow(unused)]
fn main() {
fn sliding_bars() -> impl ElementBuilder {
    stateful::<NoState>().on_state(|ctx| {
        let ((bar1_id, bar2_id, bar3_id), timeline) = ctx.use_timeline("bars", |t| {
            // Staggered entries with easing
            let bar1 = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
            let bar2 = t.add_with_easing(100, 500, 0.0, 60.0, Easing::EaseInOut);
            let bar3 = t.add_with_easing(200, 500, 0.0, 60.0, Easing::EaseInOut);

            t.set_alternate(true);
            t.set_loop(-1);
            t.start();

            (bar1, bar2, bar3)
        });

        let bar1_x = timeline.get(bar1_id).unwrap_or(0.0);
        let bar2_x = timeline.get(bar2_id).unwrap_or(0.0);
        let bar3_x = timeline.get(bar3_id).unwrap_or(0.0);

        div()
            .flex_col()
            .gap(12.0)
            .child(div().w(30.0).h(12.0).bg(Color::GREEN)
                .transform(Transform::translate(bar1_x, 0.0)))
            .child(div().w(30.0).h(12.0).bg(Color::YELLOW)
                .transform(Transform::translate(bar2_x, 0.0)))
            .child(div().w(30.0).h(12.0).bg(Color::RED)
                .transform(Transform::translate(bar3_x, 0.0)))
    })
}
}

Example: Spinning Loader

#![allow(unused)]
fn main() {
use std::f32::consts::PI;

fn spinning_loader(ctx: &WindowedContext) -> impl ElementBuilder {
    let timeline = ctx.use_animated_timeline();

    let entry_id = timeline.lock().unwrap().configure(|t| {
        let id = t.add(0, 1000, 0.0, 360.0);
        t.set_loop(-1);
        t.start();
        id
    });

    let render_timeline = Arc::clone(&timeline);

    canvas(move |draw_ctx, bounds| {
        let angle_deg = render_timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
        let angle_rad = angle_deg * PI / 180.0;

        let cx = bounds.width / 2.0;
        let cy = bounds.height / 2.0;
        let radius = 30.0;

        // Draw spinning arc
        // ... drawing code
    })
    .w(80.0)
    .h(80.0)
}
}

Example: Pulsing Ring

#![allow(unused)]
fn main() {
fn pulsing_ring() -> impl ElementBuilder {
    stateful::<ButtonState>().on_state(|ctx| {
        let is_running = ctx.use_signal("running", || false);

        // Keyframe animations with ping-pong
        let scale = ctx.use_keyframes("scale", |k| {
            k.at(0, 0.8)
             .at(800, 1.2)
             .ease(Easing::EaseInOut)
             .ping_pong()
             .loop_infinite()
        });

        let opacity = ctx.use_keyframes("opacity", |k| {
            k.at(0, 0.4)
             .at(800, 1.0)
             .ease(Easing::EaseInOut)
             .ping_pong()
             .loop_infinite()
        });

        // Toggle on click
        if let Some(event) = ctx.event() {
            if event.event_type == POINTER_UP {
                if is_running.get() {
                    scale.stop();
                    opacity.stop();
                    is_running.set(false);
                } else {
                    scale.start();
                    opacity.start();
                    is_running.set(true);
                }
            }
        }

        let s = scale.get();
        let o = opacity.get();

        div()
            .w(60.0).h(60.0)
            .border(4.0, Color::rgba(1.0, 0.5, 0.3, o))
            .rounded(30.0)
            .transform(Transform::scale(s, s))
    })
}
}

Example: Progress Bar

#![allow(unused)]
fn main() {
fn animated_progress(ctx: &WindowedContext) -> impl ElementBuilder {
    let timeline = ctx.use_animated_timeline();

    let entry_id = timeline.lock().unwrap().configure(|t| {
        let id = t.add(0, 2000, 0.0, 1.0);  // 2 second fill
        t.start();
        id
    });

    let click_timeline = Arc::clone(&timeline);
    let render_timeline = Arc::clone(&timeline);

    div()
        .w(200.0)
        .h(20.0)
        .rounded(10.0)
        .bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
        .on_click(move |_| {
            // Restart on click
            let mut t = click_timeline.lock().unwrap();
            t.stop();
            t.start();
        })
        .child(
            canvas(move |draw_ctx, bounds| {
                let progress = render_timeline.lock().unwrap()
                    .get(entry_id)
                    .unwrap_or(0.0);

                let fill_width = bounds.width * progress;
                draw_ctx.fill_rect(
                    Rect::new(0.0, 0.0, fill_width, bounds.height),
                    CornerRadius::uniform(10.0),
                    Brush::Solid(Color::rgba(0.4, 0.6, 1.0, 1.0)),
                );
            })
            .w_full()
            .h_full()
        )
}
}

ConfigureResult Types

The configure() method supports various return types:

#![allow(unused)]
fn main() {
// Single entry
let id: TimelineEntryId = t.configure(|t| t.add(...));

// Tuple of entries
let (a, b): (TimelineEntryId, TimelineEntryId) = t.configure(|t| {
    (t.add(...), t.add(...))
});

// Triple
let (a, b, c) = t.configure(|t| {
    (t.add(...), t.add(...), t.add(...))
});

// Vec for dynamic counts
let ids: Vec<TimelineEntryId> = t.configure(|t| {
    (0..5).map(|i| t.add(i * 100, 500, 0.0, 1.0)).collect()
});
}

Available Easing Functions

#![allow(unused)]
fn main() {
use blinc_animation::Easing;

Easing::Linear          // No easing
Easing::EaseIn          // Slow start (cubic)
Easing::EaseOut         // Slow end (cubic)
Easing::EaseInOut       // Slow start and end (cubic)
Easing::EaseInQuad      // Quadratic ease in
Easing::EaseOutQuad     // Quadratic ease out
Easing::EaseInOutQuad   // Quadratic ease in-out
Easing::EaseInCubic     // Cubic ease in
Easing::EaseOutCubic    // Cubic ease out
Easing::EaseInOutCubic  // Cubic ease in-out
Easing::EaseInQuart     // Quartic ease in
Easing::EaseOutQuart    // Quartic ease out
Easing::EaseInOutQuart  // Quartic ease in-out
Easing::CubicBezier(x1, y1, x2, y2)  // Custom bezier curve
}

Timeline vs Spring

FeatureTimelineSpring
DurationFixedPhysics-based
LoopingBuilt-inManual
Multiple valuesSingle timelineIndividual values
Ping-pongset_alternate(true)Manual reverse
InterruptionRestart neededNatural blend
Use caseContinuous loops, sequencesInteractive, responsive

Use timelines for:

  • Loading spinners
  • Background animations
  • Sequenced animations
  • Staggered wave effects
  • Precise timing control

Use springs for:

  • User interactions
  • Drag and drop
  • Hover effects
  • Natural motion

Motion Containers

The motion() element provides declarative enter/exit animations for content. It’s ideal for animated lists, page transitions, and conditional rendering.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_layout::motion::motion;

motion()
    .fade_in(300)     // Duration in milliseconds
    .child(my_content())
}

Animation Presets

Fade

#![allow(unused)]
fn main() {
motion()
    .fade_in(300)
    .fade_out(200)
    .child(content)
}

Scale

#![allow(unused)]
fn main() {
motion()
    .scale_in(300)    // Scale from 0 to 1
    .scale_out(200)   // Scale from 1 to 0
    .child(content)
}

Slide

#![allow(unused)]
fn main() {
use blinc_layout::motion::SlideDirection;

motion()
    .slide_in(SlideDirection::Left, 300)
    .slide_out(SlideDirection::Right, 200)
    .child(content)

// Available directions:
// SlideDirection::Top
// SlideDirection::Bottom
// SlideDirection::Left
// SlideDirection::Right
}

Bounce

#![allow(unused)]
fn main() {
motion()
    .bounce_in(400)   // Bouncy entrance
    .bounce_out(200)
    .child(content)
}

Pop

#![allow(unused)]
fn main() {
motion()
    .pop_in(300)      // Scale with overshoot
    .pop_out(200)
    .child(content)
}

Combining Animations

Apply multiple effects:

#![allow(unused)]
fn main() {
motion()
    .fade_in(300)
    .scale_in(300)
    .child(content)
}

Staggered Lists

Animate list items with delays between each:

#![allow(unused)]
fn main() {
use blinc_layout::motion::{motion, StaggerConfig, AnimationPreset};

let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];

motion()
    .stagger(
        StaggerConfig::new(100, AnimationPreset::fade_in(300))
    )
    .children(
        items.iter().map(|item| {
            div()
                .p(12.0)
                .bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
                .child(text(*item).color(Color::WHITE))
        })
    )
}

Stagger Configuration

#![allow(unused)]
fn main() {
StaggerConfig::new(delay_ms, preset)
    .reverse()          // Animate last to first
    .from_center()      // Animate from center outward
    .limit(10)          // Only stagger first N items
}

Stagger Directions

#![allow(unused)]
fn main() {
// Forward (default): 0, 1, 2, 3, 4...
StaggerConfig::new(100, preset)

// Reverse: 4, 3, 2, 1, 0...
StaggerConfig::new(100, preset).reverse()

// From center: 2, 1/3, 0/4 (for 5 items)
StaggerConfig::new(100, preset).from_center()
}

Binding to AnimatedValue

For continuous animation control, bind motion transforms to AnimatedValue:

#![allow(unused)]
fn main() {
fn pull_to_refresh(ctx: &WindowedContext) -> impl ElementBuilder {
    let offset_y = ctx.use_animated_value(0.0, SpringConfig::wobbly());
    let icon_scale = ctx.use_animated_value(0.5, SpringConfig::snappy());
    let icon_opacity = ctx.use_animated_value(0.0, SpringConfig::snappy());

    stack()
        // Refresh icon (behind content)
        .child(
            motion()
                .scale(icon_scale.clone())
                .opacity(icon_opacity.clone())
                .child(refresh_icon())
        )
        // Content (translates down to reveal icon)
        .child(
            motion()
                .translate_y(offset_y.clone())
                .child(content_list())
        )
}
}

Example: Animated Card List

Use a stateful element with .deps() to react to visibility state changes:

#![allow(unused)]
fn main() {
fn animated_card_list(ctx: &WindowedContext) -> impl ElementBuilder {
    let show_cards = ctx.use_state_keyed("show_cards", || true);
    let button_handle = ctx.use_state(ButtonState::Idle);

    stateful(button_handle)
        .flex_col()
        .gap(16.0)
        .deps(&[show_cards.signal_id()])
        .on_state(move |state, container| {
            let visible = show_cards.get();
            let label = if visible { "Hide Cards" } else { "Show Cards" };

            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };

            // Build content based on visibility
            let mut content = div()
                .bg(bg)
                .px(16.0)
                .py(8.0)
                .rounded(8.0)
                .child(text(label).color(Color::WHITE));

            container.merge(content);
        })
        .on_click(move |_| {
            show_cards.update(|v| !v);
        })
        .child(card_list(ctx))
}

fn card_list(ctx: &WindowedContext) -> impl ElementBuilder {
    // Cards with staggered animation
    motion()
        .stagger(StaggerConfig::new(80, AnimationPreset::fade_in(300)))
        .children(
            (0..5).map(|i| {
                div()
                    .w(300.0)
                    .p(16.0)
                    .rounded(12.0)
                    .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
                    .child(text(&format!("Card {}", i + 1)).color(Color::WHITE))
            })
        )
}
}

Example: Page Transition

Use a custom state type for page navigation:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::{stateful, StateTransitions};

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Page {
    Home,
    Settings,
    Profile,
}

// Pages don't auto-transition - we change them programmatically
impl StateTransitions for Page {
    fn on_event(&self, _event: u32) -> Option<Self> {
        None  // No automatic transitions
    }
}

fn page_transition(ctx: &WindowedContext) -> impl ElementBuilder {
    let page_handle = ctx.use_state(Page::Home);

    stateful(page_handle.clone())
        .w_full()
        .h_full()
        .on_state(move |page, container| {
            // Render different content based on current page
            let content = match page {
                Page::Home => div().child(text("Home Page").color(Color::WHITE)),
                Page::Settings => div().child(text("Settings Page").color(Color::WHITE)),
                Page::Profile => div().child(text("Profile Page").color(Color::WHITE)),
            };

            container.merge(
                div()
                    .child(
                        motion()
                            .fade_in(200)
                            .slide_in(SlideDirection::Right, 200)
                            .child(content)
                    )
            );
        })
}

// Navigate programmatically
fn nav_button(ctx: &WindowedContext, target: Page, label: &str) -> impl ElementBuilder {
    let page_handle = ctx.use_state(Page::Home);  // Same handle
    let handle = ctx.use_state_for(label, ButtonState::Idle);

    stateful(handle)
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };
            div.set_bg(bg);
        })
        .on_click(move |_| {
            page_handle.set(target);
        })
        .child(text(label).color(Color::WHITE))
}
}

Motion vs Manual Animation

FeatureMotionAnimatedValue
SetupDeclarativeImperative
ControlPreset-basedFull control
Enter/ExitBuilt-inManual
ListsStagger supportManual delays
Use caseTransitionsInteractive

Use motion for:

  • List item animations
  • Page transitions
  • Conditional content
  • Staggered reveals

Use AnimatedValue for:

  • Drag interactions
  • Hover effects
  • Custom physics
  • Continuous binding

BlincComponent Macro

The BlincComponent derive macro generates type-safe hooks for state and animations, eliminating manual string keys and reducing boilerplate. Use it to define component-scoped state that persists across UI rebuilds.

Overview

BlincComponent is designed for two primary use cases:

  1. State Management - Generate State<T> hooks for component data (counters, toggles, form values)
  2. Animations - Generate SharedAnimatedValue hooks for spring-based animations

Basic Usage

#![allow(unused)]
fn main() {
use blinc_app::prelude::*;

#[derive(BlincComponent)]
struct MyComponent;
}

This generates:

  • MyComponent::COMPONENT_KEY - Unique compile-time key
  • MyComponent::use_animated_value(ctx, initial, config) - Spring animation
  • MyComponent::use_animated_value_with(ctx, suffix, initial, config) - Named spring
  • MyComponent::use_animated_timeline(ctx) - Keyframe timeline
  • MyComponent::use_animated_timeline_with(ctx, suffix) - Named timeline

State Fields

Fields without #[animation] generate state hooks:

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct Counter {
    count: i32,              // Generates: use_count(ctx, initial) -> State<i32>
    step: i32,               // Generates: use_step(ctx, initial) -> State<i32>
}
}

Using State Fields

#![allow(unused)]
fn main() {
fn counter_demo(ctx: &WindowedContext) -> impl ElementBuilder {
    // BlincComponent generates type-safe state hooks
    let count = Counter::use_count(ctx, 0);
    let step = Counter::use_step(ctx, 1);

    // Create persistent button state handle
    let button_handle = ctx.use_state(ButtonState::Idle);

    // Use stateful(handle) with .deps() to react to state changes
    stateful(button_handle)
        .flex_col()
        .gap(16.0)
        .p(16.0)
        .deps(&[count.signal_id(), step.signal_id()])
        .on_state(move |state, container| {
            // Read current values inside on_state
            let current_count = count.get();
            let current_step = step.get();

            let bg = match state {
                ButtonState::Idle => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Hovered => Color::rgba(0.18, 0.18, 0.25, 1.0),
                _ => Color::rgba(0.15, 0.15, 0.2, 1.0),
            };

            // Update container with dynamic content
            container.merge(
                div()
                    .bg(bg)
                    .child(text(&format!("Count: {}", current_count)).color(Color::WHITE))
                    .child(text(&format!("Step: {}", current_step)).color(Color::WHITE))
            );
        })
        .on_click(move |_| {
            let current_step = step.get();
            count.update(|v| v + current_step);
        })
        .child(increment_button(ctx))
}

fn increment_button(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                ButtonState::Pressed => Color::rgba(0.2, 0.4, 0.8, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };
            div.set_bg(bg);
        })
        .child(text("Increment").color(Color::WHITE))
}
}

Key point: When UI content depends on state values that can change, use stateful(handle) with .deps() to declare the dependency. The on_state callback re-runs whenever those signals change, and you update the display via container.merge() or div.set_*() methods.

Common State Patterns

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct TodoList {
    items: Vec<String>,      // List of items
    filter: Filter,          // Current filter mode
    selected_index: Option<usize>,  // Currently selected item
}

#[derive(BlincComponent)]
struct FormData {
    username: String,
    email: String,
    is_valid: bool,
}

#[derive(BlincComponent)]
struct Settings {
    theme: Theme,
    notifications_enabled: bool,
    volume: f32,
}
}

Animation Fields

Fields with #[animation] generate spring animation hooks:

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct PullToRefresh {
    #[animation]
    content_offset: f32,    // Generates: use_content_offset(ctx, initial, config)

    #[animation]
    icon_scale: f32,        // Generates: use_icon_scale(ctx, initial, config)

    #[animation]
    icon_opacity: f32,      // Generates: use_icon_opacity(ctx, initial, config)
}
}

Using Animation Fields

#![allow(unused)]
fn main() {
fn pull_to_refresh_demo(ctx: &WindowedContext) -> impl ElementBuilder {
    // Each field gets its own type-safe hook
    let content_offset = PullToRefresh::use_content_offset(ctx, 0.0, SpringConfig::wobbly());
    let icon_scale = PullToRefresh::use_icon_scale(ctx, 0.5, SpringConfig::snappy());
    let icon_opacity = PullToRefresh::use_icon_opacity(ctx, 0.0, SpringConfig::snappy());

    // Use with motion() for animated rendering
    motion()
        .translate_y(content_offset.lock().unwrap().get())
        .child(/* content */)
}
}

Combining State and Animation

A component can have both state and animation fields:

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct ExpandableCard {
    // State fields
    is_expanded: bool,
    content: String,

    // Animation fields
    #[animation]
    height: f32,
    #[animation]
    arrow_rotation: f32,
}

fn expandable_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let is_expanded = ExpandableCard::use_is_expanded(ctx, false);
    let height = ExpandableCard::use_height(ctx, 60.0, SpringConfig::snappy());
    let arrow_rotation = ExpandableCard::use_arrow_rotation(ctx, 0.0, SpringConfig::snappy());

    let expanded = is_expanded.get();

    motion()
        .h(height.lock().unwrap().get())
        .on_click(move |_| {
            is_expanded.update(|v| !v);
            let target_height = if !expanded { 200.0 } else { 60.0 };
            let target_rotation = if !expanded { 180.0 } else { 0.0 };
            height.lock().unwrap().set_target(target_height);
            arrow_rotation.lock().unwrap().set_target(target_rotation);
        })
        .child(/* card content */)
}
}

Multiple Values per Component

Use _with suffix methods for multiple values of the same type:

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct DraggableBox;

fn draggable(ctx: &WindowedContext) -> impl ElementBuilder {
    // Multiple animated values with suffixes
    let x = DraggableBox::use_animated_value_with(ctx, "x", 100.0, SpringConfig::wobbly());
    let y = DraggableBox::use_animated_value_with(ctx, "y", 100.0, SpringConfig::wobbly());

    // ...
}
}

Timelines with BlincComponent

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct SpinningLoader;

fn loader(ctx: &WindowedContext) -> impl ElementBuilder {
    let timeline = SpinningLoader::use_animated_timeline(ctx);

    let entry_id = timeline.lock().unwrap().configure(|t| {
        let id = t.add(0, 1000, 0.0, 360.0);
        t.set_loop(-1);
        t.start();
        id
    });

    // ...
}
}

How It Works

The macro generates a unique key from module_path!() and the struct name:

#![allow(unused)]
fn main() {
impl MyCard {
    pub const COMPONENT_KEY: &'static str = concat!(module_path!(), "::", stringify!(MyCard));
    // e.g., "my_app::components::MyCard"
}
}

This ensures:

  • Uniqueness - Keys are unique across your entire codebase
  • Stability - Keys don’t change unless you move/rename the struct
  • No collisions - Different modules can have same-named components

Generated Methods

For Unit Structs

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct MyComponent;

// Generates:
impl MyComponent {
    pub const COMPONENT_KEY: &'static str;

    pub fn use_animated_value(
        ctx: &WindowedContext,
        initial: f32,
        config: SpringConfig,
    ) -> SharedAnimatedValue;

    pub fn use_animated_value_with(
        ctx: &WindowedContext,
        suffix: &str,
        initial: f32,
        config: SpringConfig,
    ) -> SharedAnimatedValue;

    pub fn use_animated_timeline(
        ctx: &WindowedContext,
    ) -> SharedAnimatedTimeline;

    pub fn use_animated_timeline_with(
        ctx: &WindowedContext,
        suffix: &str,
    ) -> SharedAnimatedTimeline;
}
}

For Structs with Fields

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct MyComponent {
    #[animation]
    scale: f32,
    count: i32,
}

// Additionally generates:
impl MyComponent {
    pub fn use_scale(
        ctx: &WindowedContext,
        initial: f32,
        config: SpringConfig,
    ) -> SharedAnimatedValue;

    pub fn use_count(
        ctx: &WindowedContext,
        initial: i32,
    ) -> State<i32>;
}
}

Best Practices

  1. Group related state and animations - A component should represent one logical UI element with its related state and animations.

  2. Use fields for named values - Prefer #[animation] scale: f32 over use_animated_value_with(ctx, "scale", ...).

  3. Combine state and animations - Use state fields for data, animation fields for visual transitions.

  4. Document fields - Add doc comments to fields for generated method documentation.

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct ExpandableSection {
    /// Whether the section is currently expanded
    is_expanded: bool,

    /// Animated height for smooth expand/collapse
    #[animation]
    height: f32,
}
}
  1. Use motion() with animated values - Wrap content using animated values in motion() for proper redraws.

Building Reusable Components

This guide covers patterns for creating composable, reusable UI components in Blinc.

Component Patterns

Simple Function Components

The simplest pattern - a function returning an element:

#![allow(unused)]
fn main() {
fn card(title: &str) -> Div {
    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .child(
            text(title)
                .size(18.0)
                .weight(FontWeight::SemiBold)
                .color(Color::WHITE)
        )
}

// Usage
div().child(card("My Card"))
}

Components with Children

Accept generic children with impl ElementBuilder:

#![allow(unused)]
fn main() {
fn card_with_content<E: ElementBuilder>(title: &str, content: E) -> Div {
    div()
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .flex_col()
        .gap(12.0)
        .child(
            text(title)
                .size(18.0)
                .weight(FontWeight::SemiBold)
                .color(Color::WHITE)
        )
        .child(content)
}

// Usage
card_with_content("Settings",
    div()
        .flex_col()
        .gap(8.0)
        .child(text("Option 1"))
        .child(text("Option 2"))
)
}

Components with Context

For components needing state or animations:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn counter_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let count = ctx.use_state_keyed("counter_card_count", || 0i32);
    let card_handle = ctx.use_state(ButtonState::Idle);

    stateful(card_handle)
        .p(16.0)
        .rounded(12.0)
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .flex_col()
        .gap(12.0)
        .deps(&[count.signal_id()])
        .on_state(move |_state, container| {
            let current = count.get();
            container.merge(
                div()
                    .child(text(&format!("Count: {}", current)).color(Color::WHITE))
            );
        })
        .child(increment_btn(ctx, count))
}

fn increment_btn(ctx: &WindowedContext, count: State<i32>) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
            };
            div.set_bg(bg);
        })
        .on_click(move |_| {
            count.update(|v| v + 1);
        })
        .child(text("+").color(Color::WHITE))
}
}

Animated Components

Use motion() for components with spring animations:

#![allow(unused)]
fn main() {
use blinc_layout::motion::motion;

#[derive(BlincComponent)]
struct AnimatedCard {
    #[animation]
    scale: f32,
    #[animation]
    opacity: f32,
}

fn animated_card(ctx: &WindowedContext, title: &str) -> impl ElementBuilder {
    let scale = AnimatedCard::use_scale(ctx, 1.0, SpringConfig::snappy());
    let opacity = AnimatedCard::use_opacity(ctx, 1.0, SpringConfig::gentle());

    let hover_scale = Arc::clone(&scale);
    let leave_scale = Arc::clone(&scale);

    // motion() is a container - apply transforms to it, style the child
    motion()
        .scale(scale.lock().unwrap().get())
        .opacity(opacity.lock().unwrap().get())
        .on_hover_enter(move |_| {
            hover_scale.lock().unwrap().set_target(1.05);
        })
        .on_hover_leave(move |_| {
            leave_scale.lock().unwrap().set_target(1.0);
        })
        .child(
            div()
                .p(16.0)
                .rounded(12.0)
                .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
                .child(text(title).color(Color::WHITE))
        )
}
}

Note: For hover-only visual effects without animations, prefer Stateful instead - it’s more efficient as it doesn’t require continuous redraws.


Stateful Components

Use stateful(handle) for components with visual states:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn interactive_card(ctx: &WindowedContext, title: &str) -> impl ElementBuilder {
    // Use use_state_for with title as key for reusable component
    let handle = ctx.use_state_for(title, ButtonState::Idle);

    stateful(handle)
        .p(16.0)
        .rounded(12.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Hovered => Color::rgba(0.18, 0.18, 0.25, 1.0),
                ButtonState::Pressed => Color::rgba(0.12, 0.12, 0.16, 1.0),
                _ => Color::rgba(0.15, 0.15, 0.2, 1.0),
            };
            div.set_bg(bg);
        })
        .child(text(title).color(Color::WHITE))
}
}

Builder Pattern

For highly configurable components:

#![allow(unused)]
fn main() {
pub struct CardBuilder {
    title: String,
    subtitle: Option<String>,
    icon: Option<String>,
    bg_color: Color,
    on_click: Option<Box<dyn Fn()>>,
}

impl CardBuilder {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            subtitle: None,
            icon: None,
            bg_color: Color::rgba(0.15, 0.15, 0.2, 1.0),
            on_click: None,
        }
    }

    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
        self.subtitle = Some(text.into());
        self
    }

    pub fn icon(mut self, path: impl Into<String>) -> Self {
        self.icon = Some(path.into());
        self
    }

    pub fn bg(mut self, color: Color) -> Self {
        self.bg_color = color;
        self
    }

    pub fn build(self) -> Div {
        let mut card = div()
            .p(16.0)
            .rounded(12.0)
            .bg(self.bg_color)
            .flex_col()
            .gap(8.0);

        if let Some(icon_path) = self.icon {
            card = card.child(
                svg(&icon_path).w(24.0).h(24.0).tint(Color::WHITE)
            );
        }

        card = card.child(
            text(&self.title)
                .size(18.0)
                .weight(FontWeight::SemiBold)
                .color(Color::WHITE)
        );

        if let Some(sub) = self.subtitle {
            card = card.child(
                text(&sub)
                    .size(14.0)
                    .color(Color::rgba(0.6, 0.6, 0.7, 1.0))
            );
        }

        card
    }
}

// Usage
CardBuilder::new("Settings")
    .subtitle("Manage your preferences")
    .icon("icons/settings.svg")
    .build()
}

Component Libraries

Organize related components in modules:

#![allow(unused)]
fn main() {
// src/components/cards.rs
pub mod cards {
    use blinc_app::prelude::*;

    pub fn simple_card(title: &str) -> Div {
        // ...
    }

    pub fn image_card(title: &str, image_url: &str) -> Div {
        // ...
    }

    pub fn action_card<F: Fn() + 'static>(title: &str, on_action: F) -> Div {
        // ...
    }
}

// src/components/mod.rs
pub mod cards;
pub mod buttons;
pub mod inputs;

// Usage
use crate::components::cards::*;
}

Prop Structs

For components with many parameters:

#![allow(unused)]
fn main() {
pub struct NotificationProps {
    pub title: String,
    pub message: String,
    pub variant: NotificationVariant,
    pub dismissible: bool,
    pub on_dismiss: Option<Box<dyn Fn()>>,
}

pub enum NotificationVariant {
    Info,
    Success,
    Warning,
    Error,
}

pub fn notification(props: NotificationProps) -> Div {
    let (bg, icon) = match props.variant {
        NotificationVariant::Info => (Color::rgba(0.2, 0.4, 0.8, 1.0), "info.svg"),
        NotificationVariant::Success => (Color::rgba(0.2, 0.7, 0.4, 1.0), "check.svg"),
        NotificationVariant::Warning => (Color::rgba(0.8, 0.6, 0.2, 1.0), "warning.svg"),
        NotificationVariant::Error => (Color::rgba(0.8, 0.3, 0.3, 1.0), "error.svg"),
    };

    div()
        .p(16.0)
        .rounded(8.0)
        .bg(bg)
        .flex_row()
        .gap(12.0)
        .items_center()
        .child(svg(icon).w(20.0).h(20.0).tint(Color::WHITE))
        .child(
            div()
                .flex_1()
                .flex_col()
                .gap(4.0)
                .child(text(&props.title).weight(FontWeight::SemiBold).color(Color::WHITE))
                .child(text(&props.message).size(14.0).color(Color::rgba(1.0, 1.0, 1.0, 0.8)))
        )
}
}

Best Practices

  1. Keep components focused - One component, one responsibility.

  2. Use impl ElementBuilder - For maximum flexibility in return types.

  3. Document public components - Add doc comments explaining usage.

  4. Consistent naming - Use descriptive names that indicate the component’s purpose.

  5. Default sensible styles - Provide good defaults, allow overrides.

  6. Separate stateless and stateful - Pure components are easier to test and reuse.

  7. Use BlincComponent for state and animations - Type-safe hooks for both State<T> and SharedAnimatedValue prevent key collisions.

  8. Use Stateful for visual states - Hover, press, focus effects should use Stateful rather than signals.

  9. Use motion() for animated values - Wrap animated content in motion() for proper redraws.

Component Library Overview

blinc_cn is a comprehensive component library for Blinc UI, inspired by shadcn/ui. It provides 40+ production-ready, themeable components built on top of blinc_layout.

Installation

Add blinc_cn to your Cargo.toml:

[dependencies]
blinc_cn = { path = "path/to/blinc_cn" }

Quick Start

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

fn build_ui() -> impl ElementBuilder {
    div()
        .flex_col()
        .gap(16.0)
        .p(24.0)
        .child(
            card()
                .child(card_header()
                    .child(card_title("Welcome"))
                    .child(card_description("Get started with blinc_cn")))
                .child(card_content()
                    .child(text("Beautiful, accessible components.")))
                .child(card_footer()
                    .child(button("Get Started")))
        )
}
}

Design Principles

Composable

Components are built from smaller primitives that can be combined:

#![allow(unused)]
fn main() {
// Compose dialog from parts
dialog()
    .child(dialog_trigger().child(button("Open")))
    .child(dialog_content()
        .child(dialog_header().child(dialog_title("Title")))
        .child(/* content */)
        .child(dialog_footer().child(button("Close"))))
}

Themeable

All components use theme tokens and automatically support dark mode:

#![allow(unused)]
fn main() {
// Components adapt to theme automatically
button("Click me") // Uses theme.colors.primary

// Override theme
ThemeState::set_color_scheme(ColorScheme::Dark);
}

Accessible

Components include keyboard navigation and proper semantics:

  • Focus management
  • Keyboard shortcuts
  • Screen reader support (planned)

Component Categories

CategoryComponents
ButtonsButton
CardsCard, CardHeader, CardContent, CardFooter
DialogsDialog, AlertDialog, Sheet, Drawer
FormsInput, Textarea, Checkbox, Switch, Radio, Select, Slider
NavigationTabs, DropdownMenu, ContextMenu, Breadcrumb, Sidebar
FeedbackAlert, Badge, Progress, Spinner, Skeleton, Toast
LayoutAvatar, Separator, AspectRatio, ScrollArea, Accordion
DataTooltip, HoverCard, Popover, Chart

Prelude

Import common components with the prelude:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

// Includes:
// - All component builders (button, card, dialog, etc.)
// - Variant enums (ButtonVariant, AlertVariant, etc.)
// - Size enums (ButtonSize, AvatarSize, etc.)
// - Common types and traits
}

Next Steps

Button

Buttons trigger actions or events.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

button("Click me")
    .on_click(|| println!("Clicked!"))
}

Variants

Buttons come in several visual variants:

#![allow(unused)]
fn main() {
// Primary (default) - Main actions
button("Save").variant(ButtonVariant::Primary)

// Secondary - Alternative actions
button("Cancel").variant(ButtonVariant::Secondary)

// Destructive - Dangerous actions
button("Delete").variant(ButtonVariant::Destructive)

// Outline - Bordered style
button("Edit").variant(ButtonVariant::Outline)

// Ghost - Minimal style
button("More").variant(ButtonVariant::Ghost)

// Link - Looks like a link
button("Learn more").variant(ButtonVariant::Link)
}

Sizes

#![allow(unused)]
fn main() {
// Small
button("Small").size(ButtonSize::Sm)

// Default
button("Default").size(ButtonSize::Default)

// Large
button("Large").size(ButtonSize::Lg)

// Icon only (square)
button("").size(ButtonSize::Icon).icon(icons::SETTINGS)
}

With Icons

#![allow(unused)]
fn main() {
use blinc_icons::icons;

// Icon before text
button("Settings")
    .icon(icons::SETTINGS)

// Icon after text
button("Next")
    .icon_right(icons::ARROW_RIGHT)

// Icon only
button("")
    .size(ButtonSize::Icon)
    .icon(icons::PLUS)
}

States

#![allow(unused)]
fn main() {
// Disabled
button("Disabled")
    .disabled(true)

// Loading
button("Saving...")
    .loading(true)

// Full width
button("Submit")
    .full_width(true)
}

Event Handling

#![allow(unused)]
fn main() {
button("Submit")
    .on_click(|| {
        // Handle click
        submit_form();
    })
    .on_hover(|hovering| {
        // Handle hover state
        if hovering {
            show_tooltip();
        }
    })
}

Button Groups

#![allow(unused)]
fn main() {
div()
    .flex_row()
    .gap(8.0)
    .child(button("Save").variant(ButtonVariant::Primary))
    .child(button("Cancel").variant(ButtonVariant::Outline))
}

Examples

Form Submit Button

#![allow(unused)]
fn main() {
button("Create Account")
    .variant(ButtonVariant::Primary)
    .size(ButtonSize::Lg)
    .full_width(true)
    .on_click(|| handle_submit())
}

Icon Button

#![allow(unused)]
fn main() {
button("")
    .size(ButtonSize::Icon)
    .variant(ButtonVariant::Ghost)
    .icon(icons::X)
    .on_click(|| close_dialog())
}

Loading Button

#![allow(unused)]
fn main() {
let is_loading = use_state(false);

button(if is_loading { "Saving..." } else { "Save" })
    .loading(is_loading)
    .disabled(is_loading)
    .on_click(|| {
        set_loading(true);
        save_data().then(|| set_loading(false));
    })
}

API Reference

Props

PropTypeDefaultDescription
variantButtonVariantPrimaryVisual style
sizeButtonSizeDefaultButton size
disabledboolfalseDisable interaction
loadingboolfalseShow loading state
full_widthboolfalseExpand to full width
icon&strNoneIcon before text
icon_right&strNoneIcon after text

Events

EventTypeDescription
on_clickFn()Called when clicked
on_hoverFn(bool)Called on hover change

Card

Cards group related content and actions.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

card()
    .child(card_header()
        .child(card_title("Card Title"))
        .child(card_description("Card description text")))
    .child(card_content()
        .child(text("Card content goes here.")))
    .child(card_footer()
        .child(button("Action")))
}

Card Parts

card()

The container that wraps all card content.

#![allow(unused)]
fn main() {
card()
    .w(400.0)  // Custom width
    .child(/* card parts */)
}

card_header()

Contains the title and description.

#![allow(unused)]
fn main() {
card_header()
    .child(card_title("Title"))
    .child(card_description("Description"))
}

card_title()

The main heading of the card.

#![allow(unused)]
fn main() {
card_title("Account Settings")
}

card_description()

Secondary text below the title.

#![allow(unused)]
fn main() {
card_description("Manage your account preferences")
}

card_content()

The main content area.

#![allow(unused)]
fn main() {
card_content()
    .child(/* any content */)
}

Actions and secondary information at the bottom.

#![allow(unused)]
fn main() {
card_footer()
    .child(button("Cancel").variant(ButtonVariant::Outline))
    .child(button("Save"))
}

Examples

Simple Card

#![allow(unused)]
fn main() {
card()
    .child(card_header()
        .child(card_title("Notifications"))
        .child(card_description("Configure notification settings")))
    .child(card_content()
        .child(
            div()
                .flex_col()
                .gap(12.0)
                .child(checkbox().checked(true).child(label("Email notifications")))
                .child(checkbox().child(label("Push notifications")))
        ))
}

Card with Image

#![allow(unused)]
fn main() {
card()
    .overflow_clip()
    .child(
        img("cover.jpg")
            .w_full()
            .h(200.0)
            .cover()
    )
    .child(card_header()
        .child(card_title("Beautiful Sunset"))
        .child(card_description("Photo by @photographer")))
    .child(card_footer()
        .child(button("View").variant(ButtonVariant::Outline))
        .child(button("Download")))
}

Card with Form

#![allow(unused)]
fn main() {
card()
    .w(350.0)
    .child(card_header()
        .child(card_title("Login"))
        .child(card_description("Enter your credentials")))
    .child(card_content()
        .child(
            div()
                .flex_col()
                .gap(16.0)
                .child(
                    div()
                        .flex_col()
                        .gap(4.0)
                        .child(label("Email"))
                        .child(input().placeholder("name@example.com"))
                )
                .child(
                    div()
                        .flex_col()
                        .gap(4.0)
                        .child(label("Password"))
                        .child(input().input_type("password"))
                )
        ))
    .child(card_footer()
        .child(button("Sign in").full_width(true)))
}

Card Grid

#![allow(unused)]
fn main() {
div()
    .grid()
    .grid_cols(3)
    .gap(16.0)
    .child(
        card()
            .child(card_header().child(card_title("Plan A")))
            .child(card_content().child(text("$9/month")))
            .child(card_footer().child(button("Select")))
    )
    .child(
        card()
            .child(card_header().child(card_title("Plan B")))
            .child(card_content().child(text("$19/month")))
            .child(card_footer().child(button("Select")))
    )
    .child(
        card()
            .child(card_header().child(card_title("Plan C")))
            .child(card_content().child(text("$29/month")))
            .child(card_footer().child(button("Select")))
    )
}

Interactive Card

#![allow(unused)]
fn main() {
card()
    .on_click(|| navigate_to("/details"))
    .cursor("pointer")
    .child(card_header()
        .child(card_title("Click Me"))
        .child(card_description("This entire card is clickable")))
    .child(card_content()
        .child(text("Card content...")))
}

Styling

Cards automatically use theme tokens:

  • Background: theme.colors.card
  • Border: theme.colors.border
  • Radius: theme.radius.lg
  • Shadow: theme.shadows.sm

Override with custom styles:

#![allow(unused)]
fn main() {
card()
    .bg(Color::rgb(0.1, 0.1, 0.1))
    .border(2.0, Color::BLUE)
    .rounded(16.0)
    .shadow(Shadow::lg())
}

API Reference

card()

PropTypeDescription
Standard div props-All div styling props

card_header()

PropTypeDescription
Standard div props-All div styling props

card_title()

PropTypeDescription
Text content&strTitle text

card_description()

PropTypeDescription
Text content&strDescription text

Dialog

Dialogs display content in a modal overlay that requires user interaction.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

let is_open = use_state(false);

dialog()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(dialog_trigger()
        .child(button("Open Dialog")))
    .child(dialog_content()
        .child(dialog_header()
            .child(dialog_title("Dialog Title"))
            .child(dialog_description("Dialog description")))
        .child(text("Dialog content goes here."))
        .child(dialog_footer()
            .child(button("Close").on_click(|| set_is_open(false)))))
}

Dialog Parts

dialog()

The root component that manages open state.

#![allow(unused)]
fn main() {
dialog()
    .open(is_open)
    .on_open_change(|open| set_open(open))
}

dialog_trigger()

The element that opens the dialog when clicked.

#![allow(unused)]
fn main() {
dialog_trigger()
    .child(button("Open"))
}

dialog_content()

The modal content container with backdrop.

#![allow(unused)]
fn main() {
dialog_content()
    .child(/* dialog parts */)
}

dialog_header()

Contains title and description.

#![allow(unused)]
fn main() {
dialog_header()
    .child(dialog_title("Title"))
    .child(dialog_description("Description"))
}

Contains action buttons.

#![allow(unused)]
fn main() {
dialog_footer()
    .child(button("Cancel").variant(ButtonVariant::Outline))
    .child(button("Confirm"))
}

dialog_close()

A button that closes the dialog.

#![allow(unused)]
fn main() {
dialog_close()
    .child(button("Close"))
}

Alert Dialog

For destructive or important confirmations:

#![allow(unused)]
fn main() {
let is_open = use_state(false);

alert_dialog()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(alert_dialog_trigger()
        .child(button("Delete").variant(ButtonVariant::Destructive)))
    .child(alert_dialog_content()
        .child(alert_dialog_header()
            .child(alert_dialog_title("Are you sure?"))
            .child(alert_dialog_description(
                "This action cannot be undone."
            )))
        .child(alert_dialog_footer()
            .child(alert_dialog_cancel().child(button("Cancel")))
            .child(alert_dialog_action()
                .child(button("Delete").variant(ButtonVariant::Destructive)))))
}

Sheet

A panel that slides in from the edge:

#![allow(unused)]
fn main() {
let is_open = use_state(false);

sheet()
    .open(is_open)
    .side(SheetSide::Right)  // Left, Right, Top, Bottom
    .on_open_change(|open| set_is_open(open))
    .child(sheet_trigger()
        .child(button("Open Sheet")))
    .child(sheet_content()
        .child(sheet_header()
            .child(sheet_title("Settings")))
        .child(/* content */)
        .child(sheet_footer()
            .child(button("Save changes"))))
}

Drawer

A mobile-friendly bottom sheet:

#![allow(unused)]
fn main() {
let is_open = use_state(false);

drawer()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(drawer_trigger()
        .child(button("Open Drawer")))
    .child(drawer_content()
        .child(drawer_header()
            .child(drawer_title("Menu")))
        .child(/* content */))
}

Examples

Form Dialog

#![allow(unused)]
fn main() {
let is_open = use_state(false);
let name = use_state(String::new());
let email = use_state(String::new());

dialog()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(dialog_trigger()
        .child(button("Edit Profile")))
    .child(dialog_content()
        .child(dialog_header()
            .child(dialog_title("Edit Profile"))
            .child(dialog_description("Update your profile information")))
        .child(
            div()
                .flex_col()
                .gap(16.0)
                .child(
                    div().flex_col().gap(4.0)
                        .child(label("Name"))
                        .child(input()
                            .value(&name)
                            .on_change(|v| set_name(v)))
                )
                .child(
                    div().flex_col().gap(4.0)
                        .child(label("Email"))
                        .child(input()
                            .value(&email)
                            .on_change(|v| set_email(v)))
                )
        )
        .child(dialog_footer()
            .child(dialog_close().child(
                button("Cancel").variant(ButtonVariant::Outline)
            ))
            .child(button("Save").on_click(|| {
                save_profile();
                set_is_open(false);
            }))))
}

Confirmation Dialog

#![allow(unused)]
fn main() {
let is_open = use_state(false);

alert_dialog()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(alert_dialog_trigger()
        .child(button("Delete Account").variant(ButtonVariant::Destructive)))
    .child(alert_dialog_content()
        .child(alert_dialog_header()
            .child(alert_dialog_title("Delete Account"))
            .child(alert_dialog_description(
                "Are you sure you want to delete your account? \
                 All your data will be permanently removed."
            )))
        .child(alert_dialog_footer()
            .child(alert_dialog_cancel().child(
                button("Cancel").variant(ButtonVariant::Outline)
            ))
            .child(alert_dialog_action().child(
                button("Delete")
                    .variant(ButtonVariant::Destructive)
                    .on_click(|| delete_account())
            ))))
}

API Reference

dialog()

PropTypeDefaultDescription
openboolfalseWhether dialog is open
on_open_changeFn(bool)-Called when open state changes

sheet()

PropTypeDefaultDescription
openboolfalseWhether sheet is open
sideSheetSideRightWhich side to slide from
on_open_changeFn(bool)-Called when open state changes

SheetSide

#![allow(unused)]
fn main() {
enum SheetSide {
    Left,
    Right,
    Top,
    Bottom,
}
}

Form Components

Components for building forms: inputs, checkboxes, selects, and more.

Input

Text input field:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

input()
    .placeholder("Enter your name...")
    .value(name)
    .on_change(|value| set_name(value))
}

Input Types

#![allow(unused)]
fn main() {
// Text (default)
input().placeholder("Name")

// Email
input().input_type("email").placeholder("Email")

// Password
input().input_type("password").placeholder("Password")

// Number
input().input_type("number").placeholder("Age")

// Search
input().input_type("search").placeholder("Search...")
}

Input States

#![allow(unused)]
fn main() {
// Disabled
input().disabled(true)

// Read-only
input().readonly(true)

// With error
input().error(true)
}

Textarea

Multi-line text input:

#![allow(unused)]
fn main() {
textarea()
    .placeholder("Enter description...")
    .rows(4)
    .value(description)
    .on_change(|value| set_description(value))
}

Checkbox

#![allow(unused)]
fn main() {
checkbox()
    .checked(is_checked)
    .on_change(|checked| set_checked(checked))
    .child(label("Accept terms and conditions"))
}

Indeterminate State

#![allow(unused)]
fn main() {
checkbox()
    .checked(some_checked)
    .indeterminate(some_checked && !all_checked)
    .on_change(|checked| toggle_all(checked))
    .child(label("Select all"))
}

Switch

Toggle switch:

#![allow(unused)]
fn main() {
switch_()
    .checked(is_enabled)
    .on_change(|enabled| set_enabled(enabled))
}

With Label

#![allow(unused)]
fn main() {
div()
    .flex_row()
    .items_center()
    .gap(8.0)
    .child(switch_().checked(dark_mode).on_change(|v| set_dark_mode(v)))
    .child(label("Dark mode"))
}

Radio Group

#![allow(unused)]
fn main() {
radio_group()
    .value(selected)
    .on_change(|value| set_selected(value))
    .child(
        div().flex_col().gap(8.0)
            .child(radio_item("small").child(label("Small")))
            .child(radio_item("medium").child(label("Medium")))
            .child(radio_item("large").child(label("Large")))
    )
}

Select

Dropdown selection:

#![allow(unused)]
fn main() {
select()
    .value(selected)
    .on_change(|value| set_selected(value))
    .child(select_trigger()
        .child(select_value().placeholder("Select option...")))
    .child(select_content()
        .child(select_item("opt1").child(text("Option 1")))
        .child(select_item("opt2").child(text("Option 2")))
        .child(select_item("opt3").child(text("Option 3"))))
}

Grouped Options

#![allow(unused)]
fn main() {
select()
    .child(select_trigger().child(select_value()))
    .child(select_content()
        .child(select_group()
            .child(select_label("Fruits"))
            .child(select_item("apple").child(text("Apple")))
            .child(select_item("banana").child(text("Banana"))))
        .child(select_separator())
        .child(select_group()
            .child(select_label("Vegetables"))
            .child(select_item("carrot").child(text("Carrot")))
            .child(select_item("broccoli").child(text("Broccoli")))))
}

Combobox

Searchable select with autocomplete:

#![allow(unused)]
fn main() {
combobox()
    .value(selected)
    .on_change(|value| set_selected(value))
    .child(combobox_trigger()
        .child(combobox_input().placeholder("Search...")))
    .child(combobox_content()
        .child(combobox_empty().child(text("No results found")))
        .child(combobox_item("react").child(text("React")))
        .child(combobox_item("vue").child(text("Vue")))
        .child(combobox_item("svelte").child(text("Svelte"))))
}

Slider

Range slider:

#![allow(unused)]
fn main() {
slider()
    .value(volume)
    .min(0.0)
    .max(100.0)
    .step(1.0)
    .on_change(|value| set_volume(value))
}

Range Slider

#![allow(unused)]
fn main() {
slider()
    .value_range(min_price, max_price)
    .min(0.0)
    .max(1000.0)
    .on_change_range(|min, max| {
        set_min_price(min);
        set_max_price(max);
    })
}

Label

#![allow(unused)]
fn main() {
// Associated with input via for
label("Email").for_id("email-input")

// Direct child of input
checkbox()
    .child(label("Remember me"))
}

Form Layout Example

#![allow(unused)]
fn main() {
div()
    .flex_col()
    .gap(24.0)
    .max_w(400.0)
    // Name field
    .child(
        div().flex_col().gap(4.0)
            .child(label("Name"))
            .child(input()
                .placeholder("John Doe")
                .value(&name)
                .on_change(|v| set_name(v)))
    )
    // Email field
    .child(
        div().flex_col().gap(4.0)
            .child(label("Email"))
            .child(input()
                .input_type("email")
                .placeholder("john@example.com")
                .value(&email)
                .on_change(|v| set_email(v)))
    )
    // Country select
    .child(
        div().flex_col().gap(4.0)
            .child(label("Country"))
            .child(select()
                .value(&country)
                .on_change(|v| set_country(v))
                .child(select_trigger().child(select_value()))
                .child(select_content()
                    .child(select_item("us").child(text("United States")))
                    .child(select_item("uk").child(text("United Kingdom")))
                    .child(select_item("ca").child(text("Canada")))))
    )
    // Terms checkbox
    .child(
        checkbox()
            .checked(accepted_terms)
            .on_change(|v| set_accepted_terms(v))
            .child(label("I accept the terms and conditions"))
    )
    // Submit button
    .child(
        button("Submit")
            .full_width(true)
            .disabled(!accepted_terms)
            .on_click(|| submit_form())
    )
}

Validation

#![allow(unused)]
fn main() {
let email = use_state(String::new());
let email_error = use_derived(|| {
    if email.is_empty() {
        None
    } else if !email.contains('@') {
        Some("Invalid email address")
    } else {
        None
    }
});

div().flex_col().gap(4.0)
    .child(label("Email"))
    .child(input()
        .value(&email)
        .error(email_error.is_some())
        .on_change(|v| set_email(v)))
    .child(
        email_error.map(|err|
            text(err).size(12.0).color(Color::RED)
        )
    )
}

Navigation Components

Components for navigation: tabs, menus, breadcrumbs, and sidebars.

Tabs

Organize content into tabbed sections:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

tabs()
    .value(active_tab)
    .on_change(|tab| set_active_tab(tab))
    .child(tabs_list()
        .child(tabs_trigger("account").child(text("Account")))
        .child(tabs_trigger("password").child(text("Password")))
        .child(tabs_trigger("settings").child(text("Settings"))))
    .child(tabs_content("account")
        .child(text("Account settings...")))
    .child(tabs_content("password")
        .child(text("Password settings...")))
    .child(tabs_content("settings")
        .child(text("Other settings...")))
}
#![allow(unused)]
fn main() {
dropdown_menu()
    .child(dropdown_menu_trigger()
        .child(button("Options").icon_right(icons::CHEVRON_DOWN)))
    .child(dropdown_menu_content()
        .child(dropdown_menu_label("Actions"))
        .child(dropdown_menu_item("edit")
            .child(icon(icons::EDIT))
            .child(text("Edit"))
            .on_click(|| edit_item()))
        .child(dropdown_menu_item("duplicate")
            .child(icon(icons::COPY))
            .child(text("Duplicate")))
        .child(dropdown_menu_separator())
        .child(dropdown_menu_item("delete")
            .child(icon(icons::TRASH))
            .child(text("Delete"))
            .variant(MenuItemVariant::Destructive)))
}

With Keyboard Shortcuts

#![allow(unused)]
fn main() {
dropdown_menu_item("save")
    .child(icon(icons::SAVE))
    .child(text("Save"))
    .child(dropdown_menu_shortcut("⌘S"))
}
#![allow(unused)]
fn main() {
dropdown_menu_content()
    .child(dropdown_menu_item("new").child(text("New")))
    .child(dropdown_menu_sub()
        .child(dropdown_menu_sub_trigger()
            .child(text("Share")))
        .child(dropdown_menu_sub_content()
            .child(dropdown_menu_item("email").child(text("Email")))
            .child(dropdown_menu_item("link").child(text("Copy Link")))))
}

Context Menu

Right-click menu:

#![allow(unused)]
fn main() {
context_menu()
    .child(context_menu_trigger()
        .child(div().w(200.0).h(150.0).bg(Color::GRAY)
            .child(text("Right-click me"))))
    .child(context_menu_content()
        .child(context_menu_item("cut").child(text("Cut")))
        .child(context_menu_item("copy").child(text("Copy")))
        .child(context_menu_item("paste").child(text("Paste")))
        .child(context_menu_separator())
        .child(context_menu_item("delete").child(text("Delete"))))
}

Application menu bar:

#![allow(unused)]
fn main() {
menubar()
    .child(menubar_menu()
        .child(menubar_trigger().child(text("File")))
        .child(menubar_content()
            .child(menubar_item("new").child(text("New File")))
            .child(menubar_item("open").child(text("Open...")))
            .child(menubar_separator())
            .child(menubar_item("save").child(text("Save")))
            .child(menubar_item("save-as").child(text("Save As...")))))
    .child(menubar_menu()
        .child(menubar_trigger().child(text("Edit")))
        .child(menubar_content()
            .child(menubar_item("undo").child(text("Undo")))
            .child(menubar_item("redo").child(text("Redo")))))
}

Navigation path:

#![allow(unused)]
fn main() {
breadcrumb()
    .child(breadcrumb_list()
        .child(breadcrumb_item()
            .child(breadcrumb_link("Home").href("/")))
        .child(breadcrumb_separator())
        .child(breadcrumb_item()
            .child(breadcrumb_link("Products").href("/products")))
        .child(breadcrumb_separator())
        .child(breadcrumb_item()
            .child(breadcrumb_page("Details"))))  // Current page (not a link)
}

With Ellipsis

#![allow(unused)]
fn main() {
breadcrumb()
    .child(breadcrumb_list()
        .child(breadcrumb_item().child(breadcrumb_link("Home")))
        .child(breadcrumb_separator())
        .child(breadcrumb_ellipsis())  // Collapsed items
        .child(breadcrumb_separator())
        .child(breadcrumb_item().child(breadcrumb_link("Category")))
        .child(breadcrumb_separator())
        .child(breadcrumb_item().child(breadcrumb_page("Current"))))
}

Pagination

#![allow(unused)]
fn main() {
pagination()
    .total(100)
    .page_size(10)
    .current_page(current_page)
    .on_page_change(|page| set_current_page(page))
    .child(pagination_content()
        .child(pagination_previous())
        .child(pagination_items())
        .child(pagination_next()))
}

Application sidebar navigation:

#![allow(unused)]
fn main() {
sidebar()
    .child(sidebar_header()
        .child(
            div().flex_row().items_center().gap(8.0)
                .child(icon(icons::BOX).size(24.0))
                .child(text("My App").weight(FontWeight::Bold))
        ))
    .child(sidebar_content()
        .child(sidebar_group()
            .child(sidebar_group_label("Main"))
            .child(sidebar_menu()
                .child(sidebar_menu_item("dashboard")
                    .icon(icons::HOME)
                    .active(current_route == "dashboard")
                    .on_click(|| navigate("/dashboard"))
                    .child(text("Dashboard")))
                .child(sidebar_menu_item("projects")
                    .icon(icons::FOLDER)
                    .on_click(|| navigate("/projects"))
                    .child(text("Projects")))
                .child(sidebar_menu_item("tasks")
                    .icon(icons::CHECK_SQUARE)
                    .on_click(|| navigate("/tasks"))
                    .child(text("Tasks")))))
        .child(sidebar_group()
            .child(sidebar_group_label("Settings"))
            .child(sidebar_menu()
                .child(sidebar_menu_item("settings")
                    .icon(icons::SETTINGS)
                    .on_click(|| navigate("/settings"))
                    .child(text("Settings")))
                .child(sidebar_menu_item("help")
                    .icon(icons::HELP_CIRCLE)
                    .on_click(|| navigate("/help"))
                    .child(text("Help"))))))
    .child(sidebar_footer()
        .child(
            div().flex_row().items_center().gap(8.0)
                .child(avatar().src("user.jpg").size(AvatarSize::Sm))
                .child(text("John Doe"))
        ))
}

Collapsible Sidebar

#![allow(unused)]
fn main() {
let is_collapsed = use_state(false);

sidebar()
    .collapsed(is_collapsed)
    .child(sidebar_header()
        .child(sidebar_trigger()
            .on_click(|| set_is_collapsed(!is_collapsed))))
    .child(/* rest of sidebar */)
}

Horizontal navigation with dropdowns:

#![allow(unused)]
fn main() {
navigation_menu()
    .child(navigation_menu_list()
        .child(navigation_menu_item()
            .child(navigation_menu_trigger().child(text("Products")))
            .child(navigation_menu_content()
                .child(navigation_menu_link("analytics").child(text("Analytics")))
                .child(navigation_menu_link("reports").child(text("Reports")))))
        .child(navigation_menu_item()
            .child(navigation_menu_link("pricing").child(text("Pricing"))))
        .child(navigation_menu_item()
            .child(navigation_menu_link("about").child(text("About")))))
}

Feedback Components

Components for user feedback: alerts, badges, progress indicators, and toasts.

Alert

Display important messages:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

alert()
    .child(alert_title("Heads up!"))
    .child(alert_description("This is an important message."))
}

Alert Variants

#![allow(unused)]
fn main() {
// Default
alert()
    .child(alert_title("Note"))
    .child(alert_description("This is a note."))

// Destructive (error/warning)
alert()
    .variant(AlertVariant::Destructive)
    .child(alert_title("Error"))
    .child(alert_description("Something went wrong."))
}

With Icon

#![allow(unused)]
fn main() {
alert()
    .child(icon(icons::INFO).size(16.0))
    .child(alert_title("Information"))
    .child(alert_description("Here's some useful info."))
}

Badge

Small labels for status or counts:

#![allow(unused)]
fn main() {
badge("New")
badge("3").variant(BadgeVariant::Secondary)
badge("Error").variant(BadgeVariant::Destructive)
badge("Beta").variant(BadgeVariant::Outline)
}

Badge Variants

#![allow(unused)]
fn main() {
// Default - primary color
badge("Default")

// Secondary - muted color
badge("Secondary").variant(BadgeVariant::Secondary)

// Destructive - error/warning
badge("Destructive").variant(BadgeVariant::Destructive)

// Outline - bordered
badge("Outline").variant(BadgeVariant::Outline)
}

With Icon

#![allow(unused)]
fn main() {
badge("")
    .variant(BadgeVariant::Outline)
    .child(icon(icons::CHECK).size(12.0))
    .child(text("Verified"))
}

Progress

Progress bar:

#![allow(unused)]
fn main() {
progress()
    .value(75.0)  // 0-100
}

Indeterminate

#![allow(unused)]
fn main() {
progress()
    .indeterminate(true)
}

With Label

#![allow(unused)]
fn main() {
div()
    .flex_col()
    .gap(4.0)
    .child(
        div().flex_row().justify_between()
            .child(text("Uploading..."))
            .child(text(format!("{}%", progress_value)))
    )
    .child(progress().value(progress_value))
}

Spinner

Loading indicator:

#![allow(unused)]
fn main() {
spinner()
}

Spinner Sizes

#![allow(unused)]
fn main() {
spinner().size(SpinnerSize::Sm)   // Small
spinner().size(SpinnerSize::Md)   // Medium (default)
spinner().size(SpinnerSize::Lg)   // Large
}

In Button

#![allow(unused)]
fn main() {
button(if is_loading { "" } else { "Save" })
    .loading(is_loading)
    .disabled(is_loading)
}

Skeleton

Placeholder for loading content:

#![allow(unused)]
fn main() {
skeleton().w(200.0).h(20.0)
}

Card Skeleton

#![allow(unused)]
fn main() {
card()
    .child(card_header()
        .child(skeleton().w(150.0).h(24.0))  // Title placeholder
        .child(skeleton().w(200.0).h(16.0))) // Description placeholder
    .child(card_content()
        .child(skeleton().w_full().h(100.0))) // Content placeholder
}

List Skeleton

#![allow(unused)]
fn main() {
div()
    .flex_col()
    .gap(12.0)
    .child(
        div().flex_row().gap(12.0)
            .child(skeleton().w(48.0).h(48.0).rounded_full())  // Avatar
            .child(
                div().flex_col().gap(4.0)
                    .child(skeleton().w(150.0).h(16.0))  // Name
                    .child(skeleton().w(100.0).h(14.0))) // Subtitle
    )
    // Repeat for more items...
}

Toast

Temporary notifications:

#![allow(unused)]
fn main() {
// Show a toast
show_toast(
    toast()
        .title("Success")
        .description("Your changes have been saved.")
);

// With variant
show_toast(
    toast()
        .variant(ToastVariant::Destructive)
        .title("Error")
        .description("Failed to save changes.")
);
}

Toast Variants

#![allow(unused)]
fn main() {
// Default
toast().title("Notification")

// Success
toast()
    .variant(ToastVariant::Success)
    .title("Success")

// Destructive/Error
toast()
    .variant(ToastVariant::Destructive)
    .title("Error")
}

With Action

#![allow(unused)]
fn main() {
toast()
    .title("Event created")
    .description("Friday, February 10, 2024")
    .action(
        toast_action()
            .child(button("Undo").size(ButtonSize::Sm))
            .on_click(|| undo_action())
    )
}

Toast Position

#![allow(unused)]
fn main() {
// Configure toast container position
toaster()
    .position(ToasterPosition::TopRight)  // TopLeft, TopRight, BottomLeft, BottomRight
}

Examples

Loading State

#![allow(unused)]
fn main() {
let is_loading = use_state(true);

if is_loading {
    div()
        .flex_col()
        .items_center()
        .gap(16.0)
        .child(spinner().size(SpinnerSize::Lg))
        .child(text("Loading..."))
} else {
    // Actual content
}
}

Form Submission Feedback

#![allow(unused)]
fn main() {
let status = use_state(FormStatus::Idle);

div()
    .flex_col()
    .gap(16.0)
    .child(/* form fields */)
    .child(
        match status {
            FormStatus::Idle => button("Submit").on_click(|| submit()),
            FormStatus::Submitting => button("").loading(true).disabled(true),
            FormStatus::Success => alert()
                .child(alert_title("Success"))
                .child(alert_description("Form submitted successfully!")),
            FormStatus::Error(msg) => alert()
                .variant(AlertVariant::Destructive)
                .child(alert_title("Error"))
                .child(alert_description(msg)),
        }
    )
}

Notification Center

#![allow(unused)]
fn main() {
fn notify_success(message: &str) {
    show_toast(
        toast()
            .variant(ToastVariant::Success)
            .title("Success")
            .description(message)
            .duration(Duration::from_secs(5))
    );
}

fn notify_error(message: &str) {
    show_toast(
        toast()
            .variant(ToastVariant::Destructive)
            .title("Error")
            .description(message)
            .duration(Duration::from_secs(10))
    );
}
}

Layout Components

Components for layout and structure: avatar, separator, accordion, and more.

Avatar

User profile images with fallback:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

avatar()
    .src("user.jpg")
    .fallback("JD")
}

Avatar Sizes

#![allow(unused)]
fn main() {
avatar().size(AvatarSize::Sm)    // 32px
avatar().size(AvatarSize::Md)    // 40px (default)
avatar().size(AvatarSize::Lg)    // 48px
avatar().size(AvatarSize::Xl)    // 64px
}

Avatar Fallback

#![allow(unused)]
fn main() {
// Initials fallback
avatar()
    .src("user.jpg")  // If fails to load...
    .fallback("JD")   // Show initials

// Icon fallback
avatar()
    .fallback_icon(icons::USER)
}

Avatar Group

#![allow(unused)]
fn main() {
avatar_group()
    .max(3)  // Show max 3, then "+N"
    .child(avatar().src("user1.jpg"))
    .child(avatar().src("user2.jpg"))
    .child(avatar().src("user3.jpg"))
    .child(avatar().src("user4.jpg"))
    .child(avatar().src("user5.jpg"))
// Displays: [avatar1] [avatar2] [avatar3] [+2]
}

Separator

Visual divider:

#![allow(unused)]
fn main() {
// Horizontal (default)
separator()

// Vertical
separator().orientation(Orientation::Vertical)
}

With Label

#![allow(unused)]
fn main() {
div()
    .flex_row()
    .items_center()
    .gap(8.0)
    .child(separator().flex_1())
    .child(text("or").color(Color::GRAY))
    .child(separator().flex_1())
}

Aspect Ratio

Maintain aspect ratio:

#![allow(unused)]
fn main() {
aspect_ratio(16.0 / 9.0)
    .child(img("video-thumbnail.jpg").cover())
}

Common Ratios

#![allow(unused)]
fn main() {
// 16:9 (video)
aspect_ratio(16.0 / 9.0)

// 4:3 (classic)
aspect_ratio(4.0 / 3.0)

// 1:1 (square)
aspect_ratio(1.0)

// 3:4 (portrait)
aspect_ratio(3.0 / 4.0)
}

Scroll Area

Custom scrollbars:

#![allow(unused)]
fn main() {
scroll_area()
    .h(400.0)
    .child(
        div()
            .flex_col()
            .gap(8.0)
            .children((0..50).map(|i| text(format!("Item {}", i))))
    )
}

Horizontal Scroll

#![allow(unused)]
fn main() {
scroll_area()
    .orientation(Orientation::Horizontal)
    .w(300.0)
    .child(
        div()
            .flex_row()
            .gap(8.0)
            .children((0..20).map(|i|
                card().w(150.0).child(text(format!("Card {}", i)))
            ))
    )
}

Collapsible

Expandable content:

#![allow(unused)]
fn main() {
let is_open = use_state(false);

collapsible()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(collapsible_trigger()
        .child(
            div().flex_row().items_center().gap(8.0)
                .child(text("Show more"))
                .child(icon(if is_open { icons::CHEVRON_UP } else { icons::CHEVRON_DOWN }))
        ))
    .child(collapsible_content()
        .child(text("Hidden content that expands...")))
}

Accordion

Multiple collapsible sections:

#![allow(unused)]
fn main() {
accordion()
    .accordion_type(AccordionType::Single)  // Only one open at a time
    .child(accordion_item("item-1")
        .child(accordion_trigger()
            .child(text("Section 1")))
        .child(accordion_content()
            .child(text("Content for section 1"))))
    .child(accordion_item("item-2")
        .child(accordion_trigger()
            .child(text("Section 2")))
        .child(accordion_content()
            .child(text("Content for section 2"))))
    .child(accordion_item("item-3")
        .child(accordion_trigger()
            .child(text("Section 3")))
        .child(accordion_content()
            .child(text("Content for section 3"))))
}

Multiple Open

#![allow(unused)]
fn main() {
accordion()
    .accordion_type(AccordionType::Multiple)  // Multiple can be open
    // ... accordion items
}

Resizable

Resizable panels:

#![allow(unused)]
fn main() {
resizable()
    .direction(ResizeDirection::Horizontal)
    .child(resizable_panel()
        .default_size(30.0)  // 30%
        .min_size(20.0)
        .child(text("Left Panel")))
    .child(resizable_handle())
    .child(resizable_panel()
        .default_size(70.0)  // 70%
        .child(text("Right Panel")))
}

Vertical Resizable

#![allow(unused)]
fn main() {
resizable()
    .direction(ResizeDirection::Vertical)
    .child(resizable_panel()
        .default_size(50.0)
        .child(text("Top Panel")))
    .child(resizable_handle())
    .child(resizable_panel()
        .default_size(50.0)
        .child(text("Bottom Panel")))
}

Examples

User List Item

#![allow(unused)]
fn main() {
div()
    .flex_row()
    .items_center()
    .gap(12.0)
    .p(12.0)
    .child(avatar().src(&user.avatar).fallback(&user.initials))
    .child(
        div().flex_col()
            .child(text(&user.name).weight(FontWeight::Medium))
            .child(text(&user.email).size(14.0).color(Color::GRAY))
    )
}

FAQ Accordion

#![allow(unused)]
fn main() {
accordion()
    .accordion_type(AccordionType::Single)
    .child(accordion_item("faq-1")
        .child(accordion_trigger()
            .child(text("How do I get started?")))
        .child(accordion_content()
            .child(text("To get started, first install the package..."))))
    .child(accordion_item("faq-2")
        .child(accordion_trigger()
            .child(text("What are the system requirements?")))
        .child(accordion_content()
            .child(text("You need Rust 1.70+ and..."))))
}

Split Pane Editor

#![allow(unused)]
fn main() {
resizable()
    .direction(ResizeDirection::Horizontal)
    .h_full()
    .child(resizable_panel()
        .default_size(25.0)
        .min_size(15.0)
        .child(sidebar()))  // File tree
    .child(resizable_handle())
    .child(resizable_panel()
        .default_size(75.0)
        .child(
            resizable()
                .direction(ResizeDirection::Vertical)
                .child(resizable_panel()
                    .default_size(70.0)
                    .child(editor()))  // Code editor
                .child(resizable_handle())
                .child(resizable_panel()
                    .default_size(30.0)
                    .child(terminal()))  // Terminal
        ))
}

Data Display Components

Components for displaying data: tooltips, popovers, hover cards, charts, and trees.

Tooltip

Brief information on hover:

#![allow(unused)]
fn main() {
use blinc_cn::prelude::*;

tooltip()
    .child(tooltip_trigger()
        .child(button("Hover me")))
    .child(tooltip_content()
        .child(text("This is a tooltip")))
}

Tooltip Position

#![allow(unused)]
fn main() {
tooltip()
    .side(TooltipSide::Top)     // Top (default)
    .side(TooltipSide::Bottom)  // Bottom
    .side(TooltipSide::Left)    // Left
    .side(TooltipSide::Right)   // Right
}

With Arrow

#![allow(unused)]
fn main() {
tooltip()
    .child(tooltip_trigger().child(icon(icons::INFO)))
    .child(tooltip_content()
        .with_arrow(true)
        .child(text("More information")))
}

Hover Card

Rich content on hover:

#![allow(unused)]
fn main() {
hover_card()
    .child(hover_card_trigger()
        .child(text("@username").color(Color::BLUE)))
    .child(hover_card_content()
        .child(
            div().flex_row().gap(12.0)
                .child(avatar().src("user.jpg").size(AvatarSize::Lg))
                .child(
                    div().flex_col().gap(4.0)
                        .child(text("John Doe").weight(FontWeight::Bold))
                        .child(text("@johndoe").color(Color::GRAY))
                        .child(text("Software developer at Acme Inc."))
                )
        ))
}

Popover

Interactive content in a popup:

#![allow(unused)]
fn main() {
let is_open = use_state(false);

popover()
    .open(is_open)
    .on_open_change(|open| set_is_open(open))
    .child(popover_trigger()
        .child(button("Open Popover")))
    .child(popover_content()
        .child(
            div().flex_col().gap(12.0)
                .child(text("Settings").weight(FontWeight::Bold))
                .child(
                    div().flex_col().gap(8.0)
                        .child(
                            div().flex_row().justify_between()
                                .child(label("Notifications"))
                                .child(switch_())
                        )
                        .child(
                            div().flex_row().justify_between()
                                .child(label("Dark Mode"))
                                .child(switch_())
                        )
                )
        ))
}

Chart

Data visualization:

#![allow(unused)]
fn main() {
chart()
    .chart_type(ChartType::Line)
    .data(&[
        DataPoint::new("Jan", 100.0),
        DataPoint::new("Feb", 150.0),
        DataPoint::new("Mar", 120.0),
        DataPoint::new("Apr", 180.0),
    ])
    .x_label("Month")
    .y_label("Sales")
}

Chart Types

#![allow(unused)]
fn main() {
// Line chart
chart().chart_type(ChartType::Line)

// Bar chart
chart().chart_type(ChartType::Bar)

// Area chart
chart().chart_type(ChartType::Area)

// Pie chart
chart().chart_type(ChartType::Pie)

// Histogram
chart().chart_type(ChartType::Histogram)

// Scatter plot
chart().chart_type(ChartType::Scatter)
}

Multi-Series

#![allow(unused)]
fn main() {
chart()
    .chart_type(ChartType::Line)
    .series("Revenue", &revenue_data, Color::BLUE)
    .series("Expenses", &expense_data, Color::RED)
    .series("Profit", &profit_data, Color::GREEN)
    .legend(true)
}

Bar Chart

#![allow(unused)]
fn main() {
chart()
    .chart_type(ChartType::Bar)
    .data(&[
        DataPoint::new("Q1", 1200.0),
        DataPoint::new("Q2", 1500.0),
        DataPoint::new("Q3", 1800.0),
        DataPoint::new("Q4", 2100.0),
    ])
    .color(Color::BLUE)
    .show_values(true)
}

Pie Chart

#![allow(unused)]
fn main() {
chart()
    .chart_type(ChartType::Pie)
    .data(&[
        DataPoint::new("Desktop", 45.0),
        DataPoint::new("Mobile", 35.0),
        DataPoint::new("Tablet", 20.0),
    ])
    .show_labels(true)
    .show_percentages(true)
}

Tree

Hierarchical data display:

#![allow(unused)]
fn main() {
tree()
    .child(tree_item("root")
        .child(tree_item_content()
            .child(icon(icons::FOLDER))
            .child(text("Documents")))
        .child(tree_item("doc1")
            .child(tree_item_content()
                .child(icon(icons::FILE))
                .child(text("Report.pdf"))))
        .child(tree_item("doc2")
            .child(tree_item_content()
                .child(icon(icons::FILE))
                .child(text("Notes.txt")))))
}

Expandable Tree

#![allow(unused)]
fn main() {
tree()
    .child(tree_item("projects")
        .expandable(true)
        .expanded(true)
        .child(tree_item_trigger()
            .child(icon(icons::FOLDER))
            .child(text("Projects")))
        .child(tree_item_content()
            .child(tree_item("project1")
                .child(tree_item_trigger()
                    .child(icon(icons::FOLDER))
                    .child(text("Project A")))
                .child(tree_item_content()
                    .child(tree_item("file1")
                        .child(tree_item_content()
                            .child(icon(icons::FILE))
                            .child(text("main.rs"))))))))
}

Selectable Tree

#![allow(unused)]
fn main() {
let selected = use_state(HashSet::new());

tree()
    .selectable(true)
    .selected(&selected)
    .on_select(|ids| set_selected(ids))
    .child(/* tree items */)
}

Kbd

Keyboard shortcut display:

#![allow(unused)]
fn main() {
// Single key
kbd("⌘")

// Key combination
div().flex_row().gap(4.0)
    .child(kbd("⌘"))
    .child(kbd("K"))

// In context
div().flex_row().items_center().gap(8.0)
    .child(text("Search"))
    .child(
        div().flex_row().gap(2.0)
            .child(kbd("⌘"))
            .child(kbd("K"))
    )
}

Examples

User Profile Card

#![allow(unused)]
fn main() {
hover_card()
    .child(hover_card_trigger()
        .child(
            div().flex_row().items_center().gap(8.0)
                .child(avatar().src(&user.avatar).size(AvatarSize::Sm))
                .child(text(&user.name))
        ))
    .child(hover_card_content()
        .w(300.0)
        .child(
            div().flex_col().gap(12.0)
                .child(
                    div().flex_row().gap(12.0)
                        .child(avatar().src(&user.avatar).size(AvatarSize::Lg))
                        .child(
                            div().flex_col()
                                .child(text(&user.name).weight(FontWeight::Bold))
                                .child(text(&user.title).color(Color::GRAY))
                        )
                )
                .child(text(&user.bio))
                .child(
                    div().flex_row().gap(16.0)
                        .child(
                            div().flex_col()
                                .child(text(&user.followers.to_string()).weight(FontWeight::Bold))
                                .child(text("Followers").size(12.0).color(Color::GRAY))
                        )
                        .child(
                            div().flex_col()
                                .child(text(&user.following.to_string()).weight(FontWeight::Bold))
                                .child(text("Following").size(12.0).color(Color::GRAY))
                        )
                )
        ))
}

Dashboard Chart

#![allow(unused)]
fn main() {
card()
    .child(card_header()
        .child(card_title("Revenue Overview"))
        .child(card_description("Monthly revenue for 2024")))
    .child(card_content()
        .child(
            chart()
                .chart_type(ChartType::Area)
                .h(300.0)
                .data(&monthly_revenue)
                .color(Color::rgba(0.2, 0.5, 1.0, 0.5))
                .stroke_color(Color::BLUE)
                .x_label("Month")
                .y_label("Revenue ($)")
                .grid(true)
        ))
}

File Tree

#![allow(unused)]
fn main() {
tree()
    .child(tree_item("src")
        .expandable(true)
        .expanded(true)
        .child(tree_item_trigger()
            .child(icon(icons::FOLDER_OPEN))
            .child(text("src")))
        .child(tree_item_content()
            .child(tree_item("main")
                .on_click(|| open_file("src/main.rs"))
                .child(tree_item_content()
                    .child(icon(icons::FILE_CODE))
                    .child(text("main.rs"))))
            .child(tree_item("lib")
                .on_click(|| open_file("src/lib.rs"))
                .child(tree_item_content()
                    .child(icon(icons::FILE_CODE))
                    .child(text("lib.rs"))))))
}

Buttons & Inputs

Blinc provides ready-to-use input widgets with built-in state management.

Buttons

Basic Button

#![allow(unused)]
fn main() {
use blinc_layout::widgets::button::{button, Button};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let btn_state = ctx.use_state_for("save_btn", ButtonState::Idle);

    button(btn_state, "Save")
        .on_click(|_| {
            println!("Saved!");
        })
}
}

Styled Buttons

#![allow(unused)]
fn main() {
button(state, "Primary")
    .bg_color(Color::rgba(0.3, 0.5, 0.9, 1.0))
    .hover_color(Color::rgba(0.4, 0.6, 1.0, 1.0))
    .pressed_color(Color::rgba(0.2, 0.4, 0.8, 1.0))
    .text_color(Color::WHITE)
    .rounded(8.0)
    .p(2.0)
}

Custom Content Buttons

#![allow(unused)]
fn main() {
Button::with_content(state, |s| {
    div()
        .flex_row()
        .gap(8.0)
        .items_center()
        .child(svg("icons/save.svg").w(16.0).h(16.0).tint(Color::WHITE))
        .child(text("Save").color(Color::WHITE))
})
.on_click(|_| save_file())
}

Disabled Buttons

#![allow(unused)]
fn main() {
let state = ctx.use_state_for("btn", ButtonState::Disabled);

button(state, "Cannot Click")
    .disabled_color(Color::rgba(0.2, 0.2, 0.25, 0.5))
}

Checkboxes

Basic Checkbox

#![allow(unused)]
fn main() {
use blinc_layout::widgets::checkbox::{checkbox, checkbox_state};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = checkbox_state(false);  // Initially unchecked

    checkbox(&state)
        .on_change(|checked| {
            println!("Checkbox is now: {}", checked);
        })
}
}

Labeled Checkbox

#![allow(unused)]
fn main() {
checkbox(&state)
    .label("Remember me")
    .label_color(Color::WHITE)
}

Styled Checkbox

#![allow(unused)]
fn main() {
checkbox(&state)
    .check_color(Color::rgba(0.4, 0.6, 1.0, 1.0))
    .bg_color(Color::rgba(0.2, 0.2, 0.25, 1.0))
    .rounded(4.0)
    .size(20.0)
}

Initially Checked

#![allow(unused)]
fn main() {
let state = checkbox_state(true);  // Start checked
}

Text Input

Basic Text Input

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_input::{text_input, text_input_state};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = text_input_state("Enter your name...");

    text_input(&state)
        .w(300.0)
        .on_change(|text| {
            println!("Input: {}", text);
        })
}
}

Styled Text Input

#![allow(unused)]
fn main() {
text_input(&state)
    .w(300.0)
    .rounded(8.0)
    .bg_color(Color::rgba(0.15, 0.15, 0.2, 1.0))
    .text_color(Color::WHITE)
    .placeholder_color(Color::rgba(0.5, 0.5, 0.6, 1.0))
    .focus_border_color(Color::rgba(0.4, 0.6, 1.0, 1.0))
}

Reading Input Value

#![allow(unused)]
fn main() {
let state = text_input_state("");

// Later, read the current value
let current_text = state.text();
}

Text Area

Basic Text Area

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_area::{text_area, text_area_state};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = text_area_state("Enter description...");

    text_area(&state)
        .w(400.0)
        .h(200.0)
        .on_change(|text| {
            println!("Content: {}", text);
        })
}
}

Styled Text Area

#![allow(unused)]
fn main() {
text_area(&state)
    .w(400.0)
    .h(200.0)
    .rounded(8.0)
    .bg_color(Color::rgba(0.15, 0.15, 0.2, 1.0))
    .text_color(Color::WHITE)
    .font_size(14.0)
    .line_height(1.5)
}

Code Editor

Syntax Highlighted Code

use blinc_layout::widgets::code::code;

fn my_ui() -> impl ElementBuilder {
    let source = r#"
fn main() {
    println!("Hello, Blinc!");
}
"#;

    code(source)
        .lang("rust")
        .w_full()
        .h(300.0)
        .rounded(8.0)
        .font("Fira Code")
        .size(14.0)
}

Supported Languages

  • rust, python, javascript, typescript
  • html, css, json, yaml, xml
  • sql, bash, go, java, c, cpp
  • And more…

Form Example

#![allow(unused)]
fn main() {
fn login_form(ctx: &WindowedContext) -> impl ElementBuilder {
    let email_state = text_input_state("Email address");
    let password_state = text_input_state("Password");
    let remember_state = checkbox_state(false);
    let submit_state = ctx.use_state_for("submit", ButtonState::Idle);

    div()
        .w(400.0)
        .p(24.0)
        .rounded(16.0)
        .bg(Color::rgba(0.12, 0.12, 0.16, 1.0))
        .flex_col()
        .gap(16.0)
        // Title
        .child(
            text("Sign In")
                .size(24.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
        // Email field
        .child(
            div()
                .flex_col()
                .gap(4.0)
                .child(label("Email").color(Color::WHITE))
                .child(
                    text_input(&email_state)
                        .w_full()
                        .rounded(8.0)
                )
        )
        // Password field
        .child(
            div()
                .flex_col()
                .gap(4.0)
                .child(label("Password").color(Color::WHITE))
                .child(
                    text_input(&password_state)
                        .w_full()
                        .rounded(8.0)
                        // Note: password masking would be a feature to add
                )
        )
        // Remember me
        .child(
            checkbox(&remember_state)
                .label("Remember me")
                .label_color(Color::WHITE)
        )
        // Submit button
        .child(
            button(submit_state, "Sign In")
                .w_full()
                .bg_color(Color::rgba(0.3, 0.5, 0.9, 1.0))
                .text_color(Color::WHITE)
                .rounded(8.0)
                .on_click(|_| {
                    println!("Form submitted!");
                })
        )
}
}

Widget State Types

Each widget uses a specific state type:

WidgetState TypeStates
ButtonButtonStateIdle, Hovered, Pressed, Disabled
CheckboxCheckboxStateUncheckedIdle, UncheckedHovered, CheckedIdle, CheckedHovered
TextInputTextFieldStateIdle, Hovered, Focused, FocusedHovered, Disabled
TextAreaTextFieldStateSame as TextInput

Best Practices

  1. Use unique keys for state - Each widget needs its own state key.

  2. Handle validation in on_change - Validate input as users type.

  3. Provide visual feedback - Use colors to indicate focus and errors.

  4. Group related inputs - Use flex containers to organize forms.

  5. Add labels - Every input should have an associated label for accessibility.

Text & Rich Text

Blinc provides two main elements for displaying text: text() for plain text and rich_text() for inline-formatted text with HTML-like markup.

Plain Text

The text() element is the simplest way to display text:

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

// Basic text
text("Hello, World!")

// Styled text
text("Styled text")
    .size(24.0)
    .color(Color::BLUE)
    .bold()
    .italic()

// Text with decorations
text("Underlined and struck")
    .underline()
    .strikethrough()
}

Text Properties

MethodDescription
.size(f32)Font size in pixels
.color(Color)Text color
.bold()Bold weight
.italic()Italic style
.underline()Underline decoration
.strikethrough()Strikethrough decoration
.align(TextAlign)Horizontal alignment (Left, Center, Right)
.v_align(TextVerticalAlign)Vertical alignment (Top, Middle, Bottom)
.font_family(FontFamily)Custom font family
.line_height(f32)Line height multiplier (default: 1.2)
.wrap(bool)Enable/disable text wrapping

Rich Text

The rich_text() element supports inline formatting using HTML-like tags. This is ideal for text that needs mixed styling within a single block.

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

// Basic formatting
rich_text("This has <b>bold</b> and <i>italic</i> text.")
    .size(16.0)
    .default_color(Color::WHITE)

// Nested tags
rich_text("<b>Bold with <i>nested italic</i></b>")

// Inline colors
rich_text(r#"Colors: <span color="#FF0000">red</span> and <span color="blue">blue</span>"#)

// Links (clickable, opens in browser)
rich_text(r#"Visit <a href="https://example.com">our website</a> for more info."#)
}

Supported Tags

TagEffect
<b>, <strong>Bold text
<i>, <em>Italic text
<u>Underlined text
<s>, <strike>, <del>Strikethrough text
<a href="url">Clickable link (auto-underlined)
<span color="...">Inline color

Color Formats

The <span color="..."> tag supports multiple color formats:

#![allow(unused)]
fn main() {
// Hex colors
rich_text(r#"<span color="#FF0000">Red</span>"#)
rich_text(r#"<span color="#F00">Short hex</span>"#)
rich_text(r#"<span color="#FF000080">With alpha</span>"#)

// Named colors (CSS subset)
rich_text(r#"<span color="crimson">Crimson</span>"#)
rich_text(r#"<span color="steelblue">Steel Blue</span>"#)

// RGB/RGBA
rich_text(r#"<span color="rgb(255, 128, 0)">Orange</span>"#)
}

Supported named colors: black, white, red, green, blue, yellow, cyan, magenta, gray, silver, maroon, olive, navy, purple, teal, orange, pink, brown, lime, coral, gold, indigo, violet, crimson, salmon, tomato, skyblue, steelblue, transparent

HTML Entity Decoding

Rich text automatically decodes common HTML entities:

#![allow(unused)]
fn main() {
rich_text("Use &lt;b&gt; for bold")  // Renders: Use <b> for bold
rich_text("&copy; 2024 &bull; All Rights Reserved &trade;")
rich_text("&ldquo;Smart quotes&rdquo; &mdash; and &hellip;")
}

Supported entities: &lt;, &gt;, &amp;, &quot;, &apos;, &nbsp;, &copy;, &reg;, &trade;, &mdash;, &ndash;, &hellip;, &lsquo;, &rsquo;, &ldquo;, &rdquo;, &bull;, &middot;, and numeric entities (&#65;, &#x41;)

Range-Based API

For programmatic control, use the range-based API with byte indices:

#![allow(unused)]
fn main() {
// Style specific byte ranges
rich_text("Hello World")
    .bold_range(0..5)           // "Hello" is bold
    .color_range(6..11, Color::CYAN)  // "World" is cyan
    .size(18.0)
    .default_color(Color::WHITE)

// Multiple overlapping styles
rich_text("Important Notice: Please read carefully!")
    .bold_range(0..16)           // "Important Notice" bold
    .color_range(0..9, Color::ORANGE)  // "Important" orange
    .underline_range(18..39)     // "Please read carefully" underlined
}

Available range methods:

  • .bold_range(Range<usize>)
  • .italic_range(Range<usize>)
  • .underline_range(Range<usize>)
  • .strikethrough_range(Range<usize>)
  • .color_range(Range<usize>, Color)
  • .link_range(Range<usize>, url: &str)

Links in rich text are fully interactive:

  • Click to open: Clicking a link opens the URL in the system’s default browser
  • Pointer cursor: The cursor changes to a pointer when hovering over links
  • Auto-underlined: Links are automatically underlined for visibility
#![allow(unused)]
fn main() {
rich_text(r#"
    Check the <a href="https://docs.example.com">documentation</a>
    or view the <a href="https://github.com/example">source code</a>.
"#)
    .size(14.0)
    .default_color(Color::WHITE)
}

For simple clickable text, use the link() widget:

#![allow(unused)]
fn main() {
// Default behavior - opens URL in browser
link("Click here", "https://example.com")

// Custom styling
link("Styled link", "https://example.com")
    .size(18.0)
    .color(Color::CYAN)
    .no_underline()

// Underline only on hover
link("Hover to see underline", "https://example.com")
    .underline_on_hover()
}

From StyledText

For integration with syntax highlighting or markdown rendering, create rich text from a pre-built StyledText:

#![allow(unused)]
fn main() {
use blinc_layout::styled_text::{StyledText, StyledLine, TextSpan};

let styled = StyledText {
    lines: vec![
        StyledLine {
            text: "Hello World".to_string(),
            spans: vec![
                TextSpan {
                    start: 0,
                    end: 5,
                    bold: true,
                    color: Color::RED,
                    ..Default::default()
                },
            ],
        },
    ],
};

rich_text_styled(styled)
    .size(16.0)
    .default_color(Color::WHITE)
}

Example

Here’s a complete example demonstrating various text features:

#![allow(unused)]
fn main() {
use blinc_app::prelude::*;
use blinc_core::Color;

fn demo_ui() -> impl ElementBuilder {
    div()
        .flex_col()
        .gap(16.0)
        .p(20.0)
        // Plain text
        .child(
            text("Plain Text Example")
                .size(24.0)
                .color(Color::WHITE)
                .bold()
        )
        // Rich text with inline formatting
        .child(
            rich_text("This is <b>bold</b>, <i>italic</i>, and <span color=\"#00FF00\">green</span>.")
                .size(16.0)
                .default_color(Color::WHITE)
        )
        // Interactive link
        .child(
            rich_text(r#"Visit <a href="https://github.com">GitHub</a> for more."#)
                .size(16.0)
                .default_color(Color::WHITE)
        )
        // Range-based styling
        .child(
            rich_text("Programmatic styling with ranges")
                .bold_range(0..13)
                .color_range(14..21, Color::CYAN)
                .underline_range(22..32)
                .size(16.0)
                .default_color(Color::WHITE)
        )
}
}

Run the rich text demo to see all features in action:

cargo run -p blinc_app --example rich_text_demo --features windowed

Scroll Containers

Blinc provides scroll containers with WebKit-style momentum scrolling and bounce physics.

Basic Scroll

#![allow(unused)]
fn main() {
use blinc_layout::widgets::scroll::scroll;

fn scrollable_content() -> impl ElementBuilder {
    scroll()
        .h(400.0)
        .child(
            div()
                .flex_col()
                .gap(8.0)
                .child(/* ... long content ... */)
        )
}
}

Scroll Without Bounce

#![allow(unused)]
fn main() {
use blinc_layout::widgets::scroll::scroll_no_bounce;

scroll_no_bounce()
    .h(400.0)
    .child(content)
}

Scroll Configuration

#![allow(unused)]
fn main() {
use blinc_layout::widgets::scroll::{Scroll, ScrollConfig, ScrollDirection};
use blinc_animation::SpringConfig;

Scroll::with_config(ScrollConfig {
    bounce_enabled: true,
    bounce_spring: SpringConfig::wobbly(),
    deceleration: 1500.0,
    velocity_threshold: 10.0,
    max_overscroll: 0.3,  // 30% of viewport
    direction: ScrollDirection::Vertical,
})
.h(400.0)
.child(content)
}

Configuration Presets

#![allow(unused)]
fn main() {
ScrollConfig::default()       // Standard bounce
ScrollConfig::no_bounce()     // No bounce physics
ScrollConfig::stiff_bounce()  // Tight, minimal bounce
ScrollConfig::gentle_bounce() // Soft, more bounce
}

Scroll Directions

#![allow(unused)]
fn main() {
// Vertical only (default)
Scroll::with_config(ScrollConfig {
    direction: ScrollDirection::Vertical,
    ..Default::default()
})

// Horizontal only
Scroll::with_config(ScrollConfig {
    direction: ScrollDirection::Horizontal,
    ..Default::default()
})

// Both directions
Scroll::with_config(ScrollConfig {
    direction: ScrollDirection::Both,
    ..Default::default()
})
}

Scroll States

Scroll containers use ScrollState for physics-driven behavior:

#![allow(unused)]
fn main() {
ScrollState::Idle         // Not scrolling
ScrollState::Scrolling    // User is dragging
ScrollState::Decelerating // Momentum after release
ScrollState::Bouncing     // Edge bounce animation
}

Example: Scrollable List

#![allow(unused)]
fn main() {
fn message_list() -> impl ElementBuilder {
    scroll()
        .h(500.0)
        .w_full()
        .child(
            div()
                .flex_col()
                .gap(8.0)
                .p(16.0)
                .child(
                    (0..50).map(|i| {
                        div()
                            .p(12.0)
                            .rounded(8.0)
                            .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
                            .child(
                                text(&format!("Message {}", i + 1))
                                    .color(Color::WHITE)
                            )
                    })
                )
        )
}
}
#![allow(unused)]
fn main() {
fn image_gallery() -> impl ElementBuilder {
    Scroll::with_config(ScrollConfig {
        direction: ScrollDirection::Horizontal,
        ..Default::default()
    })
    .h(200.0)
    .w_full()
    .child(
        div()
            .flex_row()
            .gap(16.0)
            .p(16.0)
            .child(
                (0..10).map(|i| {
                    div()
                        .w(150.0)
                        .h(150.0)
                        .rounded(12.0)
                        .bg(Color::rgba(0.2, 0.3, 0.5, 1.0))
                        .flex_center()
                        .child(text(&format!("{}", i + 1)).size(24.0).color(Color::WHITE))
                })
            )
    )
}
}

Nested Scrolling

Scroll containers handle nested scrolling automatically. Inner scrolls consume events when they can scroll; outer scrolls take over at boundaries.

#![allow(unused)]
fn main() {
fn nested_scroll_example() -> impl ElementBuilder {
    // Outer vertical scroll
    scroll()
        .h(600.0)
        .child(
            div()
                .flex_col()
                .gap(16.0)
                .child(text("Section 1").size(24.0))
                // Inner horizontal scroll
                .child(
                    Scroll::with_config(ScrollConfig {
                        direction: ScrollDirection::Horizontal,
                        ..Default::default()
                    })
                    .h(120.0)
                    .child(horizontal_items())
                )
                .child(text("Section 2").size(24.0))
                .child(more_content())
        )
}
}

Physics Parameters

ParameterDefaultDescription
deceleration1500.0How quickly momentum decays (higher = faster stop)
velocity_threshold10.0Minimum velocity to continue momentum
max_overscroll0.3Maximum overscroll as fraction of viewport
bounce_springwobblySpring config for bounce animation

Programmatic Scroll Control

Blinc provides a powerful selector API for programmatic scroll control through ScrollRef. This allows you to scroll to specific elements, positions, or the top/bottom of content.

Creating a ScrollRef

Use ctx.use_scroll_ref() to create a persistent scroll reference:

#![allow(unused)]
fn main() {
use blinc_layout::selector::{ScrollRef, ScrollOptions, ScrollBehavior, ScrollBlock};

fn my_component(ctx: &WindowedContext) -> impl ElementBuilder {
    // Create a ScrollRef - persists across rebuilds
    let scroll_ref = ctx.use_scroll_ref("my_scroll");

    scroll()
        .bind(&scroll_ref)  // Bind the ref to this scroll container
        .child(content)
}
}

Element IDs

Assign IDs to elements you want to scroll to:

#![allow(unused)]
fn main() {
fn card_list() -> impl ElementBuilder {
    div()
        .flex_col()
        .children(
            (0..10).map(|i| {
                div()
                    .id(format!("card-{}", i))  // Assign unique ID
                    .child(text(&format!("Card {}", i)))
            })
        )
}
}

Scrolling to Elements

Use scroll_to() or scroll_to_with_options() to scroll to an element by ID:

#![allow(unused)]
fn main() {
// Simple scroll to element
scroll_ref.scroll_to("card-5");

// Scroll with options
scroll_ref.scroll_to_with_options(
    "card-5",
    ScrollOptions {
        behavior: ScrollBehavior::Smooth,  // Animate the scroll
        block: ScrollBlock::Center,        // Center element in viewport
        ..Default::default()
    },
);
}

ScrollOptions

Configure how the scroll behaves:

#![allow(unused)]
fn main() {
ScrollOptions {
    behavior: ScrollBehavior::Smooth,  // or ScrollBehavior::Auto (instant)
    block: ScrollBlock::Center,        // Vertical alignment
    inline: ScrollInline::Nearest,     // Horizontal alignment
}
}
Block/Inline ValueDescription
StartAlign to top/left of viewport
CenterAlign to center of viewport
EndAlign to bottom/right of viewport
NearestScroll minimum distance to make visible (default)

Other Scroll Operations

#![allow(unused)]
fn main() {
// Scroll to top/bottom
scroll_ref.scroll_to_top();
scroll_ref.scroll_to_bottom();

// With smooth animation
scroll_ref.scroll_to_bottom_with_behavior(ScrollBehavior::Smooth);

// Scroll by relative amount
scroll_ref.scroll_by(0.0, 100.0);  // Scroll down 100px

// Set absolute offset
scroll_ref.set_scroll_offset(0.0, 500.0);
}

Querying Scroll State

#![allow(unused)]
fn main() {
// Current offset
let (x, y) = scroll_ref.offset();
let y = scroll_ref.scroll_y();

// Content and viewport sizes
let content_size = scroll_ref.content_size();
let viewport_size = scroll_ref.viewport_size();

// Position checks
if scroll_ref.is_at_top() { /* ... */ }
if scroll_ref.is_at_bottom() { /* ... */ }

// Scroll progress (0.0 = top, 1.0 = bottom)
let progress = scroll_ref.scroll_progress();
}

Here’s a complete example of a horizontal carousel with clickable navigation dots:

#![allow(unused)]
fn main() {
use blinc_app::prelude::*;
use blinc_layout::selector::{ScrollBehavior, ScrollBlock, ScrollOptions, ScrollRef};
use blinc_layout::units::px;  // Semantic unit for raw pixels

fn carousel(ctx: &WindowedContext) -> impl ElementBuilder {
    let scroll_ref = ctx.use_scroll_ref("carousel_scroll");
    let current_index = ctx.use_state_keyed("current_index", || 0usize);

    div()
        .flex_col()
        .items_center()
        .gap(16.0)
        // Horizontal scroll carousel
        .child(
            scroll()
                .bind(&scroll_ref)
                .direction(ScrollDirection::Horizontal)
                .w(400.0)
                .h(300.0)
                .child(
                    div()
                        .flex_row()
                        .gap(20.0)
                        .padding_x(px(60.0))  // Padding to center first/last cards
                        .children(
                            (0..5).map(|i| {
                                div()
                                    .id(format!("card-{}", i))  // Element ID
                                    .w(280.0)
                                    .h(280.0)
                                    .bg(Color::rgba(0.2, 0.3, 0.5, 1.0))
                                    .rounded(16.0)
                                    .child(text(&format!("Card {}", i + 1)))
                            })
                        ),
                ),
        )
        // Navigation dots
        .child(build_dots(ctx, &scroll_ref, &current_index))
}

fn build_dots(
    ctx: &WindowedContext,
    scroll_ref: &ScrollRef,
    current_index: &State<usize>,
) -> impl ElementBuilder {
    div()
        .flex_row()
        .gap(12.0)
        .children(
            (0..5).map(|i| {
                let scroll_ref = scroll_ref.clone();
                let current_index = current_index.clone();

                div()
                    .w(12.0)
                    .h(12.0)
                    .rounded(6.0)
                    .bg(if i == current_index.get() {
                        Color::rgba(0.4, 0.6, 1.0, 1.0)
                    } else {
                        Color::rgba(0.3, 0.3, 0.4, 1.0)
                    })
                    .on_click(move |_| {
                        current_index.set(i);
                        scroll_ref.scroll_to_with_options(
                            &format!("card-{}", i),
                            ScrollOptions {
                                behavior: ScrollBehavior::Smooth,
                                block: ScrollBlock::Center,
                                ..Default::default()
                            },
                        );
                    })
            })
        )
}
}

Best Practices

  1. Set explicit height - Scroll containers need a bounded height to work.

  2. Use overflow_clip on parent - Ensure parent clips overflowing content.

  3. Prefer vertical for long content - Horizontal scrolling is less intuitive for lists.

  4. Consider no-bounce for forms - Disable bounce for content that needs precise positioning.

  5. Test nested scrolling - Verify inner/outer scroll interactions work as expected.

  6. Use meaningful element IDs - Choose descriptive IDs like "message-123" or "section-intro" for elements you need to scroll to.

  7. Prefer ctx.use_scroll_ref() - Always use the context method rather than ScrollRef::new() for proper reactive integration.

Canvas Drawing

The canvas() element provides direct GPU drawing access for custom graphics, charts, and procedural content.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_core::{DrawContext, Rect, Brush, Color, CornerRadius};

canvas(|ctx: &mut dyn DrawContext, bounds| {
    // bounds contains the canvas size
    ctx.fill_rect(
        Rect::new(0.0, 0.0, bounds.width, bounds.height),
        CornerRadius::uniform(8.0),
        Brush::Solid(Color::RED),
    );
})
.w(200.0)
.h(100.0)
}

Drawing Primitives

Filled Rectangles

#![allow(unused)]
fn main() {
ctx.fill_rect(
    Rect::new(x, y, width, height),
    CornerRadius::uniform(8.0),  // Corner radius
    Brush::Solid(Color::BLUE),
);

// No corner radius
ctx.fill_rect(
    Rect::new(10.0, 10.0, 100.0, 50.0),
    CornerRadius::default(),
    Brush::Solid(Color::GREEN),
);
}

Stroked Rectangles

#![allow(unused)]
fn main() {
ctx.stroke_rect(
    Rect::new(x, y, width, height),
    CornerRadius::uniform(4.0),
    2.0,  // Stroke width
    Brush::Solid(Color::WHITE),
);
}

Circles

#![allow(unused)]
fn main() {
// Filled circle
ctx.fill_circle(
    Point::new(cx, cy),  // Center
    radius,
    Brush::Solid(Color::BLUE),
);

// Stroked circle
ctx.stroke_circle(
    Point::new(cx, cy),
    radius,
    2.0,  // Stroke width
    Brush::Solid(Color::WHITE),
);
}

Text

#![allow(unused)]
fn main() {
use blinc_core::TextStyle;

ctx.draw_text(
    "Hello, Canvas!",
    Point::new(x, y),
    &TextStyle::new(16.0).with_color(Color::WHITE),
);
}

Gradients

#![allow(unused)]
fn main() {
use blinc_core::{Gradient, GradientStop, Point};

// Linear gradient
let gradient = Brush::Gradient(Gradient::linear(
    Point::new(0.0, 0.0),      // Start
    Point::new(200.0, 0.0),    // End
    Color::rgba(0.9, 0.2, 0.5, 1.0),
    Color::rgba(0.2, 0.8, 0.6, 1.0),
));

ctx.fill_rect(
    Rect::new(0.0, 0.0, 200.0, 100.0),
    CornerRadius::default(),
    gradient,
);

// Multi-stop gradient
let gradient = Brush::Gradient(Gradient::linear_with_stops(
    Point::new(0.0, 0.0),
    Point::new(200.0, 0.0),
    vec![
        GradientStop::new(0.0, Color::RED),
        GradientStop::new(0.5, Color::YELLOW),
        GradientStop::new(1.0, Color::GREEN),
    ],
));
}

Transforms

#![allow(unused)]
fn main() {
use blinc_core::Transform;

// Push transform
ctx.push_transform(Transform::translate(50.0, 50.0));

// Draw in transformed space
ctx.fill_rect(/* ... */);

// Pop transform
ctx.pop_transform();

// Rotation
ctx.push_transform(Transform::rotate(angle_radians));
// ... draw ...
ctx.pop_transform();

// Scale
ctx.push_transform(Transform::scale(2.0, 2.0));
// ... draw ...
ctx.pop_transform();
}

Clipping

#![allow(unused)]
fn main() {
// Push clip region
ctx.push_clip(Rect::new(10.0, 10.0, 100.0, 100.0));

// Only content within clip region is visible
ctx.fill_rect(/* ... */);

// Pop clip
ctx.pop_clip();
}

Example: Animated Spinner

#![allow(unused)]
fn main() {
use std::f32::consts::PI;

fn spinner(ctx: &WindowedContext) -> impl ElementBuilder {
    let timeline = ctx.use_animated_timeline();

    let entry_id = timeline.lock().unwrap().configure(|t| {
        let id = t.add(0, 1000, 0.0, 360.0);
        t.set_loop(-1);
        t.start();
        id
    });

    let render_timeline = Arc::clone(&timeline);

    canvas(move |draw_ctx, bounds| {
        let angle_deg = render_timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);
        let angle_rad = angle_deg * PI / 180.0;

        let cx = bounds.width / 2.0;
        let cy = bounds.height / 2.0;
        let radius = 30.0;

        // Draw spinning segments
        for i in 0..8 {
            let segment_angle = angle_rad + (i as f32 * PI / 4.0);
            let alpha = 1.0 - (i as f32 * 0.1);

            let x = cx + segment_angle.cos() * radius;
            let y = cy + segment_angle.sin() * radius;

            draw_ctx.fill_circle(
                Point::new(x, y),
                4.0,
                Brush::Solid(Color::rgba(0.4, 0.6, 1.0, alpha)),
            );
        }
    })
    .w(80.0)
    .h(80.0)
}
}

Example: Progress Ring

#![allow(unused)]
fn main() {
fn progress_ring(progress: f32) -> impl ElementBuilder {
    canvas(move |ctx, bounds| {
        let cx = bounds.width / 2.0;
        let cy = bounds.height / 2.0;
        let radius = bounds.width.min(bounds.height) / 2.0 - 4.0;

        // Background ring
        ctx.stroke_circle(
            Point::new(cx, cy),
            radius,
            4.0,
            Brush::Solid(Color::rgba(0.2, 0.2, 0.25, 1.0)),
        );

        // Progress arc (simplified - actual arc drawing would need path API)
        // For now, draw segments
        let segments = 32;
        let filled = (segments as f32 * progress) as i32;

        for i in 0..filled {
            let angle = (i as f32 / segments as f32) * 2.0 * PI - PI / 2.0;
            let x = cx + angle.cos() * radius;
            let y = cy + angle.sin() * radius;

            ctx.fill_circle(
                Point::new(x, y),
                3.0,
                Brush::Solid(Color::rgba(0.4, 0.6, 1.0, 1.0)),
            );
        }

        // Center text
        ctx.draw_text(
            &format!("{}%", (progress * 100.0) as i32),
            Point::new(cx - 15.0, cy + 6.0),
            &TextStyle::new(16.0).with_color(Color::WHITE),
        );
    })
    .w(80.0)
    .h(80.0)
}
}

Example: Color Palette

#![allow(unused)]
fn main() {
fn color_palette() -> impl ElementBuilder {
    canvas(|ctx, bounds| {
        let cols = 8;
        let rows = 3;
        let cell_w = bounds.width / cols as f32;
        let cell_h = bounds.height / rows as f32;

        for row in 0..rows {
            for col in 0..cols {
                let hue = col as f32 / cols as f32;
                let sat = 1.0 - (row as f32 * 0.25);
                let color = hsv_to_rgb(hue, sat, 0.9);

                ctx.fill_rect(
                    Rect::new(
                        col as f32 * cell_w,
                        row as f32 * cell_h,
                        cell_w - 2.0,
                        cell_h - 2.0,
                    ),
                    CornerRadius::uniform(4.0),
                    Brush::Solid(color),
                );
            }
        }
    })
    .w(240.0)
    .h(90.0)
}
}

Best Practices

  1. Set explicit size - Canvas needs width and height to render.

  2. Use bounds parameter - Draw relative to bounds.width and bounds.height.

  3. Clone Arcs for closures - Animation values need Arc::clone() before the render closure.

  4. Push/pop transforms - Always pop what you push to avoid state leaks.

  5. Prefer elements when possible - Use div(), text() for standard UI; canvas for custom graphics.

Images & SVG

Blinc supports raster images and SVG graphics with flexible sizing and styling options.

Images

Basic Image

#![allow(unused)]
fn main() {
use blinc_layout::image::image;

image("path/to/photo.png")
    .w(200.0)
    .h(150.0)
}

Image from URL

#![allow(unused)]
fn main() {
image("https://example.com/image.jpg")
    .w(300.0)
    .h(200.0)
}

Object Fit

Control how the image fills its container:

#![allow(unused)]
fn main() {
image(src)
    .w(200.0)
    .h(200.0)
    .cover()      // Fill container, crop if needed (default)

image(src)
    .contain()    // Fit entirely, may letterbox

image(src)
    .fill()       // Stretch to fill exactly

image(src)
    .scale_down() // Scale down only if larger

image(src)
    .no_scale()   // No scaling, original size
}

Object Position

Control alignment within the container:

#![allow(unused)]
fn main() {
image(src)
    .cover()
    .center()         // Center (default)

image(src)
    .cover()
    .top_left()

image(src)
    .cover()
    .top_center()

image(src)
    .cover()
    .bottom_right()

// Custom position (0.0 to 1.0)
image(src)
    .cover()
    .position_xy(0.25, 0.75)
}

Image Filters

#![allow(unused)]
fn main() {
image(src)
    .w(200.0)
    .h(200.0)
    .grayscale(0.5)      // 0.0 = color, 1.0 = grayscale
    .sepia(0.3)          // Sepia tone
    .brightness(1.2)     // > 1.0 brighter, < 1.0 darker
    .contrast(1.1)       // > 1.0 more contrast
    .saturate(0.8)       // < 1.0 less saturated
    .hue_rotate(45.0)    // Rotate hue (degrees)
    .invert(0.2)         // Color inversion
    .blur(2.0)           // Blur radius
}

Lazy Loading

For content-heavy applications with many images (galleries, feeds, chat apps), lazy loading defers image loading until the image is visible in the viewport. This reduces initial memory usage and load time.

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
use std::time::Duration;

// Basic lazy loading
img("large-photo.jpg")
    .lazy()
    .w(300.0)
    .h(200.0)

// With placeholder color
img("photo.jpg")
    .lazy()
    .placeholder_color(Color::rgba(0.2, 0.2, 0.2, 1.0))
    .w(300.0)
    .h(200.0)

// With gradient placeholder using Brush
img("photo.jpg")
    .lazy()
    .placeholder_brush(Brush::Gradient(Gradient::linear(
        Point::new(0.0, 0.0),
        Point::new(1.0, 1.0),
        Color::rgba(0.4, 0.6, 1.0, 1.0),
        Color::rgba(0.6, 0.4, 1.0, 1.0),
    )))
    .w(300.0)
    .h(200.0)

// With thumbnail placeholder
img("large-photo.jpg")
    .lazy()
    .placeholder_image("thumbnail.jpg")
    .fade_in(Duration::from_millis(300))
    .w(300.0)
    .h(200.0)

// Skeleton loading animation
img("photo.jpg")
    .lazy()
    .skeleton()
    .fade_in(Duration::from_millis(250))
    .w(300.0)
    .h(200.0)

// Disable fade animation
img("photo.jpg")
    .lazy()
    .no_fade()
    .w(300.0)
    .h(200.0)
}

Loading Strategies

StrategyDescription
Eager (default)Load immediately when element is created
LazyLoad only when visible in viewport

Placeholder Types

PlaceholderDescription
NoneNo placeholder (blank until loaded)
Color(color)Solid color background
Brush(brush)Any brush (gradient, glass effect, etc.)
Image(url)Another image (e.g., low-res thumbnail, blur hash)
SkeletonShimmer loading animation

Emoji Images

Render emoji as images at arbitrary sizes using the system emoji font. Emoji images are automatically lazy-loaded for memory efficiency.

#![allow(unused)]
fn main() {
use blinc_layout::image::{emoji, emoji_sized};

// Default size (64px)
emoji("😀")

// Custom size
emoji_sized("🚀", 128.0)

// In a layout
div()
    .flex_row()
    .gap(8.0)
    .child(emoji_sized("👍", 32.0))
    .child(emoji_sized("🎉", 32.0))
    .child(emoji_sized("✨", 32.0))
}

Emoji images use the system color emoji font (Apple Color Emoji on macOS, Segoe UI Emoji on Windows, Noto Color Emoji on Linux).


SVG

Basic SVG

#![allow(unused)]
fn main() {
use blinc_layout::svg::svg;

svg("icons/menu.svg")
    .w(24.0)
    .h(24.0)
}

SVG with Tint

Apply a color tint to monochrome SVGs:

#![allow(unused)]
fn main() {
svg("icons/settings.svg")
    .w(24.0)
    .h(24.0)
    .tint(Color::WHITE)

svg("icons/error.svg")
    .w(20.0)
    .h(20.0)
    .tint(Color::rgba(0.9, 0.3, 0.3, 1.0))
}

SVG Sizing

#![allow(unused)]
fn main() {
// Fixed size
svg(src).w(32.0).h(32.0)

// Square shorthand
svg(src).square(24.0)

// Aspect ratio preserved
svg(src).w(48.0).h_auto()
}

Common Patterns

Avatar Image

#![allow(unused)]
fn main() {
fn avatar(url: &str, size: f32) -> impl ElementBuilder {
    image(url)
        .w(size)
        .h(size)
        .cover()
        .rounded_full()  // Circular
}
}

Icon Button

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn icon_button(ctx: &WindowedContext, icon_path: &str) -> impl ElementBuilder {
    // Use use_state_for with icon_path as key for reusable component
    let handle = ctx.use_state_for(icon_path, ButtonState::Idle);

    stateful(handle)
        .w(40.0)
        .h(40.0)
        .rounded(8.0)
        .flex_center()
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::TRANSPARENT,
                ButtonState::Hovered => Color::rgba(0.2, 0.2, 0.25, 1.0),
                ButtonState::Pressed => Color::rgba(0.15, 0.15, 0.2, 1.0),
                _ => Color::TRANSPARENT,
            };
            div.set_bg(bg);
        })
        .child(
            svg(icon_path)
                .w(20.0)
                .h(20.0)
                .tint(Color::WHITE)
        )
}
}

Image Card

#![allow(unused)]
fn main() {
fn image_card(image_url: &str, title: &str) -> impl ElementBuilder {
    div()
        .w(300.0)
        .rounded(12.0)
        .overflow_clip()
        .bg(Color::rgba(0.15, 0.15, 0.2, 1.0))
        .child(
            image(image_url)
                .w_full()
                .h(180.0)
                .cover()
        )
        .child(
            div()
                .p(16.0)
                .child(
                    text(title)
                        .size(18.0)
                        .weight(FontWeight::SemiBold)
                        .color(Color::WHITE)
                )
        )
}
}
#![allow(unused)]
fn main() {
fn gallery(images: &[&str]) -> impl ElementBuilder {
    div()
        .flex_row()
        .flex_wrap()
        .gap(8.0)
        .child(
            images.iter().map(|url| {
                image(*url)
                    .w(150.0)
                    .h(150.0)
                    .cover()
                    .rounded(8.0)
            })
        )
}
}

Placeholder with Fallback

#![allow(unused)]
fn main() {
fn image_with_placeholder(url: Option<&str>) -> impl ElementBuilder {
    match url {
        Some(src) => image(src)
            .w(200.0)
            .h(200.0)
            .cover()
            .rounded(8.0),
        None => div()
            .w(200.0)
            .h(200.0)
            .rounded(8.0)
            .bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
            .flex_center()
            .child(
                svg("icons/image-placeholder.svg")
                    .w(48.0)
                    .h(48.0)
                    .tint(Color::rgba(0.4, 0.4, 0.5, 1.0))
            ),
    }
}
}

Supported Formats

Images

  • PNG
  • JPEG
  • WebP
  • GIF (first frame)
  • BMP
  • ICO

SVG

  • Standard SVG 1.1
  • Path elements
  • Basic shapes (rect, circle, ellipse, line, polyline, polygon)
  • Transforms
  • Fill and stroke

Best Practices

  1. Set explicit dimensions - Images need width and height for layout.

  2. Use cover for photos - Fills container nicely without distortion.

  3. Use contain for diagrams - Ensures nothing is cropped.

  4. Tint icons - Use .tint() to match your color scheme.

  5. Use SVG for icons - Scales perfectly at any size.

  6. Optimize images - Use appropriate formats and compression for web.

  7. Use lazy loading for galleries - In scroll containers with many images, use .lazy() to reduce memory usage and improve initial load time.

  8. Use emoji images for large emoji - For emoji larger than ~24px, use emoji_sized() instead of text for crisp rendering.

Markdown Rendering

Blinc includes a built-in markdown renderer that converts CommonMark + GFM markdown to native layout elements.

Basic Usage

#![allow(unused)]
fn main() {
use blinc_layout::markdown::markdown;

// Render markdown to a Div
let content = markdown(r#"
Hello World

This is **bold** and *italic* text.

- List item 1
- List item 2
"#);

// Use in your layout
div()
    .flex_col()
    .child(content)
}

Themes

The renderer supports light and dark themes:

#![allow(unused)]
fn main() {
use blinc_layout::markdown::{markdown, markdown_light, markdown_with_config, MarkdownConfig};

// Dark theme (default) - for dark backgrounds
let dark_content = markdown("# Dark Theme");

// Light theme - for white/light backgrounds
let light_content = markdown_light("# Light Theme");

// Custom configuration
let custom = markdown_with_config("# Custom", MarkdownConfig {
    h1_size: 36.0,
    body_size: 16.0,
    ..MarkdownConfig::default()
});
}

Supported Elements

Text Formatting

MarkdownResult
**bold**bold text
*italic*italic text
~~strikethrough~~strikethrough text
`inline code`inline code
[link](url)clickable link
#![allow(unused)]
fn main() {
markdown(r#"
This is **bold**, *italic*, and ~~strikethrough~~ text.

Here's some `inline code` and a [link](https://example.com).
"#)
}

Headings

#![allow(unused)]
fn main() {
markdown(r#"
Heading 1
# Heading 2
## Heading 3
### Heading 4
#### Heading 5
##### Heading 6
"#)
}

Lists

Unordered lists:

#![allow(unused)]
fn main() {
markdown(r#"
- First item
- Second item
  - Nested item
  - Another nested
- Third item
"#)
}

Ordered lists:

#![allow(unused)]
fn main() {
markdown(r#"
1. First step
2. Second step
3. Third step
"#)
}

Task lists (GFM extension):

#![allow(unused)]
fn main() {
markdown(r#"
- [x] Completed task
- [ ] Pending task
- [x] Another done
"#)
}

Code Blocks

Fenced code blocks with optional language:

markdown(r#"
```rust
fn main() {
    println!("Hello, Blinc!");
}

“#)


Supported languages for syntax highlighting include Rust, Python, JavaScript, TypeScript, and more.

### Blockquotes

```rust
markdown(r#"
> This is a blockquote.
> It can span multiple lines.
>
> And have multiple paragraphs.
"#)

Tables (GFM)

#![allow(unused)]
fn main() {
markdown(r#"
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |
"#)
}

Horizontal Rules

#![allow(unused)]
fn main() {
markdown(r#"
Content above

---

Content below
"#)
}

Images

#![allow(unused)]
fn main() {
markdown(r#"
![Alt text](path/to/image.png)

![Remote image](https://example.com/photo.jpg)
"#)
}

Configuration

Customize the renderer with MarkdownConfig:

#![allow(unused)]
fn main() {
use blinc_layout::markdown::{markdown_with_config, MarkdownConfig};
use blinc_core::Color;

let config = MarkdownConfig {
    // Typography sizes
    h1_size: 32.0,
    h2_size: 28.0,
    h3_size: 24.0,
    h4_size: 20.0,
    h5_size: 18.0,
    h6_size: 16.0,
    body_size: 16.0,
    code_size: 14.0,

    // Colors
    text_color: Color::WHITE,
    heading_color: Color::WHITE,
    link_color: Color::rgba(0.4, 0.6, 1.0, 1.0),
    code_bg: Color::rgba(0.1, 0.1, 0.12, 1.0),
    code_text: Color::rgba(0.9, 0.6, 0.3, 1.0),

    // Spacing
    paragraph_spacing: 16.0,
    heading_margin_top: 24.0,
    heading_margin_bottom: 12.0,

    // Lists
    list_indent: 24.0,
    list_item_spacing: 4.0,

    ..Default::default()
};

let content = markdown_with_config("# Custom Styled", config);
}

Preset Themes

#![allow(unused)]
fn main() {
// Dark theme (default) - white text on dark backgrounds
let dark = MarkdownConfig::default();

// Light theme - dark text on light backgrounds
let light = MarkdownConfig::light();
}

Live Editor Example

A full markdown editor with live preview is available in the examples:

cargo run -p blinc_app --example markdown_demo --features windowed

This demonstrates:

  • TextArea for markdown source editing
  • Live preview with markdown_light()
  • Stateful reactive updates on text change

HTML Entities

The renderer automatically decodes HTML entities in text:

#![allow(unused)]
fn main() {
markdown(r#"
&copy; 2025 &mdash; All rights reserved

Temperature: 72&deg;F

Price: &euro;99.99
"#)
}

Common entities: &amp; (&), &lt; (<), &gt; (>), &quot; ("), &nbsp; (non-breaking space), &copy; (©), &trade; (), and many more.

Best Practices

  1. Use markdown_light() for light backgrounds - The default theme assumes dark backgrounds.

  2. Wrap in scroll for long content - Markdown can produce tall content:

    #![allow(unused)]
    fn main() {
    scroll()
        .h(600.0)
        .direction(ScrollDirection::Vertical)
        .child(markdown(long_content))
    }
  3. Set container width - Markdown content respects parent width:

    #![allow(unused)]
    fn main() {
    div()
        .w(800.0)
        .child(markdown(content))
    }
  4. Code blocks need height - For syntax highlighting to render properly, ensure the container has adequate height.

  5. Images need explicit dimensions - While images will render, they work best when the markdown container has width constraints.

Element Query API

The Element Query API provides programmatic access to elements in the UI tree, enabling imperative operations like scrolling, focusing, reading bounds, and triggering updates.

Overview

#![allow(unused)]
fn main() {
// Query an element by its string ID
let handle = ctx.query("my-element");

// Check if it exists
if handle.exists() {
    // Get computed bounds
    if let Some(bounds) = handle.bounds() {
        println!("Element at ({}, {}) size {}x{}",
            bounds.x, bounds.y, bounds.width, bounds.height);
    }

    // Scroll into view
    handle.scroll_into_view();

    // Focus the element
    handle.focus();
}
}

Assigning Element IDs

To query an element, it must have a string ID assigned via .id():

#![allow(unused)]
fn main() {
div()
    .id("sidebar")
    .w(250.0)
    .h_full()
    .child(
        div()
            .id("nav-item-home")
            .child(text("Home"))
    )
    .child(
        div()
            .id("nav-item-settings")
            .child(text("Settings"))
    )
}

IDs should be unique within your UI. Duplicate IDs will cause the last element to win.


ElementHandle API

Creation & Existence

#![allow(unused)]
fn main() {
// Get a handle - works even if element doesn't exist yet
let handle = ctx.query("my-element");

// Check if element exists in the tree
if handle.exists() {
    // Element is rendered
}

// Get the string ID
let id = handle.id();  // "my-element"
}

Bounds & Visibility

#![allow(unused)]
fn main() {
// Get computed bounds after layout
if let Some(bounds) = handle.bounds() {
    println!("Position: ({}, {})", bounds.x, bounds.y);
    println!("Size: {}x{}", bounds.width, bounds.height);
}

// Check if visible in viewport
if handle.is_visible() {
    // Element intersects with window viewport
}
}
#![allow(unused)]
fn main() {
// Scroll element into view (smooth scroll)
handle.scroll_into_view();

// Focus the element (for inputs, updates EventRouter)
handle.focus();

// Remove focus
handle.blur();

// Check focus state
if handle.is_focused() {
    // Element has keyboard focus
}
}

Tree Traversal

#![allow(unused)]
fn main() {
// Get parent element
if let Some(parent) = handle.parent() {
    println!("Parent ID: {}", parent.id());
}

// Iterate over ancestors (parent → grandparent → root)
for ancestor in handle.ancestors() {
    println!("Ancestor: {}", ancestor.id());
}
}

Triggering Updates

ElementHandle provides three levels of update granularity:

#![allow(unused)]
fn main() {
// 1. Visual-only update (fastest - skips layout)
// Use for: background color, opacity, shadows, transforms
handle.mark_visual_dirty(
    RenderProps::default().with_background(Color::RED.into())
);

// 2. Subtree rebuild with new children
// Use for: structural changes where you know the new content
handle.mark_dirty_subtree(
    div().child(text("New content"))
);

// 3. Full rebuild (fallback)
// Triggers complete UI rebuild, diffing determines actual changes
handle.mark_dirty();
}

Signal Integration

#![allow(unused)]
fn main() {
// Emit a signal to trigger reactive updates
// Only rebuilds stateful elements that depend on this signal
handle.emit_signal(my_signal_id);
}

On-Ready Callbacks

Register callbacks that fire once after an element’s first layout:

#![allow(unused)]
fn main() {
ctx.query("progress-bar").on_ready(|bounds| {
    // Element has been laid out
    println!("Progress bar width: {}", bounds.width);

    // Start an animation based on computed size
    progress_anim.lock().unwrap().set_target(bounds.width * 0.75);
});
}

On-ready callbacks:

  • Fire only once per element ID
  • Work even if element doesn’t exist yet (callback queued)
  • Survive tree rebuilds (tracked by string ID)

Querying in Event Handlers

Inside event handlers, use the global query() function to get an ElementHandle:

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

div()
    .on_click(|_| {
        // query() returns Option<ElementHandle> - None if element doesn't exist
        if let Some(handle) = query("my-element") {
            handle.scroll_into_view();
            handle.focus();
        }
    })
}

The query() function uses the global BlincContextState internally, so you don’t need to capture any context or registry in your closures.

For simple operations like scroll and focus without needing the full handle:

#![allow(unused)]
fn main() {
use blinc_core::BlincContextState;

div()
    .on_click(|_| {
        // Direct access for simple operations
        if let Some(ctx) = BlincContextState::try_get() {
            ctx.scroll_element_into_view("my-element");
            ctx.set_focus(Some("my-input"));
        }
    })
}

Use Cases

Scroll to Element on Action

#![allow(unused)]
fn main() {
fn scrollable_list(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .flex_col()
        .child(
            div()
                .on_click(|_| {
                    // Use query() to get handle and scroll
                    if let Some(handle) = query("list-bottom") {
                        handle.scroll_into_view();
                    }
                })
                .child(text("Jump to Bottom"))
        )
        .child(
            scroll()
                .h(400.0)
                .child(
                    div()
                        .flex_col()
                        .children((0..100).map(|i| {
                            div()
                                .id(format!("item-{}", i))
                                .child(text(format!("Item {}", i)))
                        }))
                        .child(
                            div().id("list-bottom").h(1.0)
                        )
                )
        )
}
}

Focus Management

#![allow(unused)]
fn main() {
fn login_form(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .flex_col()
        .gap(16.0)
        .child(
            text_input(ctx.use_state_keyed::<TextInputState>("username"))
                .id("username-input")
                .placeholder("Username")
        )
        .child(
            text_input(ctx.use_state_keyed::<TextInputState>("password"))
                .id("password-input")
                .placeholder("Password")
                .on_key_down(|evt| {
                    if evt.key_code == 9 && evt.shift {  // Shift+Tab
                        if let Some(handle) = query("username-input") {
                            handle.focus();
                        }
                    }
                })
        )
        .child(
            div()
                .on_click(|_| {
                    // Focus username on form reset
                    if let Some(handle) = query("username-input") {
                        handle.focus();
                    }
                })
                .child(text("Reset"))
        )
}
}

Measure Element After Layout

#![allow(unused)]
fn main() {
fn responsive_card(ctx: &WindowedContext) -> impl ElementBuilder {
    let card_width = ctx.use_signal(0.0f32);

    // Register callback to measure after layout
    ctx.query("adaptive-card").on_ready(move |bounds| {
        // on_ready callback has access to bounds directly
        println!("Card width: {}", bounds.width);
    });

    let width = ctx.get(card_width).unwrap_or(0.0);
    let columns = if width > 600.0 { 3 } else if width > 400.0 { 2 } else { 1 };

    div()
        .id("adaptive-card")
        .w_full()
        .flex_wrap()
        .children((0..9).map(|i| {
            div()
                .w(pct(100.0 / columns as f32))
                .child(text(format!("Item {}", i)))
        }))
}
}

Efficient Visual Updates

Use mark_visual_dirty for visual-only changes that don’t affect layout:

#![allow(unused)]
fn main() {
fn highlight_on_selection(ctx: &WindowedContext, selected_id: Option<&str>) -> impl ElementBuilder {
    let selected = selected_id.map(|s| s.to_string());

    div()
        .flex_col()
        .children(["item-a", "item-b", "item-c"].iter().map(|id| {
            let is_selected = selected.as_deref() == Some(*id);
            let id_string = id.to_string();

            div()
                .id(*id)
                .p(12.0)
                .bg(if is_selected {
                    Color::rgba(0.2, 0.5, 1.0, 0.3)
                } else {
                    Color::TRANSPARENT
                })
                .on_click(move |_| {
                    // Visual-only update - skips layout recomputation
                    if let Some(handle) = query(&id_string) {
                        handle.mark_visual_dirty(
                            RenderProps::default()
                                .with_background(Color::rgba(0.2, 0.5, 1.0, 0.3).into())
                        );
                    }
                })
                .child(text(*id))
        }))
}
}
#![allow(unused)]
fn main() {
fn carousel(ctx: &WindowedContext, items: &[String]) -> impl ElementBuilder {
    let current_index = ctx.use_signal(0usize);

    div()
        .flex_col()
        .child(
            scroll()
                .id("carousel-scroll")
                .w(300.0)
                .h(200.0)
                .scroll_x()
                .child(
                    div()
                        .flex_row()
                        .children(items.iter().enumerate().map(|(i, item)| {
                            div()
                                .id(format!("slide-{}", i))
                                .w(300.0)
                                .h(200.0)
                                .flex_center()
                                .child(text(item))
                        }))
                )
        )
        .child(
            div()
                .flex_row()
                .justify_center()
                .gap(8.0)
                .children((0..items.len()).map(|i| {
                    div()
                        .circle(8.0)
                        .bg(Color::WHITE.with_alpha(0.5))
                        .on_click(move |_| {
                            if let Some(handle) = query(&format!("slide-{}", i)) {
                                handle.scroll_into_view();
                            }
                        })
                }))
        )
}

---

# Performance Considerations

## Update Granularity

Choose the right update method based on what changed:

| Method | When to Use | Layout Cost |
|--------|-------------|-------------|
| `mark_visual_dirty(props)` | Background, opacity, shadow, transform | None (visual only) |
| `mark_dirty_subtree(div)` | Children structure changed | Subtree only |
| `mark_dirty()` | Unknown changes, fallback | Full rebuild |
| `emit_signal(id)` | Signal-based state change | Targeted stateful |

## Avoid Frequent Queries in Render

```rust
// Bad: Query in render function (called every frame)
fn bad_example(ctx: &WindowedContext) -> impl ElementBuilder {
    let bounds = ctx.query("my-element").bounds();  // Called every render!
    // ...
}

// Good: Query in event handler or on_ready
fn good_example(ctx: &WindowedContext) -> impl ElementBuilder {
    div()
        .on_click(|_| {
            // query() is designed for use in event handlers
            if let Some(handle) = query("my-element") {
                let bounds = handle.bounds();
                // Use bounds...
            }
        })
}
}

Use on_ready for Post-Layout Measurements

#![allow(unused)]
fn main() {
// The on_ready callback fires once after first layout
ctx.query("my-element").on_ready(|bounds| {
    // Safe to use bounds here - layout is complete
    setup_animations_based_on_size(bounds);
});
}

Best Practices

  1. Assign meaningful IDs - Use descriptive IDs like "sidebar", "submit-button", "user-avatar" rather than generic names.

  2. Prefer declarative state - Use signals and reactive state for most UI updates. Use ElementHandle for imperative operations like scroll-to and focus.

  3. Use visual-only updates - When only colors/opacity/shadows change, use mark_visual_dirty() to skip layout.

  4. Handle missing elements - Always check exists() or handle None from bounds() when the element might not be rendered.

  5. Avoid ID collisions - Each ID should be unique. Consider namespacing like "dialog-submit", "sidebar-nav-home".

  6. Use on_ready for measurements - Don’t assume bounds are available immediately. Use on_ready for post-layout operations.

Overlay System

Blinc provides an overlay system for modals, dialogs, toasts, and context menus.

Overview

Overlays render on top of the main UI and handle their own lifecycle. Access the overlay manager through the context:

#![allow(unused)]
fn main() {
ctx.overlay_manager()
}

Modals

Full-screen overlays with backdrop:

#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .modal()
    .title("Confirm Action")
    .content(|| {
        div()
            .flex_col()
            .gap(16.0)
            .child(text("Are you sure you want to proceed?"))
    })
    .show();
}
#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .modal()
    .title("Delete Item")
    .content(|| {
        text("This action cannot be undone.")
    })
    .primary_action("Delete", |_| {
        delete_item();
    })
    .secondary_action("Cancel", |_| {
        // Modal closes automatically
    })
    .show();
}

Dialogs

Centered dialogs with customizable content:

#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .dialog()
    .title("Settings")
    .content(|| build_settings_form())
    .primary_action("Save", |_| {
        save_settings();
    })
    .secondary_action("Cancel", |_| {})
    .show();
}

Dialog Sizing

#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .dialog()
    .width(600.0)
    .height(400.0)
    .title("Large Dialog")
    .content(|| content)
    .show();
}

Toasts

Brief notifications:

#![allow(unused)]
fn main() {
// Simple toast
ctx.overlay_manager()
    .toast("Item saved successfully!")
    .show();

// With duration
ctx.overlay_manager()
    .toast("Processing...")
    .duration(5000)  // 5 seconds
    .show();

// Positioned
ctx.overlay_manager()
    .toast("Copied to clipboard")
    .position(ToastPosition::BottomCenter)
    .show();
}

Toast Positions

#![allow(unused)]
fn main() {
ToastPosition::TopLeft
ToastPosition::TopCenter
ToastPosition::TopRight
ToastPosition::BottomLeft
ToastPosition::BottomCenter
ToastPosition::BottomRight
}

Context Menus

Right-click menus:

#![allow(unused)]
fn main() {
div()
    .on_context_menu(|evt| {
        ctx.overlay_manager()
            .context_menu()
            .item("Copy", || copy_to_clipboard())
            .item("Paste", || paste_from_clipboard())
            .separator()
            .item("Delete", || delete_selected())
            .show_at(evt.mouse_x, evt.mouse_y);
    })
}

Nested Menus

#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .context_menu()
    .item("Edit", || {})
    .submenu("Export", |menu| {
        menu.item("PNG", || export_png())
            .item("JPEG", || export_jpeg())
            .item("SVG", || export_svg())
    })
    .show_at(x, y);
}

Dismissing Overlays

Overlays close when:

  • User clicks outside (backdrop click)
  • Escape key is pressed
  • Action callback completes
  • Programmatically dismissed
#![allow(unused)]
fn main() {
let overlay_id = ctx.overlay_manager()
    .modal()
    .title("Loading...")
    .content(|| spinner())
    .show();

// Later, dismiss programmatically
ctx.overlay_manager().dismiss(overlay_id);
}

Custom Overlay Content

For full control, use a custom overlay:

#![allow(unused)]
fn main() {
ctx.overlay_manager()
    .custom(|| {
        stack()
            .w_full()
            .h_full()
            // Backdrop
            .child(
                div()
                    .w_full()
                    .h_full()
                    .bg(Color::rgba(0.0, 0.0, 0.0, 0.5))
            )
            // Content
            .child(
                div()
                    .absolute()
                    .inset(0.0)
                    .flex_center()
                    .child(my_custom_modal())
            )
    })
    .show();
}

Best Practices

  1. Use appropriate overlay type - Modal for blocking actions, toast for notifications, dialog for forms.

  2. Provide escape routes - Always include a way to close (cancel button, backdrop click).

  3. Keep toasts brief - Short messages that don’t require action.

  4. Position context menus near cursor - Use event coordinates for natural placement.

  5. Limit overlay nesting - Avoid opening overlays from within overlays.

Custom State Machines

For complex interactions beyond hover/press, define custom state types with the StateTransitions trait.

Defining Custom States

#![allow(unused)]
fn main() {
use blinc_layout::stateful::StateTransitions;
use blinc_core::events::event_types::*;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum PlayerState {
    #[default]
    Stopped,
    Playing,
    Paused,
}

impl StateTransitions for PlayerState {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            // Click cycles through states
            (PlayerState::Stopped, POINTER_UP) => Some(PlayerState::Playing),
            (PlayerState::Playing, POINTER_UP) => Some(PlayerState::Paused),
            (PlayerState::Paused, POINTER_UP) => Some(PlayerState::Playing),
            _ => None,
        }
    }
}
}

Using Custom States

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

fn player_button() -> impl ElementBuilder {
    stateful::<PlayerState>()
        .w(60.0)
        .h(60.0)
        .rounded_full()
        .flex_center()
        .on_state(|ctx| {
            let bg = match ctx.state() {
                PlayerState::Stopped => Color::rgba(0.3, 0.3, 0.35, 1.0),
                PlayerState::Playing => Color::rgba(0.2, 0.8, 0.4, 1.0),
                PlayerState::Paused => Color::rgba(0.9, 0.6, 0.2, 1.0),
            };
            div().bg(bg).child(text("▶").color(Color::WHITE))
        })
}
}

Event Types

Available event types for state transitions:

#![allow(unused)]
fn main() {
use blinc_core::events::event_types::*;

POINTER_ENTER    // Mouse enters element
POINTER_LEAVE    // Mouse leaves element
POINTER_DOWN     // Mouse button pressed
POINTER_UP       // Mouse button released (click)
POINTER_MOVE     // Mouse moved over element

KEY_DOWN         // Keyboard key pressed
KEY_UP           // Keyboard key released
TEXT_INPUT       // Character typed

FOCUS            // Element gained focus
BLUR             // Element lost focus

SCROLL           // Scroll event
DRAG             // Drag motion
DRAG_END         // Drag completed
}

Multi-Phase Interactions

Drag State Machine

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum DragPhase {
    #[default]
    Idle,
    Hovering,
    Pressing,
    Dragging,
}

impl StateTransitions for DragPhase {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            // Enter hover
            (DragPhase::Idle, POINTER_ENTER) => Some(DragPhase::Hovering),
            (DragPhase::Hovering, POINTER_LEAVE) => Some(DragPhase::Idle),

            // Start press
            (DragPhase::Hovering, POINTER_DOWN) => Some(DragPhase::Pressing),

            // Transition to drag on move while pressed
            (DragPhase::Pressing, DRAG) => Some(DragPhase::Dragging),

            // Release
            (DragPhase::Pressing, POINTER_UP) => Some(DragPhase::Hovering),
            (DragPhase::Dragging, DRAG_END) => Some(DragPhase::Idle),

            _ => None,
        }
    }
}

fn draggable_card() -> impl ElementBuilder {
    stateful::<DragPhase>()
        .w(120.0)
        .h(80.0)
        .rounded(8.0)
        .on_state(|ctx| {
            let (bg, cursor) = match ctx.state() {
                DragPhase::Idle => (Color::BLUE, "default"),
                DragPhase::Hovering => (Color::CYAN, "grab"),
                DragPhase::Pressing => (Color::YELLOW, "grabbing"),
                DragPhase::Dragging => (Color::GREEN, "grabbing"),
            };
            div().bg(bg).cursor(cursor)
        })
}
}

Focus State Machine

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum InputFocus {
    #[default]
    Idle,
    Hovered,
    Focused,
    FocusedHovered,
}

impl StateTransitions for InputFocus {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            // Hover transitions
            (InputFocus::Idle, POINTER_ENTER) => Some(InputFocus::Hovered),
            (InputFocus::Hovered, POINTER_LEAVE) => Some(InputFocus::Idle),
            (InputFocus::Focused, POINTER_ENTER) => Some(InputFocus::FocusedHovered),
            (InputFocus::FocusedHovered, POINTER_LEAVE) => Some(InputFocus::Focused),

            // Focus transitions
            (InputFocus::Idle, FOCUS) => Some(InputFocus::Focused),
            (InputFocus::Hovered, FOCUS) => Some(InputFocus::FocusedHovered),
            (InputFocus::Hovered, POINTER_UP) => Some(InputFocus::FocusedHovered),
            (InputFocus::Focused, BLUR) => Some(InputFocus::Idle),
            (InputFocus::FocusedHovered, BLUR) => Some(InputFocus::Hovered),

            _ => None,
        }
    }
}

fn focusable_input() -> impl ElementBuilder {
    stateful::<InputFocus>()
        .w(200.0)
        .h(40.0)
        .rounded(4.0)
        .on_state(|ctx| {
            let (border_color, border_width) = match ctx.state() {
                InputFocus::Idle => (Color::GRAY, 1.0),
                InputFocus::Hovered => (Color::LIGHT_GRAY, 1.0),
                InputFocus::Focused => (Color::BLUE, 2.0),
                InputFocus::FocusedHovered => (Color::BLUE, 2.0),
            };
            div().border(border_width, border_color)
        })
}
}

Combining with External State

Use .deps() to combine state machine transitions with external signals:

#![allow(unused)]
fn main() {
fn smart_button() -> impl ElementBuilder {
    let enabled = use_state_keyed("enabled", || true);

    stateful::<ButtonState>()
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .deps([enabled.signal_id()])
        .on_state(move |ctx| {
            let is_enabled = enabled.get();

            let bg = if !is_enabled {
                Color::rgba(0.2, 0.2, 0.25, 0.5)  // Disabled
            } else {
                match ctx.state() {
                    ButtonState::Idle => Color::rgba(0.3, 0.5, 0.9, 1.0),
                    ButtonState::Hovered => Color::rgba(0.4, 0.6, 1.0, 1.0),
                    ButtonState::Pressed => Color::rgba(0.2, 0.4, 0.8, 1.0),
                    _ => Color::rgba(0.3, 0.5, 0.9, 1.0),
                }
            };

            div().bg(bg).child(text("Submit").color(Color::WHITE))
        })
}
}

Accessing Dependencies via Context

Use ctx.dep() for cleaner dependency access:

#![allow(unused)]
fn main() {
fn counter_button(count: State<i32>) -> impl ElementBuilder {
    stateful::<ButtonState>()
        .deps([count.signal_id()])
        .on_state(|ctx| {
            // Access by index - no need to capture in closure
            let value: i32 = ctx.dep(0).unwrap_or_default();

            // Or get a State handle for reading/writing
            if let Some(count_state) = ctx.dep_as_state::<i32>(0) {
                // count_state.set(value + 1);
            }

            let bg = match ctx.state() {
                ButtonState::Hovered => Color::CYAN,
                _ => Color::BLUE,
            };

            div()
                .bg(bg)
                .child(text(&format!("Count: {}", value)))
        })
}
}

Using Scoped State

StateContext provides scoped signals and animated values:

#![allow(unused)]
fn main() {
fn interactive_counter() -> impl ElementBuilder {
    stateful::<ButtonState>()
        .on_state(|ctx| {
            // Scoped signal - persists across rebuilds
            let clicks = ctx.use_signal("clicks", || 0);

            // Scoped animated value with spring physics
            let scale = ctx.use_animated_value("scale", 1.0);

            // Animate based on state
            match ctx.state() {
                ButtonState::Pressed => {
                    scale.lock().unwrap().set_target(0.95);
                }
                _ => {
                    scale.lock().unwrap().set_target(1.0);
                }
            }

            let s = scale.lock().unwrap().get();

            div()
                .transform(Transform::scale(s, s))
                .child(text(&format!("Clicks: {}", clicks.get())))
                .on_click(move |_| {
                    clicks.update(|n| n + 1);
                })
        })
}
}

State Debugging

Log state transitions for debugging:

#![allow(unused)]
fn main() {
impl StateTransitions for MyState {
    fn on_event(&self, event: u32) -> Option<Self> {
        let next = match (self, event) {
            // ... transitions ...
            _ => None,
        };

        if let Some(ref new_state) = next {
            println!("State: {:?} -> {:?} (event: {})", self, new_state, event);
        }

        next
    }
}
}

Setting Initial State

Use .initial() when you need a non-default starting state:

#![allow(unused)]
fn main() {
fn initially_disabled_button(disabled: bool) -> impl ElementBuilder {
    stateful::<ButtonState>()
        .initial(if disabled { ButtonState::Disabled } else { ButtonState::Idle })
        .on_state(|ctx| {
            let bg = match ctx.state() {
                ButtonState::Disabled => Color::GRAY,
                ButtonState::Idle => Color::BLUE,
                ButtonState::Hovered => Color::CYAN,
                ButtonState::Pressed => Color::DARK_BLUE,
            };
            div().bg(bg)
        })
}
}

NoState for Dependency-Only Containers

When you only need dependency tracking without state transitions:

#![allow(unused)]
fn main() {
fn data_display(data: State<Vec<String>>) -> impl ElementBuilder {
    stateful::<NoState>()
        .deps([data.signal_id()])
        .on_state(|ctx| {
            // Access data via context
            let items: Vec<String> = ctx.dep(0).unwrap_or_default();

            div()
                .flex_col()
                .gap(4.0)
                .children(items.iter().map(|item| {
                    div().child(text(item))
                }))
        })
}
}

Best Practices

  1. Keep states minimal - Only include states you need to distinguish visually.

  2. Handle all paths - Consider every possible event in each state.

  3. Use descriptive names - State names should clearly indicate the UI appearance.

  4. Return None for no-ops - If an event doesn’t cause a transition, return None.

  5. Test transitions - Verify all state paths work as expected.

  6. Use .deps() for external dependencies - When combining with signals.

  7. Use ctx.dep() over closures - Cleaner access to dependency values.

  8. Implement Default - Mark the default state with #[default] attribute.

  9. Use scoped signals - ctx.use_signal() for state local to the stateful.

  10. Use animated values - ctx.use_animated_value() for smooth transitions.

Performance Tips

Blinc is designed for high performance, but following these guidelines ensures your UI stays smooth.

Use Stateful for Visual States

Do: Use stateful(handle) for hover, press, and focus effects:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::stateful;

fn hover_button(ctx: &WindowedContext) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        .px(16.0)
        .py(8.0)
        .rounded(8.0)
        .on_state(|state, div| {
            let bg = match state {
                ButtonState::Idle => Color::RED,
                ButtonState::Hovered => Color::BLUE,
                _ => Color::RED,
            };
            div.set_bg(bg);
        })
        .child(text("Hover me").color(Color::WHITE))
}
}

Don’t: Use if-else or signals for visual-only state changes:

#![allow(unused)]
fn main() {
// AVOID - causes full tree rebuild on every hover
let is_hovered = ctx.use_signal(false);
div()
    .on_hover_enter(move |_| ctx.set(is_hovered, true))
    .on_hover_leave(move |_| ctx.set(is_hovered, false))
    .bg(if ctx.get(is_hovered).unwrap_or(false) {
        Color::BLUE
    } else {
        Color::RED
    })
}

The stateful(handle) pattern only updates the affected element, while signals with if-else rebuild the entire UI tree.

Minimize Signal Updates

Signals trigger UI rebuilds. Batch related updates:

#![allow(unused)]
fn main() {
// Good - single rebuild
ctx.batch(|g| {
    g.set(x, 10);
    g.set(y, 20);
    g.set(z, 30);
});

// Avoid - three rebuilds
ctx.set(x, 10);
ctx.set(y, 20);
ctx.set(z, 30);
}

Use Keyed State Appropriately

Keyed state persists across rebuilds. Use it for:

  • Form input values
  • Toggle states
  • Selected items

Don’t overuse - each key adds memory overhead.

Efficient List Rendering

For large lists, consider:

  1. Virtualization - Only render visible items
  2. Stable keys - Use consistent identifiers for list items
  3. Memoization - Cache expensive computations
#![allow(unused)]
fn main() {
// For very long lists, wrap in scroll and limit rendered items
scroll()
    .h(500.0)
    .child(
        div()
            .flex_col()
            .child(
                visible_items.iter().map(|item| render_item(item))
            )
    )
}

Canvas Optimization

For custom drawing:

  1. Minimize state reads - Read animated values once, not per-shape
  2. Use transforms - Push/pop transforms instead of recalculating positions
  3. Batch similar draws - Group shapes by color/brush
#![allow(unused)]
fn main() {
canvas(move |ctx, bounds| {
    // Read once
    let angle = timeline.lock().unwrap().get(entry_id).unwrap_or(0.0);

    // Use transform for rotation
    ctx.push_transform(Transform::rotate(angle));
    // ... draw ...
    ctx.pop_transform();
})
}

Animation Performance

  1. Use appropriate spring stiffness - Stiffer springs settle faster
  2. Limit simultaneous animations - Too many can cause jank
  3. Use timelines for loops - More efficient than many spring values
#![allow(unused)]
fn main() {
// Good - single timeline with multiple entries
let timeline = ctx.use_animated_timeline();
let (x, y, scale) = timeline.lock().unwrap().configure(|t| {
    (t.add(0, 1000, 0.0, 100.0),
     t.add(0, 1000, 0.0, 100.0),
     t.add(0, 500, 1.0, 1.5))
});
}

Memory Management

  1. Clone Arc, not data - Use Arc::clone() for shared state
  2. Drop unused state - Clean up keyed state when no longer needed
  3. Avoid closures capturing large data - Clone only what’s needed
#![allow(unused)]
fn main() {
// Good - clone the Arc, not the data
let data = Arc::clone(&shared_data);

// Avoid - captures entire struct
let large_struct = expensive_struct.clone();
div().on_click(move |_| use_struct(&large_struct))
}

Lazy Loading for Images

For applications with many images (galleries, feeds, chat), use lazy loading to defer loading until images are visible:

#![allow(unused)]
fn main() {
// Images in a scrollable gallery
scroll()
    .h(600.0)
    .child(
        div()
            .flex_row()
            .flex_wrap()
            .gap(8.0)
            .child(
                image_urls.iter().map(|url| {
                    img(*url)
                        .lazy()  // Only loads when scrolled into view
                        .placeholder_color(Color::rgba(0.2, 0.2, 0.2, 1.0))
                        .w(150.0)
                        .h(150.0)
                        .cover()
                })
            )
    )
}

Benefits:

  • Reduced initial memory - Only visible images are loaded
  • Faster startup - No waiting for off-screen images
  • Automatic cleanup - LRU cache evicts old images

Emoji images (emoji() and emoji_sized()) are automatically lazy-loaded. The ~180MB system emoji font is only loaded when emoji characters actually appear on screen.

Debugging Performance

Enable tracing to identify bottlenecks:

#![allow(unused)]
fn main() {
tracing_subscriber::fmt()
    .with_env_filter("blinc_layout=debug")
    .init();
}

Look for:

  • Frequent tree rebuilds
  • Long frame times
  • Excessive state updates

Summary

DoDon’t
Use Stateful for hover/pressUse signals for visual-only changes
Batch signal updatesUpdate signals one at a time
Use Arc::clone()Clone large data into closures
Use timelines for loopsCreate many spring values
Read animated values onceRead repeatedly in draw loops

Architecture Overview

Blinc is a high-performance UI framework built from the ground up for GPU-accelerated rendering without virtual DOM overhead. This chapter explains how the major systems work together.

Design Philosophy

Blinc follows several key principles:

  1. Fine-grained Reactivity - Signal-based state management with automatic dependency tracking eliminates the need for virtual DOM diffing
  2. Layout as Separate Concern - Tree structure is independent from visual properties, enabling visual-only updates without layout recomputation
  3. GPU-First Rendering - SDF shaders provide resolution-independent, smooth rendering with glass/blur effects
  4. Incremental Updates - Hash-based diffing with change categories minimizes recomputation
  5. Background Thread Animations - Animation scheduler runs independently from the UI thread

System Architecture

┌─────────────────────────────────────────────────────────────┐
│  WindowedApp Event Loop (Platform abstraction)              │
├─────────────────────────────────────────────────────────────┤
│ • Receives pointer, keyboard, lifecycle events              │
│ • Routes through EventRouter -> StateMachines               │
│ • Triggers reactive signal updates                          │
│ • Checks signal dependencies for rebuilds                   │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│  ReactiveGraph & Stateful Element Updates                   │
├─────────────────────────────────────────────────────────────┤
│ • Signals change -> Effects run -> Rebuilds queued          │
│ • Stateful elements transition -> Subtree rebuild queued    │
│ • Diff algorithm determines change categories               │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│  RenderTree Update (Incremental)                            │
├─────────────────────────────────────────────────────────────┤
│ • incremental_update() compares hashes                      │
│ • VisualOnly: apply prop updates only                       │
│ • ChildrenChanged: rebuild subtrees + mark layout dirty     │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│  Layout Computation (Taffy Flexbox)                         │
├─────────────────────────────────────────────────────────────┤
│ • compute_layout() on dirty nodes only                      │
│ • Returns (x, y, width, height) for all nodes               │
│ • Cached in LayoutTree                                      │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│  Animation Scheduler (Background Thread @ 120fps)           │
├─────────────────────────────────────────────────────────────┤
│ • Ticks springs, keyframes, timelines                       │
│ • Stores current values in AnimatedValue                    │
│ • Sets needs_redraw flag, wakes main thread                 │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│  GPU Rendering (DrawContext -> GpuRenderer)                 │
├─────────────────────────────────────────────────────────────┤
│ • RenderTree traversal, samples animation values            │
│ • Emits SDF primitives to batches                           │
│ • Multi-pass rendering: Background -> Glass -> Foreground  │
└─────────────────────────────────────────────────────────────┘

Core Crates

CratePurpose
blinc_coreReactive signals, FSM, core types, event system
blinc_layoutElement builders, Taffy integration, diff system, stateful elements
blinc_animationSpring physics, keyframe timelines, animation scheduler
blinc_gpuwgpu renderer, SDF shaders, glass effects, text rendering
blinc_textFont loading, glyph shaping, text atlas
blinc_appWindowedApp, render context, platform integration

Why No Virtual DOM?

Traditional frameworks (React, Vue) use a virtual DOM to diff the entire component tree on every state change. This has overhead:

  1. Creating VDOM objects for every render
  2. Diffing the full tree to find changes
  3. Patching the real DOM with changes

Blinc avoids this with:

  1. Fine-grained signals - Only dependent code re-runs when state changes
  2. Stateful elements - UI state managed at the element level, not rebuilt from scratch
  3. Hash-based diffing - Quick equality checks without deep comparison
  4. Change categories - Visual vs layout vs structural changes handled differently

The result: updates proportional to what changed, not to tree size.


Chapter Contents

GPU Rendering

Blinc uses GPU-accelerated rendering via wgpu with Signed Distance Field (SDF) shaders for smooth, resolution-independent UI primitives.

Signed Distance Fields

An SDF shader computes the signed distance from each pixel to the geometry’s edge:

  • Negative distance: pixel is inside the shape
  • Positive distance: pixel is outside the shape
  • Zero: pixel is exactly on the edge

This enables:

  • Smooth anti-aliasing at any scale
  • Per-corner rounded rectangles
  • Soft shadows with Gaussian blur
  • Sharp text at any zoom level

GPU Primitive Structure

All UI elements are converted to GpuPrimitive structs (192 bytes each):

#![allow(unused)]
fn main() {
struct GpuPrimitive {
    // Geometry
    bounds: [f32; 4],           // x, y, width, height
    corner_radii: [f32; 4],     // top-left, top-right, bottom-right, bottom-left

    // Fill
    fill_type: FillType,        // Solid, LinearGradient, RadialGradient
    fill_colors: [Color; 4],    // gradient stops or solid color

    // Border
    border_width: f32,
    border_color: Color,

    // Shadow
    shadow_offset: [f32; 2],
    shadow_blur: f32,
    shadow_color: Color,

    // Clipping
    clip_bounds: [f32; 4],
    clip_radii: [f32; 4],
}
}

Primitive Types

TypeDescription
RectRounded rectangle with per-corner radius
CirclePerfect circle
EllipseAxis-aligned ellipse
ShadowDrop shadow with Gaussian blur
InnerShadowInset shadow
TextGlyph sampled from texture atlas

Fill Types

TypeDescription
SolidSingle color fill
LinearGradientGradient between two points
RadialGradientGradient radiating from center

Rendering Pipeline

Rendering happens in multiple passes for proper layering:

1. Background Pass (non-glass elements)
   └── SDF shader for rects, circles, shadows

2. Glass Pass (frosted glass elements)
   └── Glass shader samples backbuffer + blur

3. Foreground Pass (text, overlays)
   └── Text shader with glyph atlases
   └── SDF shader for overlays

4. Composite Pass
   └── Blend layers + MSAA resolve

SDF Shader

The main SDF shader handles rectangles with:

// Signed distance to rounded rectangle
fn sdf_rounded_rect(p: vec2f, size: vec2f, radii: vec4f) -> f32 {
    // Select corner radius based on quadrant
    let radius = select_corner_radius(p, radii);

    // Compute distance to rounded corner
    let q = abs(p) - size + radius;
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2f(0.0))) - radius;
}

Anti-aliasing uses the SDF gradient:

// Smooth alpha based on distance
let alpha = 1.0 - smoothstep(-0.5, 0.5, distance);

Shadow Rendering

Soft shadows use Gaussian blur approximated with the error function:

fn shadow_alpha(distance: f32, blur_radius: f32) -> f32 {
    // Error function approximation for Gaussian
    let t = distance / (blur_radius * 0.5);
    return 0.5 * (1.0 - erf(t));
}

Glass Effects

Blinc implements Apple-style frosted glass (vibrancy) with backdrop blur:

Glass Types

TypeBlurSaturationUse Case
UltraThinLightHighSubtle overlays
ThinMediumMediumSidebars
RegularStandardStandardModals, cards
ThickHeavyLowHeaders
ChromeVery heavyLowWindow chrome

Glass Shader

The glass shader:

  1. Samples backbuffer - Reads rendered content behind the glass
  2. Applies Gaussian blur - Multi-tap sampling with weights
  3. Adjusts saturation - Controls color vibrancy
  4. Adds tint color - Overlays glass tint
  5. Applies noise grain - Frosted texture effect
  6. Rim bending - Light refraction at edges
// Simplified glass computation
fn glass_color(uv: vec2f, glass_type: u32) -> vec4f {
    // Sample and blur background
    var color = blur_sample(backbuffer, uv, blur_radius);

    // Adjust saturation
    color = saturate_color(color, saturation);

    // Apply tint
    color = mix(color, tint_color, tint_strength);

    // Add noise grain
    color += noise(uv) * grain_amount;

    return color;
}

Three-Layer Rendering

Glass requires separating background from foreground:

┌─────────────────────────────────┐
│  Foreground Layer               │  Text, icons on glass
├─────────────────────────────────┤
│  Glass Layer                    │  Frosted glass with blur
├─────────────────────────────────┤
│  Background Layer               │  Content behind glass
└─────────────────────────────────┘

The renderer:

  1. Renders background to backbuffer texture
  2. Renders glass elements sampling the backbuffer
  3. Renders foreground elements on top
  4. Composites all layers

Text Rendering

Text uses a separate glyph atlas system:

  1. Font Loading - Parses TTF/OTF via rustybuzz
  2. Glyph Shaping - HarfBuzz-compatible shaping for complex scripts
  3. Atlas Generation - Rasterizes glyphs to texture atlas
  4. SDF Text - Stores distance field for each glyph
  5. Rendering - Samples atlas with color spans
#![allow(unused)]
fn main() {
// Text rendering produces glyph primitives
for glyph in shaped_text.glyphs() {
    emit_text_primitive(GpuPrimitive {
        primitive_type: PrimitiveType::Text,
        bounds: glyph.bounds,
        texture_coords: glyph.atlas_uv,
        color: text_color,
        // ...
    });
}
}

Batching & Instancing

Primitives are batched by type and rendered with GPU instancing:

#![allow(unused)]
fn main() {
// All rects in one draw call
batch.add_primitive(rect1);
batch.add_primitive(rect2);
batch.add_primitive(rect3);
// Single instanced draw call for all rects
}

Benefits:

  • Minimal CPU-GPU communication
  • Efficient use of GPU parallelism
  • Scales to thousands of primitives

MSAA Support

Blinc supports multi-sample anti-aliasing:

Sample CountQualityPerformance
1xBaselineFastest
2xImproved edgesSlight cost
4xGood qualityModerate cost
8xHigh qualityHigher cost

MSAA is resolved in the final composite pass.

DrawContext Trait

The bridge between layout and GPU is the DrawContext trait:

#![allow(unused)]
fn main() {
trait DrawContext {
    fn draw_rect(&mut self, bounds: Rect, props: &RenderProps);
    fn draw_text(&mut self, text: &ShapedText, position: Point);
    fn draw_shadow(&mut self, bounds: Rect, shadow: &Shadow);
    fn push_clip(&mut self, bounds: Rect, radii: [f32; 4]);
    fn pop_clip(&mut self);
    fn push_transform(&mut self, transform: Transform);
    fn pop_transform(&mut self);
}
}

The RenderTree traverses nodes and calls DrawContext methods, which accumulate GPU primitives for the render passes.

Reactive State System

Blinc implements a push-pull hybrid reactive system for fine-grained state management without virtual DOM overhead. This is inspired by modern reactive frameworks like Leptos and SolidJS.

Core Concepts

Signals

A Signal<T> is a reactive container for a value. When the value changes, all dependent computations automatically update.

#![allow(unused)]
fn main() {
// Create a signal
let count = ctx.use_state_keyed("count", || 0i32);

// Read the current value
let value = count.get();

// Update the value
count.set(5);
count.update(|v| v + 1);
}

Signal IDs

Signals are identified by SignalId, a cheap-to-copy handle:

#![allow(unused)]
fn main() {
// Get the signal's ID for dependency tracking
let id = count.signal_id();
}

Automatic Dependency Tracking

When code accesses a signal’s value, the dependency is automatically recorded:

#![allow(unused)]
fn main() {
// Stateful element with signal dependency
stateful(handle)
    .deps(&[count.signal_id()])  // Declare dependency
    .on_state(move |state, div| {
        // Reading count.get() here is tracked
        let value = count.get();
        div.set_bg(color_for_value(value));
    })
}

When count changes, only elements depending on it re-run their callbacks.

ReactiveGraph Internals

The ReactiveGraph manages all reactive state:

#![allow(unused)]
fn main() {
struct ReactiveGraph {
    signals: SlotMap<SignalId, SignalNode>,
    deriveds: SlotMap<DerivedId, DerivedNode>,
    effects: SlotMap<EffectId, EffectNode>,
    pending_effects: Vec<EffectId>,
    batch_depth: u32,
}
}

Data Structures

TypePurpose
SignalNodeStores value + list of subscribers
DerivedNodeCached computed value + dirty flag
EffectNodeSide-effect function + dependencies

Subscription Flow

Signal.set(new_value)
    │
    ├── Mark all subscribers dirty
    │
    ├── Propagate to derived values
    │
    └── Queue effects for execution

Derived Values

Derived values compute from other signals and cache the result:

#![allow(unused)]
fn main() {
// Conceptual - derived values
let doubled = derived(|| count.get() * 2);

// Value is cached until count changes
let value = doubled.get();  // Computed once
let again = doubled.get();  // Returns cached value
}

Lazy Evaluation

Derived values only compute when:

  1. First accessed after creation
  2. Accessed after a dependency changed
  3. Their value is explicitly needed

This prevents wasted computation for unused values.

Effects

Effects are side-effects that run when dependencies change:

#![allow(unused)]
fn main() {
// Conceptual - effects
effect(|| {
    let value = count.get();  // Tracks dependency on count
    println!("Count changed to {}", value);
});
}

Effects are:

  • Queued when dependencies change
  • Executed after the current batch completes
  • Run in topological order (respecting dependency depth)

Batching

Multiple signal updates can be batched to prevent redundant recomputation:

#![allow(unused)]
fn main() {
// Without batching: 3 separate updates, 3 effect runs
count.set(1);
name.set("Alice");
enabled.set(true);

// With batching: 1 combined update, 1 effect run
ctx.batch(|g| {
    g.set(count, 1);
    g.set(name, "Alice");
    g.set(enabled, true);
});
}

How Batching Works

  1. batch_start() increments batch depth counter
  2. Signal updates mark subscribers dirty but don’t run effects
  3. batch_end() decrements counter
  4. When counter reaches 0, all pending effects execute

Integration with Stateful Elements

The reactive system integrates with stateful elements via .deps():

#![allow(unused)]
fn main() {
fn counter_display(ctx: &WindowedContext, count: State<i32>) -> impl ElementBuilder {
    let handle = ctx.use_state(ButtonState::Idle);

    stateful(handle)
        // Declare signal dependencies
        .deps(&[count.signal_id()])
        .on_state(move |_state, container| {
            // This callback re-runs when count changes
            let current = count.get();
            container.merge(
                div().child(text(&format!("{}", current)).color(Color::WHITE))
            );
        })
}
}

Dependency Registry

The system maintains a registry of signal dependencies:

#![allow(unused)]
fn main() {
// Internal tracking
struct DependencyEntry {
    signal_ids: Vec<SignalId>,
    node_id: LayoutNodeId,
    refresh_callback: Box<dyn Fn()>,
}
}

When signals change, the registry triggers rebuilds for dependent nodes.

Performance Characteristics

O(1) Signal Access

Reading a signal is a simple memory lookup:

#![allow(unused)]
fn main() {
fn get(&self) -> T {
    self.value.clone()  // Direct access, no computation
}
}

O(subscribers) Propagation

Updates only touch direct subscribers:

#![allow(unused)]
fn main() {
fn set(&mut self, value: T) {
    self.value = value;
    for subscriber in &self.subscribers {
        subscriber.mark_dirty();
    }
}
}

Minimal Allocations

  • SignalId is a 64-bit handle (Copy)
  • Subscriber lists use SmallVec<[_; 4]> (inline for small counts)
  • SlotMap provides dense storage without gaps

Comparison to Virtual DOM

AspectVirtual DOMBlinc Reactive
State changeRebuild entire componentUpdate only affected nodes
DiffingO(tree size)O(1) per signal
MemoryVDOM objects per renderFixed signal storage
Dependency trackingManual (useEffect deps)Automatic

Best Practices

  1. Use keyed state for persistence - use_state_keyed("key", || value) survives rebuilds

  2. Batch related updates - Group multiple signal changes to avoid redundant work

  3. Declare dependencies explicitly - Use .deps() for stateful elements that read signals

  4. Prefer stateful for visual changes - Use stateful elements instead of signals for hover/press effects

  5. Keep signals granular - Fine-grained signals enable more precise updates

Layout & Diff System

Blinc separates layout computation from visual rendering, enabling incremental updates without full tree rebuilds.

Layout System

Taffy Integration

Blinc uses Taffy for flexbox layout computation. Taffy is a high-performance layout engine that implements the CSS Flexbox specification.

#![allow(unused)]
fn main() {
// LayoutTree wraps Taffy
struct LayoutTree {
    taffy: TaffyTree,
    node_map: HashMap<LayoutNodeId, TaffyNodeId>,
    reverse_map: HashMap<TaffyNodeId, LayoutNodeId>,
}
}

Layout Properties vs Visual Properties

Layout and visual properties are handled separately:

Layout PropertiesVisual Properties
width, heightbackground
padding, marginborder_color
flex_directionshadow
justify_contentopacity
align_itemstransform
gaprounded

This separation allows visual-only updates without layout recomputation.

RenderTree Structure

#![allow(unused)]
fn main() {
struct RenderTree {
    layout_tree: LayoutTree,           // Taffy wrapper
    render_props: HashMap<NodeId, RenderProps>,  // Visual properties
    dirty_nodes: HashSet<NodeId>,      // Nodes needing update
    scroll_state: HashMap<NodeId, ScrollState>,
    motion_bindings: HashMap<NodeId, MotionBinding>,
}
}

Layout Computation

Layout is computed on-demand when the tree structure changes:

#![allow(unused)]
fn main() {
// Compute layout for the tree
render_tree.compute_layout(root_id, AvailableSpace {
    width: window_width,
    height: window_height,
});

// Get computed bounds for a node
let layout = render_tree.get_layout(node_id);
// Returns: Layout { x, y, width, height }
}

Diff System

The diff system determines the minimum changes needed when UI structure updates.

DivHash - Content-Based Identity

Every element has a hash computed from its properties:

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct DivHash(u64);

impl Div {
    // Hash of this element only (excludes children)
    fn compute_hash(&self) -> DivHash {
        let mut hasher = DefaultHasher::new();
        self.width.hash(&mut hasher);
        self.height.hash(&mut hasher);
        self.background.hash(&mut hasher);
        // ... all properties
        DivHash(hasher.finish())
    }

    // Hash including entire subtree
    fn compute_tree_hash(&self) -> DivHash {
        let mut hasher = DefaultHasher::new();
        self.compute_hash().hash(&mut hasher);
        for child in &self.children {
            child.compute_tree_hash().hash(&mut hasher);
        }
        DivHash(hasher.finish())
    }
}
}

Change Categories

Changes are classified to determine the update strategy:

#![allow(unused)]
fn main() {
struct ChangeCategory {
    layout: bool,      // Requires layout recomputation
    visual: bool,      // Only visual properties changed
    children: bool,    // Children added/removed/reordered
    handlers: bool,    // Event handlers changed
}
}
CategoryExample ChangesAction
Visual onlybg, opacity, shadowUpdate RenderProps
Layoutwidth, padding, gapRecompute layout
ChildrenAdd/remove childRebuild subtree
HandlersEvent callback changedUpdate handlers

Child Diffing Algorithm

When children change, the diff algorithm matches old and new children:

#![allow(unused)]
fn main() {
enum ChildDiff {
    Unchanged { index: usize },           // Same content, same position
    Moved { old_idx: usize, new_idx: usize },  // Same content, moved
    Modified { old_idx: usize, new_idx: usize, diff: Box<DiffResult> },
    Added { index: usize },               // New child
    Removed { index: usize },             // Old child gone
}
}

Matching Strategy

  1. Compute hashes for all old and new children
  2. Build hash map of old children by hash
  3. Match new children to old by hash lookup
  4. Detect moves when hash matches at different position
  5. Classify remaining as added or removed
  6. Merge same-position changes as modifications
#![allow(unused)]
fn main() {
fn diff_children(old: &[Div], new: &[Div]) -> Vec<ChildDiff> {
    let old_hashes: Vec<_> = old.iter().map(|c| c.compute_tree_hash()).collect();
    let new_hashes: Vec<_> = new.iter().map(|c| c.compute_tree_hash()).collect();

    let mut old_by_hash: HashMap<DivHash, usize> = old_hashes
        .iter()
        .enumerate()
        .map(|(i, h)| (*h, i))
        .collect();

    let mut diffs = Vec::new();

    for (new_idx, new_hash) in new_hashes.iter().enumerate() {
        if let Some(old_idx) = old_by_hash.remove(new_hash) {
            if old_idx == new_idx {
                diffs.push(ChildDiff::Unchanged { index: new_idx });
            } else {
                diffs.push(ChildDiff::Moved { old_idx, new_idx });
            }
        } else {
            diffs.push(ChildDiff::Added { index: new_idx });
        }
    }

    // Remaining old children were removed
    for (old_idx, _) in old_by_hash {
        diffs.push(ChildDiff::Removed { index: old_idx });
    }

    diffs
}
}

Incremental Updates

Update Result

The incremental update process returns what changed:

#![allow(unused)]
fn main() {
enum UpdateResult {
    NoChanges,                    // Nothing to do
    VisualOnly(Vec<PropUpdate>),  // Apply prop updates only
    LayoutChanged,                // Props + recompute layout
    ChildrenChanged,              // Rebuild subtrees + layout
}
}

Reconciliation

The reconcile function determines actions from diffs:

#![allow(unused)]
fn main() {
struct ReconcileActions {
    prop_updates: Vec<(NodeId, RenderProps)>,  // Visual updates
    subtree_rebuild_ids: Vec<NodeId>,          // Nodes to rebuild
    needs_layout: bool,                        // Layout recomputation needed
}

fn reconcile(old: &Div, new: &Div) -> ReconcileActions {
    let changes = categorize_changes(old, new);

    let mut actions = ReconcileActions::default();

    if changes.visual && !changes.layout && !changes.children {
        // Visual-only: just update props
        actions.prop_updates.push((node_id, new.to_render_props()));
    }

    if changes.layout {
        actions.needs_layout = true;
    }

    if changes.children {
        actions.subtree_rebuild_ids.push(node_id);
        actions.needs_layout = true;
    }

    actions
}
}

Update Flow

incremental_update(root, new_element)
    │
    ├── Compare hashes: old_tree_hash vs new_tree_hash
    │   └── Same? Return NoChanges
    │
    ├── Compare element hashes: old_hash vs new_hash
    │   └── Same? Children might have changed
    │
    ├── Categorize changes
    │   ├── Visual only? Queue prop updates
    │   ├── Layout changed? Mark layout dirty
    │   └── Children changed? Diff children recursively
    │
    └── Return UpdateResult

Pending Update Queues

The system uses global queues for deferred updates:

#![allow(unused)]
fn main() {
// Global pending updates (thread-local)
static PENDING_PROP_UPDATES: RefCell<Vec<(NodeId, RenderProps)>>;
static PENDING_SUBTREE_REBUILDS: RefCell<Vec<NodeId>>;
static NEEDS_REDRAW: AtomicBool;
}

Queue Processing

The windowed app processes queues each frame:

#![allow(unused)]
fn main() {
fn process_pending_updates(&mut self) {
    // Apply prop updates (no layout needed)
    for (node_id, props) in drain_prop_updates() {
        self.render_tree.update_props(node_id, props);
    }

    // Rebuild dirty subtrees
    for node_id in drain_subtree_rebuilds() {
        self.rebuild_subtree(node_id);
    }

    // Recompute layout if needed
    if self.layout_dirty {
        self.render_tree.compute_layout(self.root_id, self.available_space);
        self.layout_dirty = false;
    }

    // Trigger redraw if visual changes occurred
    if NEEDS_REDRAW.swap(false, Ordering::SeqCst) {
        self.request_redraw();
    }
}
}

Performance Benefits

Hash-Based Comparison

  • O(1) per element to compute hash
  • O(1) equality check via hash comparison
  • No deep property-by-property comparison needed

Child Matching

  • O(n) to build hash map
  • O(n) to match children
  • Detects moves without position-based assumptions

Minimal Recomputation

ScenarioWhat Runs
Hover color changeUpdate 1 RenderProps
Text content changeRebuild 1 text node
Add item to listInsert node, layout affected subtree
Reorder listMove nodes, minimal layout

Layout Caching

  • Layout only recomputes when structure/dimensions change
  • Visual-only changes skip layout entirely
  • Taffy caches intermediate results

Animation System

Blinc provides a multi-layered animation system with physics-based springs and timed keyframe animations.

Spring Physics

Springs are the foundation of Blinc’s animation system, providing natural, interruptible motion.

Spring Model

A spring follows Hooke’s law with damping:

Force = -k * (position - target) - d * velocity

where:
  k = stiffness (spring tightness)
  d = damping (friction)

Spring Structure

#![allow(unused)]
fn main() {
struct Spring {
    value: f32,      // Current position
    velocity: f32,   // Current velocity
    target: f32,     // Destination
    config: SpringConfig,
}

struct SpringConfig {
    stiffness: f32,  // Spring constant (k)
    damping: f32,    // Damping coefficient (d)
    mass: f32,       // Virtual mass
}
}

RK4 Integration

Blinc uses 4th-order Runge-Kutta (RK4) integration for stability:

#![allow(unused)]
fn main() {
fn step(&mut self, dt: f32) {
    // RK4 provides stable integration even with large timesteps
    let k1 = self.acceleration(self.value, self.velocity);
    let k2 = self.acceleration(
        self.value + self.velocity * dt * 0.5,
        self.velocity + k1 * dt * 0.5
    );
    let k3 = self.acceleration(
        self.value + (self.velocity + k2 * dt * 0.5) * dt * 0.5,
        self.velocity + k2 * dt * 0.5
    );
    let k4 = self.acceleration(
        self.value + (self.velocity + k3 * dt) * dt,
        self.velocity + k3 * dt
    );

    self.velocity += (k1 + 2.0 * k2 + 2.0 * k3 + k4) * dt / 6.0;
    self.value += self.velocity * dt;
}
}

Spring Presets

PresetStiffnessDampingCharacter
stiff()40030Fast, minimal overshoot
snappy()30020Quick with slight bounce
gentle()12014Soft, slower motion
wobbly()18012Bouncy, playful
molasses()5020Very slow, heavy

Settling Detection

A spring is considered settled when:

#![allow(unused)]
fn main() {
fn is_settled(&self) -> bool {
    let position_settled = (self.value - self.target).abs() < EPSILON;
    let velocity_settled = self.velocity.abs() < VELOCITY_EPSILON;
    position_settled && velocity_settled
}
}

AnimatedValue

AnimatedValue wraps a spring for easy use in components:

#![allow(unused)]
fn main() {
// Create an animated value
let scale = ctx.use_animated_value(1.0, SpringConfig::snappy());

// Read current value
let current = scale.lock().unwrap().get();

// Set new target (animates to it)
scale.lock().unwrap().set_target(1.2);

// Set immediately (no animation)
scale.lock().unwrap().set(1.0);
}

SharedAnimatedValue

For use across closures, values are wrapped in Arc<Mutex<_>>:

#![allow(unused)]
fn main() {
let scale = ctx.use_animated_value(1.0, SpringConfig::snappy());

// Clone Arc for closure
let hover_scale = Arc::clone(&scale);

motion()
    .scale(scale.lock().unwrap().get())
    .on_hover_enter(move |_| {
        hover_scale.lock().unwrap().set_target(1.1);
    })
}

Keyframe Animations

For time-based animations with specific durations:

Keyframe Structure

#![allow(unused)]
fn main() {
struct Keyframe {
    time: f32,           // Time in animation (0.0 - 1.0)
    value: f32,          // Value at this keyframe
    easing: EasingFn,    // Interpolation to next keyframe
}
}

Easing Functions

EasingDescription
linearConstant speed
ease_inStart slow, end fast
ease_outStart fast, end slow
ease_in_outSlow at both ends
ease_in_quadQuadratic ease in
ease_out_cubicCubic ease out
ease_in_out_elasticElastic bounce

Animation Fill Modes

ModeDescription
NoneRevert after animation
ForwardsKeep final value
BackwardsApply initial before start
BothForwards + Backwards

Timelines

Timelines coordinate multiple animations:

#![allow(unused)]
fn main() {
let timeline = ctx.use_animated_timeline();

let entry_id = timeline.lock().unwrap().configure(|t| {
    // Add animation entries
    let rotation_id = t.add(
        0,      // start_ms
        1000,   // duration_ms
        0.0,    // from
        360.0   // to
    );

    // Configure looping
    t.set_loop(-1);  // -1 = infinite loop

    // Start the timeline
    t.start();

    rotation_id
});
}

Timeline Features

  • Stagger - Delay between child animations
  • Loop - Repeat animations
  • Reverse - Play backwards
  • Alternate - Ping-pong direction

Animation Scheduler

A background thread ticks all animations at 120fps:

#![allow(unused)]
fn main() {
struct AnimationScheduler {
    springs: Vec<SharedAnimatedValue>,
    timelines: Vec<SharedAnimatedTimeline>,
    running: AtomicBool,
    needs_redraw: Arc<AtomicBool>,
    wake_callback: Box<dyn Fn() + Send>,
}
}

Scheduler Loop

#![allow(unused)]
fn main() {
fn run(&self) {
    let frame_duration = Duration::from_secs_f64(1.0 / 120.0);

    while self.running.load(Ordering::SeqCst) {
        let start = Instant::now();

        // Tick all springs
        for spring in &self.springs {
            spring.lock().unwrap().step(frame_duration.as_secs_f32());
        }

        // Tick all timelines
        for timeline in &self.timelines {
            timeline.lock().unwrap().tick(frame_duration);
        }

        // If any animation is active, request redraw
        if self.has_active_animations() {
            self.needs_redraw.store(true, Ordering::SeqCst);
            (self.wake_callback)();  // Wake the main thread
        }

        // Sleep for remaining frame time
        let elapsed = start.elapsed();
        if elapsed < frame_duration {
            thread::sleep(frame_duration - elapsed);
        }
    }
}
}

Benefits of Background Thread

  1. Consistent timing - Animations run at 120fps regardless of main thread
  2. Survives focus loss - Continues when window loses focus
  3. Non-blocking - Doesn’t block UI event processing
  4. Battery efficient - Only runs when animations are active

Motion Container

motion() binds animations to elements:

#![allow(unused)]
fn main() {
motion()
    .scale(scale.lock().unwrap().get())      // Read current value
    .opacity(opacity.lock().unwrap().get())
    .translate_y(y.lock().unwrap().get())
    .child(content)
}

How Motion Works

  1. At build time: Reads current animation values
  2. Stores binding: Remembers which animated values to sample
  3. At render time: Samples current values from scheduler
  4. No rebuild needed: Animation updates don’t trigger tree rebuilds

Enter/Exit Animations

Motion also provides declarative enter/exit:

#![allow(unused)]
fn main() {
motion()
    .fade_in(300)                           // Fade in over 300ms
    .scale_in(300)                          // Scale from 0 to 1
    .slide_in(SlideDirection::Right, 200)   // Slide from right
    .child(content)
}

Integration Points

With Stateful Elements

#![allow(unused)]
fn main() {
fn animated_button(ctx: &WindowedContext) -> impl ElementBuilder {
    let scale = ctx.use_animated_value(1.0, SpringConfig::snappy());
    let hover = Arc::clone(&scale);
    let leave = Arc::clone(&scale);

    motion()
        .scale(scale.lock().unwrap().get())
        .on_hover_enter(move |_| {
            hover.lock().unwrap().set_target(1.05);
        })
        .on_hover_leave(move |_| {
            leave.lock().unwrap().set_target(1.0);
        })
        .child(button_content())
}
}

With BlincComponent

#![allow(unused)]
fn main() {
#[derive(BlincComponent)]
struct ExpandableCard {
    #[animation]
    height: f32,
    #[animation]
    arrow_rotation: f32,
}

fn card(ctx: &WindowedContext) -> impl ElementBuilder {
    let height = ExpandableCard::use_height(ctx, 60.0, SpringConfig::snappy());
    let rotation = ExpandableCard::use_arrow_rotation(ctx, 0.0, SpringConfig::snappy());

    motion()
        .h(height.lock().unwrap().get())
        .on_click(move |_| {
            height.lock().unwrap().set_target(200.0);
            rotation.lock().unwrap().set_target(180.0);
        })
        .child(card_content())
}
}

Performance Considerations

  1. Spring settling - Stopped springs don’t consume CPU
  2. Batched ticks - All animations tick together
  3. No allocations - Animation values are pre-allocated
  4. GPU transforms - Motion transforms are GPU-accelerated
  5. Minimal redraws - Only redraw when animations are active

Stateful Elements & FSM

Blinc uses Finite State Machines (FSM) to manage interactive UI state. This provides predictable state transitions for widgets like buttons, checkboxes, and text fields.

Finite State Machines

Core Concepts

An FSM defines:

  • States: Discrete conditions the element can be in
  • Events: Inputs that trigger transitions
  • Transitions: Rules mapping (state, event) -> new_state
#![allow(unused)]
fn main() {
// State IDs and Event IDs are u32
type StateId = u32;
type EventId = u32;

struct Transition {
    from_state: StateId,
    event: EventId,
    to_state: StateId,
    guard: Option<Box<dyn Fn() -> bool>>,  // Conditional transition
    action: Option<Box<dyn Fn()>>,          // Side effect
}
}

FSM Builder

#![allow(unused)]
fn main() {
let fsm = StateMachine::builder(initial_state)
    .on(State::Idle, Event::PointerEnter, State::Hovered)
    .on(State::Hovered, Event::PointerLeave, State::Idle)
    .on(State::Hovered, Event::PointerDown, State::Pressed)
    .on(State::Pressed, Event::PointerUp, State::Hovered)
    .on_enter(State::Pressed, || {
        println!("Button pressed!");
    })
    .build();
}

Entry/Exit Callbacks

#![allow(unused)]
fn main() {
.on_enter(state, || { /* called when entering state */ })
.on_exit(state, || { /* called when leaving state */ })
}

Guard Conditions

Transitions can be conditional:

#![allow(unused)]
fn main() {
.transition(
    Transition::new(State::Idle, Event::Click, State::Active)
        .with_guard(|| is_enabled())
)
}

StateTransitions Trait

For type-safe state definitions, implement StateTransitions:

#![allow(unused)]
fn main() {
use blinc_layout::stateful::StateTransitions;
use blinc_core::events::event_types::*;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum ButtonState {
    #[default]
    Idle,
    Hovered,
    Pressed,
    Disabled,
}

impl StateTransitions for ButtonState {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            (ButtonState::Idle, POINTER_ENTER) => Some(ButtonState::Hovered),
            (ButtonState::Hovered, POINTER_LEAVE) => Some(ButtonState::Idle),
            (ButtonState::Hovered, POINTER_DOWN) => Some(ButtonState::Pressed),
            (ButtonState::Pressed, POINTER_UP) => Some(ButtonState::Hovered),
            _ => None,
        }
    }
}
}

Available Event Types

#![allow(unused)]
fn main() {
// Pointer events
POINTER_ENTER    // Mouse enters element
POINTER_LEAVE    // Mouse leaves element
POINTER_DOWN     // Mouse button pressed
POINTER_UP       // Mouse button released
POINTER_MOVE     // Mouse moved over element

// Keyboard events
KEY_DOWN         // Key pressed
KEY_UP           // Key released
TEXT_INPUT       // Character typed

// Focus events
FOCUS            // Element gained focus
BLUR             // Element lost focus

// Other
SCROLL           // Scroll event
DRAG             // Drag motion
DRAG_END         // Drag completed
}

Stateful Elements

Creating Stateful Elements

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

fn interactive_card() -> impl ElementBuilder {
    stateful::<ButtonState>()
        .w(200.0)
        .h(120.0)
        .rounded(12.0)
        .on_state(|ctx| {
            let bg = match ctx.state() {
                ButtonState::Idle => Color::rgba(0.15, 0.15, 0.2, 1.0),
                ButtonState::Hovered => Color::rgba(0.18, 0.18, 0.25, 1.0),
                ButtonState::Pressed => Color::rgba(0.12, 0.12, 0.16, 1.0),
                ButtonState::Disabled => Color::rgba(0.1, 0.1, 0.12, 0.5),
            };
            div().bg(bg).child(text("Hover me").color(Color::WHITE))
        })
}
}

How It Works

  1. Builder creation: stateful::<S>() creates a StatefulBuilder for state type S
  2. Key generation: Automatic key based on call site location
  3. Event routing: Pointer/keyboard events are routed to the FSM
  4. State transition: FSM computes new state from (current_state, event)
  5. Callback invocation: on_state callback runs with StateContext
  6. Visual update: Returned Div is merged onto container

StateContext API

The callback receives a StateContext with these methods:

#![allow(unused)]
fn main() {
.on_state(|ctx| {
    // Get current state
    let state = ctx.state();

    // Get triggering event (if any)
    if let Some(event) = ctx.event() {
        // Handle specific event types
        match event.event_type {
            POINTER_UP => println!("Clicked!"),
            _ => {}
        }
    }

    // Create scoped signals
    let counter = ctx.use_signal("counter", || 0);

    // Create animated values (spring physics)
    let scale = ctx.use_spring("scale", 1.0, SpringConfig::snappy());

    // Create animated timelines (keyframe sequences)
    let (entry_id, timeline) = ctx.use_timeline("fade", |t| {
        let id = t.add(0, 500, 0.0, 1.0);
        t.set_loop(-1);
        t.start();
        id
    });

    // Create keyframe animations with fluent API
    let anim = ctx.use_keyframes("pulse", |k| {
        k.at(0, 0.8).at(800, 1.2).ease(Easing::EaseInOut).ping_pong().loop_infinite()
    });

    // Access dependency values by index
    let value: i32 = ctx.dep(0).unwrap_or_default();

    // Get dependency as State handle
    let state_handle = ctx.dep_as_state::<i32>(0);

    // Dispatch events
    ctx.dispatch(CUSTOM_EVENT);

    div()
})
}

Built-in State Types

ButtonState

#![allow(unused)]
fn main() {
enum ButtonState {
    Idle,      // Default
    Hovered,   // Mouse over
    Pressed,   // Mouse down
    Disabled,  // Non-interactive
}
}

Transitions:

  • Idle → Hovered (pointer enter)
  • Hovered → Idle (pointer leave)
  • Hovered → Pressed (pointer down)
  • Pressed → Hovered (pointer up)

NoState

For elements that only need dependency tracking:

#![allow(unused)]
fn main() {
stateful::<NoState>()
    .deps([signal.signal_id()])
    .on_state(|_ctx| {
        div().child(text("Rebuilds on signal change"))
    })
}

ToggleState

#![allow(unused)]
fn main() {
enum ToggleState {
    Off,
    On,
}
}

Transitions:

  • Off → On (click)
  • On → Off (click)

CheckboxState

#![allow(unused)]
fn main() {
enum CheckboxState {
    UncheckedIdle,
    UncheckedHovered,
    CheckedIdle,
    CheckedHovered,
}
}

TextFieldState

#![allow(unused)]
fn main() {
enum TextFieldState {
    Idle,
    Hovered,
    Focused,
    FocusedHovered,
    Disabled,
}
}

ScrollState

#![allow(unused)]
fn main() {
enum ScrollState {
    Idle,
    Scrolling,
    Decelerating,
    Bouncing,
}
}

Signal Dependencies

Stateful elements can depend on external signals using .deps():

#![allow(unused)]
fn main() {
fn counter_display(count: State<i32>) -> impl ElementBuilder {
    stateful::<ButtonState>()
        .deps([count.signal_id()])  // Re-run on_state when count changes
        .on_state(move |ctx| {
            // Access via captured variable
            let value = count.get();

            // Or via context by index
            let value_alt: i32 = ctx.dep(0).unwrap_or_default();

            div().child(
                text(&format!("Count: {}", value)).color(Color::WHITE)
            )
        })
}
}

Accessing Dependencies

Two patterns for accessing dependency values:

#![allow(unused)]
fn main() {
// Pattern 1: Capture in closure
let my_signal = use_state(|| 42);

stateful::<ButtonState>()
    .deps([my_signal.signal_id()])
    .on_state(move |ctx| {
        let value = my_signal.get();  // Via captured variable
        div()
    })

// Pattern 2: Access via context
stateful::<ButtonState>()
    .deps([my_signal.signal_id()])
    .on_state(|ctx| {
        let value: i32 = ctx.dep(0).unwrap_or_default();  // Via index
        div()
    })
}

When to Use .deps()

Without .deps()With .deps()
Only runs on state transitionsAlso runs when dependencies change
Hover/press onlyExternal data + hover/press

Scoped State Management

StateContext provides scoped utilities that persist across rebuilds:

Scoped Signals

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Signal keyed as "{stateful_key}:signal:click_count"
        let clicks = ctx.use_signal("click_count", || 0);

        div()
            .child(text(&format!("Clicks: {}", clicks.get())))
            .on_click(move |_| clicks.update(|n| n + 1))
    })
}

Springs (use_spring)

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Target value changes based on state
        let target = match ctx.state() {
            ButtonState::Hovered => 1.1,
            _ => 1.0,
        };

        // use_spring automatically animates to target
        let scale = ctx.use_spring("scale", target, SpringConfig::snappy());

        div().transform(Transform::scale(scale, scale))
    })
}

Keyframes (use_keyframes)

#![allow(unused)]
fn main() {
stateful::<ButtonState>()
    .on_state(|ctx| {
        // Keyframe animation with ping-pong and easing
        let pulse = ctx.use_keyframes("pulse", |k| {
            k.at(0, 0.8)
             .at(800, 1.2)
             .ease(Easing::EaseInOut)
             .ping_pong()
             .loop_infinite()
             .start()
        });

        let scale = pulse.get();
        div().transform(Transform::scale(scale, scale))
    })
}

Timelines (use_timeline)

#![allow(unused)]
fn main() {
stateful::<NoState>()
    .on_state(|ctx| {
        // Timeline with staggered entries
        let ((bar1, bar2), timeline) = ctx.use_timeline("bars", |t| {
            let b1 = t.add_with_easing(0, 500, 0.0, 60.0, Easing::EaseInOut);
            let b2 = t.add_with_easing(100, 500, 0.0, 60.0, Easing::EaseInOut);
            t.set_alternate(true);
            t.set_loop(-1);
            t.start();
            (b1, b2)
        });

        let x1 = timeline.get(bar1).unwrap_or(0.0);
        let x2 = timeline.get(bar2).unwrap_or(0.0);

        div()
            .child(div().transform(Transform::translate(x1, 0.0)))
            .child(div().transform(Transform::translate(x2, 0.0)))
    })
}

Custom State Machines

For complex interactions, define your own states:

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
enum DragState {
    #[default]
    Idle,
    Hovering,
    Pressing,
    Dragging,
}

impl StateTransitions for DragState {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            (DragState::Idle, POINTER_ENTER) => Some(DragState::Hovering),
            (DragState::Hovering, POINTER_LEAVE) => Some(DragState::Idle),
            (DragState::Hovering, POINTER_DOWN) => Some(DragState::Pressing),
            (DragState::Pressing, DRAG) => Some(DragState::Dragging),
            (DragState::Pressing, POINTER_UP) => Some(DragState::Hovering),
            (DragState::Dragging, DRAG_END) => Some(DragState::Idle),
            _ => None,
        }
    }
}

fn draggable_element() -> impl ElementBuilder {
    stateful::<DragState>()
        .on_state(|ctx| {
            let bg = match ctx.state() {
                DragState::Idle => Color::BLUE,
                DragState::Hovering => Color::CYAN,
                DragState::Pressing => Color::YELLOW,
                DragState::Dragging => Color::GREEN,
            };
            div().w(100.0).h(100.0).bg(bg)
        })
}
}

Event Routing

Event Flow

Platform Event (pointer, keyboard)
    │
    ├── Hit test: which element?
    │
    ├── EventRouter dispatches to element
    │
    ├── StateMachine receives event
    │   └── Computes transition
    │
    └── on_state callback invoked

Event Context

Handlers receive event details:

#![allow(unused)]
fn main() {
.on_click(|ctx| {
    println!("Clicked at ({}, {})", ctx.local_x, ctx.local_y);
})
.on_key_down(|ctx| {
    if ctx.ctrl && ctx.key_code == 83 {  // Ctrl+S
        save();
    }
})
}

Performance

Why FSM Over Signals?

Signals for visual stateFSM for visual state
Triggers full rebuildUpdates only affected element
Creates new VDOMMutates existing element
O(tree size)O(1)

Minimal Updates

Stateful elements only update their own RenderProps:

#![allow(unused)]
fn main() {
// State change only affects this element
.on_state(|ctx| {
    div().bg(new_color)  // Updates RenderProps
    // No layout recomputation
    // No tree diff
    // Just visual update
})
}

Queued Updates

State changes queue updates efficiently:

#![allow(unused)]
fn main() {
static PENDING_PROP_UPDATES: Vec<(NodeId, RenderProps)>;

// Stateful callback queues update
fn on_state(ctx) -> Div {
    div().bg(color)
    // Queues: (node_id, updated_props)
}

// Processed in batch by windowed app
for (node_id, props) in drain_pending() {
    render_tree.update_props(node_id, props);
}
}