egui-cha-analyzer 0.3.0

Static analyzer for egui UI flow: UI -> Action -> State
Documentation

egui-cha

A TEA (The Elm Architecture) framework for egui with a built-in Design System.

Why egui-cha?

Building admin panels, debug tools, or internal utilities with egui is great, but:

  • Time consuming - Writing everything from scratch takes time you don't have
  • Prototypes grow fast - "Quick hack" becomes 5000 lines of spaghetti
  • Separation gets blurry - UI code and application logic become tangled

The Problem with Raw egui

// Typical egui code - logic scattered everywhere
if ui.button("Save").clicked() {
    self.saving = true;
    self.data.validate();  // Business logic here?
    self.api.save(&self.data);  // Side effects here?
    self.saving = false;
    self.last_error = None;  // State management here?
}

The TEA Solution

// View: purely presentational
Button::primary("Save").on_click(ctx, Msg::Save);

// Update: all logic in one place
fn update(model: &mut Model, msg: Msg) -> Cmd<Msg> {
    match msg {
        Msg::Save => {
            model.saving = true;
            Cmd::try_task(
                api::save(&model.data),
                |_| Msg::SaveSuccess,
                |e| Msg::SaveError(e),
            )
        }
        Msg::SaveSuccess => { model.saving = false; Cmd::none() }
        Msg::SaveError(e) => { model.last_error = Some(e); Cmd::none() }
    }
}

Result: Logic stays in update(), views stay simple, and your "quick tool" won't fall apart as it grows.

Features

  • TEA Architecture: Model-View-Update pattern with Cmd for side effects
  • Router: Built-in navigation with history (back/forward)
  • Design System: Themed components following Atomic Design principles
  • Icon Support: Phosphor Icons integration (embedded font)
  • Error Handling: Structured error handling with Cmd::try_task, ErrorConsole, and on_framework_error
  • Testing Utilities: TestRunner for unit testing your app logic

Installation

cargo add egui-cha egui-cha-ds

Or add to your Cargo.toml:

[dependencies]
egui-cha = "0.1"
egui-cha-ds = "0.1"

Quick Start

use egui_cha::prelude::*;
use egui_cha_ds::prelude::*;

fn main() -> eframe::Result<()> {
    egui_cha::run::<MyApp>(RunConfig::new("My App"))
}

struct MyApp;

#[derive(Default)]
struct Model {
    count: i32,
}

#[derive(Clone)]
enum Msg {
    Increment,
    Decrement,
}

impl App for MyApp {
    type Model = Model;
    type Msg = Msg;

    fn init() -> (Model, Cmd<Msg>) {
        (Model::default(), Cmd::none())
    }

    fn update(model: &mut Model, msg: Msg) -> Cmd<Msg> {
        match msg {
            Msg::Increment => model.count += 1,
            Msg::Decrement => model.count -= 1,
        }
        Cmd::none()
    }

    fn view(model: &Model, ctx: &mut ViewCtx<Msg>) {
        ctx.ui.heading("Counter");
        ctx.ui.label(format!("Count: {}", model.count));

        ctx.horizontal(|ctx| {
            Button::primary("+").on_click(ctx, Msg::Increment);
            Button::secondary("-").on_click(ctx, Msg::Decrement);
        });
    }
}

Architecture

┌────────────────────────────────────────┐
│  egui-cha-ds (Design System)           │
│  Button, Input, Card, Icon, Theme...   │
├────────────────────────────────────────┤
│  egui-cha (TEA Core)                   │
│  App, Cmd, ViewCtx, Router, Component  │
└────────────────────────────────────────┘
                   ↓
                 egui

Components

Core (egui-cha)

Component Description
App Main application trait with init, update, view
Cmd Side effects: task, delay, try_task, from_result, retry
Severity Error levels: Debug, Info, Warn, Error, Critical
FrameworkError Framework internal errors with on_framework_error hook
ViewCtx UI context with emit, horizontal, vertical, group
Router Page navigation with history stack
Component Reusable component trait
ScrollArea Scrollable container with builder pattern

Design System (egui-cha-ds)

Theme

// Apply theme to egui context
let theme = Theme::dark(); // or Theme::light(), Theme::pastel(), Theme::pastel_dark()
theme.apply(ctx.ui.ctx());

Theme includes:

  • Colors: Primary, Secondary, UI State (state_success/warning/danger/info), Log Severity (log_debug/info/warn/error/critical), Background, Text, Border
  • Spacing: xs (4px), sm (8px), md (16px), lg (24px), xl (32px)
  • Border Radius: sm, md, lg
  • Typography: font_size_xs (10px) through font_size_3xl (30px)

