rae 0.1.11

Renderer-neutral widget, layout, sanitization, and GLSL shader primitives for Rust desktop tools.
Documentation
mod support;

use macroquad::prelude::*;
use rae::{Palette, Rect, TranscriptItem, TranscriptViewport};
use support::*;

fn window_conf() -> Conf {
    Conf {
        window_title: "rae transcript viewport".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 items = transcript_items();
    let max_frames = max_frames_from_env();
    let mut frames = 0u64;

    loop {
        frames = frames.saturating_add(1);
        if should_stop(frames, max_frames) {
            break;
        }

        let time = get_time() as f32;
        let viewport = Rect::new(0.0, 0.0, screen_width(), screen_height());
        draw_scene_backdrop(&palette, time);
        draw_header(viewport, &palette);
        draw_transcript_viewports(viewport, &palette, &items, time);

        next_frame().await;
    }
}

fn transcript_items() -> Vec<TranscriptItem> {
    vec![
        TranscriptItem::system("rae example session initialized"),
        TranscriptItem::user("Show how transcript rows wrap and clamp across renderer sizes."),
        TranscriptItem::assistant(
            "TranscriptViewport computes columns from glyph metrics, wraps long rows, preserves role labels on first lines, marks detail rows as muted, and clamps scroll to the available content.",
            "detail: renderer chooses fonts, colors, selection, and scroll gestures",
        ),
        TranscriptItem::assistant(
            "Because this crate stays renderer-neutral, the viewport output is just rows and flags.",
            "This example renders two different viewport sizes from the same transcript model.",
        ),
    ]
}

fn draw_header(viewport: Rect, palette: &Palette) {
    let header = Rect::new(28.0, 24.0, viewport.width - 56.0, 74.0);
    draw_glass_panel(header, 26.0, palette, palette.accent, true);
    draw_text_line(
        "Transcript",
        header.x + 26.0,
        header.y + 42.0,
        31.0,
        to_mq(palette.text),
    );
    draw_text_line(
        "viewport wrapping, muted detail rows, and clamped scroll windows",
        header.x + 222.0,
        header.y + 40.0,
        17.0,
        to_mq(palette.muted),
    );
}

fn draw_transcript_viewports(
    viewport: Rect,
    palette: &Palette,
    items: &[TranscriptItem],
    time: f32,
) {
    let area = Rect::new(28.0, 126.0, viewport.width - 56.0, viewport.height - 154.0);
    let left = Rect::new(area.x, area.y, area.width * 0.62 - 12.0, area.height);
    let right = Rect::new(
        left.right() + 24.0,
        area.y,
        area.width * 0.38 - 12.0,
        area.height,
    );
    draw_transcript_panel(
        left,
        palette,
        "wide viewport",
        TranscriptViewport::new(left.width - 64.0, left.height - 118.0).with_metrics(9.0, 22.0),
        items,
        palette.success,
        time,
    );
    draw_transcript_panel(
        right,
        palette,
        "narrow scrolled",
        TranscriptViewport::new(right.width - 64.0, right.height - 118.0)
            .with_metrics(8.5, 20.0)
            .with_scroll(3),
        items,
        palette.warning,
        time + 0.7,
    );
}

fn draw_transcript_panel(
    rect: Rect,
    palette: &Palette,
    title: &str,
    viewport: TranscriptViewport,
    items: &[TranscriptItem],
    accent: rae::Rgba,
    time: f32,
) {
    draw_glass_panel(rect, 30.0, palette, accent, true);
    let view = viewport.materialize(items);
    draw_text_line(
        title,
        rect.x + 26.0,
        rect.y + 38.0,
        24.0,
        to_mq(palette.text),
    );
    draw_text_line(
        &format!(
            "total {} visible {} scroll {}/{}",
            view.total_rows, view.visible_rows, view.scroll, view.max_scroll
        ),
        rect.x + 26.0,
        rect.y + 64.0,
        14.0,
        to_mq(palette.muted),
    );
    draw_glow(
        rect.right() - 80.0,
        rect.y + 92.0,
        120.0,
        accent.with_alpha(0.10 + time.sin().abs() * 0.04),
        6,
    );

    let body = Rect::new(
        rect.x + 26.0,
        rect.y + 96.0,
        rect.width - 52.0,
        rect.height - 122.0,
    );
    draw_rounded_rect(body, 22.0, rgba8(1, 5, 16, 150));
    let mut y = body.y + 32.0;
    for row in view.rows {
        let role_color = match row.role.as_str() {
            "user" => palette.warning,
            "assistant" => palette.success,
            "system" => palette.accent,
            _ => palette.muted,
        };
        if !row.role.is_empty() {
            draw_pill(
                Rect::new(body.x + 18.0, y - 18.0, 86.0, 25.0),
                &row.role,
                role_color,
                palette.text,
            );
        }
        let color = if row.muted {
            palette.muted
        } else {
            palette.text
        };
        draw_wrapped_text(
            &row.text,
            body.x + 124.0,
            y,
            body.width - 146.0,
            15.0,
            20.0,
            to_mq(color),
        );
        y += 36.0;
    }
}