rae 0.1.10

Renderer-neutral widget, layout, sanitization, and GLSL shader primitives for Rust desktop tools.
Documentation
use std::env;

use macroquad::prelude::*;
use rae::{compute_shell_layout, Palette, Rect, Rgba};

fn window_conf() -> Conf {
    Conf {
        window_title: "rae layout demo".to_string(),
        window_width: 1440,
        window_height: 900,
        high_dpi: true,
        sample_count: 4,
        ..Default::default()
    }
}

#[macroquad::main(window_conf)]
async fn main() {
    let palette = Palette::midnight();
    let max_frames = env::var("RAE_RENDER_DEMO_FRAMES")
        .ok()
        .and_then(|value| value.parse::<u64>().ok());
    let mut frames = 0u64;

    loop {
        if is_key_pressed(KeyCode::Escape) {
            break;
        }

        let viewport = Rect::new(0.0, 0.0, screen_width(), screen_height());
        let layout = compute_shell_layout(viewport);
        let time = get_time() as f32;

        clear_background(to_mq(palette.background));
        draw_background(&palette, viewport, time);
        draw_shell_layout(&palette, layout);
        draw_device_previews(&palette, layout);

        frames = frames.saturating_add(1);
        if max_frames.is_some_and(|max_frames| frames >= max_frames) {
            break;
        }
        next_frame().await;
    }
}

fn draw_shell_layout(palette: &Palette, layout: rae::AppLayout) {
    if layout.header.height > 0.0 {
        draw_panel(
            layout.header,
            palette.panel_hot.with_alpha(0.72),
            palette.accent,
            "header",
            "Desktop shell chrome. Resize the window; Esc closes.",
        );
    }

    draw_panel(
        layout.main,
        palette.panel.with_alpha(0.72),
        palette.success,
        "main",
        "Primary content surface. This is rendered, not printed coordinates.",
    );

    if layout.sidebar_collapsed {
        let badge = Rect::new(
            layout.main.right() - 184.0,
            layout.main.y + 18.0,
            156.0,
            34.0,
        );
        draw_rounded_rect(badge, 13.0, to_mq(palette.warning.with_alpha(0.22)));
        draw_text_line(
            "sidebar collapsed",
            badge.x + 14.0,
            badge.y + 22.0,
            14.0,
            to_mq(palette.warning),
        );
    } else {
        draw_panel(
            layout.sidebar,
            palette.panel_hot.with_alpha(0.68),
            palette.warning,
            "sidebar",
            "Inspector/responsive secondary surface.",
        );
    }

    draw_panel(
        layout.prompt,
        palette.panel_hot.with_alpha(0.74),
        palette.accent,
        "prompt",
        "Input surface pinned above status.",
    );
    draw_panel(
        layout.status,
        palette.panel.with_alpha(0.78),
        palette.muted,
        "status",
        "status bar",
    );
}

fn draw_device_previews(palette: &Palette, layout: rae::AppLayout) {
    if layout.sidebar.is_empty() || layout.sidebar.width < 300.0 || layout.sidebar.height < 420.0 {
        return;
    }

    let presets = [
        ("desktop", Rect::new(0.0, 0.0, 1920.0, 1080.0)),
        ("laptop", Rect::new(0.0, 0.0, 1366.0, 768.0)),
        ("tablet", Rect::new(0.0, 0.0, 900.0, 720.0)),
        ("phone", Rect::new(0.0, 0.0, 430.0, 860.0)),
    ];

    let x = layout.sidebar.x + 22.0;
    let mut y = layout.sidebar.y + 92.0;
    draw_text_line(
        "responsive previews",
        x,
        y - 28.0,
        16.0,
        to_mq(palette.accent),
    );

    for (label, viewport) in presets {
        let preview = Rect::new(x, y, layout.sidebar.width - 44.0, 116.0);
        draw_preview_card(palette, label, viewport, preview);
        y += 132.0;
    }
}