Atoms (13 components)

Component Description
Button Primary, Secondary, Outline, Ghost, Danger, Warning, Success, Info
Badge Default, Success, Warning, Error, Info
Text Typography: h1, h2, h3, body, small, caption with modifiers
Input Text input with TEA-style callbacks
ValidatedInput Input with validation state
Checkbox Boolean toggle with label
Toggle Switch-style boolean toggle
Slider Numeric range input
Select Dropdown selection
Icon Phosphor Icons (house, gear, info, etc.)
Link Hyperlink component
Code Code block display
Tooltip Themed tooltips via ResponseExt trait
ContextMenu Right-click menu via ContextMenuExt trait

Molecules (9 components)

Component Description
Card Container with optional title
Tabs Tabbed navigation with TabPanel
Modal Dialog overlay
Table Data table component
Navbar Horizontal navigation bar
ErrorConsole 5-level severity message display (Debug/Info/Warning/Error/Critical)
Toast Temporary notifications with auto-dismiss
Form Structured form with validation
SearchBar Search input with submit

Semantics (Pre-defined buttons)

// Consistent labels & icons across your app
semantics::save(ButtonStyle::Both).on_click(ctx, Msg::Save);
semantics::delete(ButtonStyle::Icon).on_click(ctx, Msg::Delete);

Available: save, edit, delete, close, add, remove, search, refresh, play, pause, stop, settings, back, forward, confirm, cancel, copy

Layout DSL (cha! macro)

Declarative layout syntax for composing views:

use egui_cha_ds::cha;

cha!(ctx, {
    Col(spacing: 8.0) {
        Card("Settings") {
            Row {
                @gear(20.0)
                ctx.ui.label("Options")
            }
            Button::primary("Save").on_click(ctx, Msg::Save)
        }

        Scroll(max_height: 300.0) {
            // scrollable content
        }
    }
});

Supported Nodes

Node Description
Col Vertical layout (ctx.vertical)
Row Horizontal layout (ctx.horizontal)
Group Grouped layout (ctx.group)
Scroll Vertical scroll area
ScrollH Horizontal scroll area
ScrollBoth Both directions scroll
Card("title") Card container
@icon Icon shorthand (e.g., @house, @gear(20.0))

Properties

  • Layout: spacing, padding
  • Scroll: max_height, max_width, min_height, min_width, id
  • Card: padding

Commands (Side Effects)

// Async task
Cmd::task(async {
    let data = fetch_data().await;
    Msg::DataLoaded(data)
})

// Delayed message
Cmd::delay(Duration::from_secs(1), Msg::Tick)

// Fallible async with error handling
Cmd::try_task(
    fetch_data(),
    |data| Msg::Success(data),
    |err| Msg::Error(err.to_string()),
)

// Batch multiple commands
Cmd::batch([cmd1, cmd2, cmd3])

Router

#[derive(Clone, PartialEq, Default)]
enum Page {
    #[default]
    Home,
    Settings,
    Profile(u64),
}

// In your Model
struct Model {
    router: Router<Page>,
}

// Navigate
Msg::Router(RouterMsg::Navigate(Page::Settings))
Msg::Router(RouterMsg::Back)
Msg::Router(RouterMsg::Forward)

// In view
navbar(ctx, &model.router, &[
    ("Home", Page::Home),
    ("Settings", Page::Settings),
], Msg::Router);

Icons (Phosphor)

use egui_cha_ds::atoms::{Icon, icons};

// Using convenience constructors
Icon::house().size(20.0).show(ctx.ui);
Icon::gear().color(Color32::RED).show(ctx.ui);

// Using icon constants directly
Icon::new(icons::CHECK).show(ctx.ui);

Available icons: HOUSE, GEAR, HASH, INFO, USER, CHECK, WARNING, PLUS, MINUS, X, ARROW_LEFT, ARROW_RIGHT, FIRE, BUG, WRENCH, X_CIRCLE

Testing

use egui_cha::test_prelude::*;

#[test]
fn test_increment() {
    let mut runner = TestRunner::<MyApp>::new();

    runner.send(Msg::Increment);
    assert_eq!(runner.model().count, 1);
    assert!(runner.last_was_none()); // No side effects
}

#[test]
fn test_async_command() {
    let mut runner = TestRunner::<MyApp>::new();

    runner.send(Msg::FetchData);
    assert!(runner.last_was_task()); // Returned an async task
}

License

MIT

Credits