rae 0.1.12

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);
        maybe_save_screenshot("demo_transcript", frames);

        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 + 264.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, 220.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);
    let visible_end = view.total_rows.saturating_sub(view.scroll);
    let visible_start = visible_end.saturating_sub(view.visible_rows);
    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));
    draw_rounded_rect_lines(body, 22.0, 1.0, to_mq(accent.with_alpha(0.20)));
    let narrow = body.width < 560.0;
    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
        };
        let text_x = if narrow {
            body.x + 26.0
        } else {
            body.x + 124.0
        };
        let text_y = if narrow && !row.role.is_empty() {
            y + 32.0
        } else {
            y
        };
        let text_width = if narrow {
            body.width - 52.0
        } else {
            body.width - 146.0
        };
        let font_size = if narrow { 14.0 } else { 15.0 };
        let line_height = if narrow { 18.0 } else { 20.0 };

        draw_wrapped_text(
            &row.text,
            text_x,
            text_y,
            text_width,
            font_size,
            line_height,
            to_mq(color),
        );
        y += if narrow {
            if row.role.is_empty() {
                28.0
            } else {
                58.0
            }
        } else {
            36.0
        };
    }

    let diagnostics_top = (y + 16.0).max(body.bottom() - 126.0);
    let diagnostics = Rect::new(
        body.x + 18.0,
        diagnostics_top,
        body.width - 36.0,
        body.bottom() - diagnostics_top - 18.0,
    );
    if diagnostics.height >= 78.0 {
        draw_viewport_diagnostics(
            diagnostics,
            palette,
            accent,
            viewport.columns(),
            view.total_rows,
            view.visible_rows,
            view.scroll,
            view.max_scroll,
            visible_start,
            visible_end,
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn draw_viewport_diagnostics(
    rect: Rect,
    palette: &Palette,
    accent: rae::Rgba,
    columns: usize,
    total_rows: usize,
    visible_rows: usize,
    scroll: usize,
    max_scroll: usize,
    visible_start: usize,
    visible_end: usize,
) {
    draw_rounded_rect(rect, 18.0, rgba8(1, 5, 16, 132));
    draw_rounded_rect_lines(rect, 18.0, 1.0, to_mq(accent.with_alpha(0.26)));
    draw_text_line(
        "viewport diagnostics",
        rect.x + 16.0,
        rect.y + 23.0,
        13.0,
        to_mq(accent),
    );
    draw_text_line(
        &format!("{columns} cols  rows {visible_start}..{visible_end} of {total_rows}"),
        rect.x + 162.0,
        rect.y + 23.0,
        12.0,
        to_mq(palette.muted),
    );

    let track = Rect::new(rect.x + 16.0, rect.y + 42.0, rect.width - 32.0, 14.0);
    draw_rounded_rect(track, 7.0, rgba8(1, 5, 16, 180));
    let total = total_rows.max(1) as f32;
    let start_t = visible_start as f32 / total;
    let width_t = (visible_rows.min(total_rows) as f32 / total).clamp(0.04, 1.0);
    let window = Rect::new(
        track.x + track.width * start_t,
        track.y,
        track.width * width_t,
        track.height,
    );
    draw_rounded_rect(window, 7.0, to_mq(accent.with_alpha(0.58)));
    for index in 0..=total_rows.min(18) {
        let x = track.x + track.width * (index as f32 / total_rows.max(1) as f32);
        draw_line(
            x,
            track.y - 5.0,
            x,
            track.bottom() + 5.0,
            1.0,
            to_mq(palette.accent.with_alpha(0.12)),
        );
    }

    let scroll_ratio = if max_scroll == 0 {
        0.0
    } else {
        scroll as f32 / max_scroll as f32
    };
    draw_text_line(
        &format!("scroll clamp {scroll}/{max_scroll}"),
        rect.x + 16.0,
        rect.bottom() - 16.0,
        12.0,
        to_mq(palette.muted),
    );
    draw_text_line(
        &format!("{:.0}%", scroll_ratio * 100.0),
        rect.right() - 48.0,
        rect.bottom() - 16.0,
        12.0,
        to_mq(accent),
    );
}