fn draw_preview_card(palette: &Palette, label: &str, viewport: Rect, card: Rect) {
    draw_rounded_rect(card, 16.0, to_mq(palette.background.with_alpha(0.86)));
    draw_rectangle_lines(
        card.x,
        card.y,
        card.width,
        card.height,
        1.0,
        to_mq(palette.accent.with_alpha(0.32)),
    );
    draw_text_line(
        label,
        card.x + 12.0,
        card.y + 22.0,
        13.0,
        to_mq(palette.text),
    );

    let target = Rect::new(
        card.x + 12.0,
        card.y + 34.0,
        card.width - 24.0,
        card.height - 46.0,
    );
    let scale = (target.width / viewport.width).min(target.height / viewport.height);
    let scaled_root = Rect::new(
        target.x + (target.width - viewport.width * scale) * 0.5,
        target.y + (target.height - viewport.height * scale) * 0.5,
        viewport.width * scale,
        viewport.height * scale,
    );
    let layout = compute_shell_layout(viewport);

    draw_rectangle(
        scaled_root.x,
        scaled_root.y,
        scaled_root.width,
        scaled_root.height,
        to_mq(palette.panel.with_alpha(0.34)),
    );
    draw_scaled_rect(
        layout.header,
        scale,
        scaled_root,
        palette.accent.with_alpha(0.70),
    );
    draw_scaled_rect(
        layout.main,
        scale,
        scaled_root,
        palette.success.with_alpha(0.66),
    );
    draw_scaled_rect(
        layout.sidebar,
        scale,
        scaled_root,
        palette.warning.with_alpha(0.66),
    );
    draw_scaled_rect(
        layout.prompt,
        scale,
        scaled_root,
        palette.accent.with_alpha(0.42),
    );
    draw_scaled_rect(
        layout.status,
        scale,
        scaled_root,
        palette.muted.with_alpha(0.72),
    );
}

fn draw_scaled_rect(rect: Rect, scale: f32, root: Rect, color: Rgba) {
    if rect.is_empty() {
        return;
    }
    draw_rectangle(
        root.x + rect.x * scale,
        root.y + rect.y * scale,
        rect.width * scale,
        rect.height * scale,
        to_mq(color),
    );
}

fn draw_panel(rect: Rect, fill: Rgba, accent: Rgba, title: &str, detail: &str) {
    draw_rounded_rect(rect, 18.0, to_mq(fill));
    draw_rectangle_lines(
        rect.x,
        rect.y,
        rect.width,
        rect.height,
        1.4,
        to_mq(accent.with_alpha(0.54)),
    );
    draw_text_line(title, rect.x + 18.0, rect.y + 29.0, 18.0, to_mq(accent));
    if rect.height > 72.0 {
        draw_text_line(detail, rect.x + 18.0, rect.y + 56.0, 15.0, WHITE);
        let bounds = format!(
            "x:{:.0} y:{:.0} w:{:.0} h:{:.0}",
            rect.x, rect.y, rect.width, rect.height
        );
        draw_text_line(
            &bounds,
            rect.x + 18.0,
            rect.bottom() - 18.0,
            13.0,
            LIGHTGRAY,
        );
    }
}

fn draw_background(palette: &Palette, viewport: Rect, time: f32) {
    for index in 0..9 {
        let radius = 180.0 + index as f32 * 62.0;
        let alpha = 0.05 - index as f32 * 0.004;
        let x = viewport.width * (0.46 + (time * 0.08).sin() * 0.06);
        let y = viewport.height * (0.36 + (time * 0.11).cos() * 0.08);
        draw_circle(
            x,
            y,
            radius,
            to_mq(palette.accent.with_alpha(alpha.max(0.006))),
        );
    }
}

fn draw_rounded_rect(rect: Rect, radius: f32, color: Color) {
    let radius = radius.min(rect.width * 0.5).min(rect.height * 0.5).max(0.0);
    draw_rectangle(
        rect.x + radius,
        rect.y,
        rect.width - radius * 2.0,
        rect.height,
        color,
    );
    draw_rectangle(
        rect.x,
        rect.y + radius,
        rect.width,
        rect.height - radius * 2.0,
        color,
    );
    draw_circle(rect.x + radius, rect.y + radius, radius, color);
    draw_circle(rect.right() - radius, rect.y + radius, radius, color);
    draw_circle(rect.x + radius, rect.bottom() - radius, radius, color);
    draw_circle(rect.right() - radius, rect.bottom() - radius, radius, color);
}

fn draw_text_line(text: &str, x: f32, y: f32, size: f32, color: Color) {
    draw_text_ex(
        text,
        x,
        y,
        TextParams {
            font_size: size as u16,
            color,
            ..Default::default()
        },
    );
}

fn to_mq(color: Rgba) -> Color {
    Color::new(color.r, color.g, color.b, color.a)
}