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::{
    materialize_output, CodeTokenKind, OutputDocument, OutputOptions, OutputRow, Palette, Rect,
    Rgba,
};
use support::*;

fn window_conf() -> Conf {
    Conf {
        window_title: "rae code output".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 doc = materialize_output(
        assistant_output(),
        OutputOptions::for_width(82).with_code_line_numbers(true),
    );
    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, &doc);
        draw_output_document(viewport, &palette, &doc);
        draw_copy_panel(viewport, &palette, &doc);

        next_frame().await;
    }
}

fn assistant_output() -> &'static str {
    r#"# Patch

Here is a minimal safe code output. The incoming text can contain ANSI like red or OSC links.

```rust
pub fn greet(name: &str) -> String {
    // renderer gets sanitized tokens, not raw terminal escapes
    format!("hello {name}")
}
```

- Strip ANSI before rendering.
- Preserve copy-safe fenced code.
- Keep line numbers and syntax token classes renderer-neutral.
"#
}

fn draw_header(viewport: Rect, palette: &Palette, doc: &OutputDocument) {
    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(
        "Code Output",
        header.x + 26.0,
        header.y + 42.0,
        31.0,
        to_mq(palette.text),
    );
    draw_text_line(
        "sanitized Markdown/code rows with renderer-neutral token classes",
        header.x + 248.0,
        header.y + 40.0,
        17.0,
        to_mq(palette.muted),
    );
    draw_pill(
        Rect::new(header.right() - 376.0, header.y + 20.0, 142.0, 32.0),
        &format!("{} rows", doc.rows.len()),
        palette.success,
        palette.text,
    );
    draw_pill(
        Rect::new(header.right() - 220.0, header.y + 20.0, 198.0, 32.0),
        &format!("{} escapes removed", doc.removed_escape_sequences),
        palette.warning,
        palette.text,
    );
}

fn draw_output_document(viewport: Rect, palette: &Palette, doc: &OutputDocument) {
    let rect = Rect::new(28.0, 126.0, viewport.width * 0.66, viewport.height - 154.0);
    draw_glass_panel(rect, 30.0, palette, palette.success, true);
    draw_section_header(
        "RENDERED OUTPUT ROWS",
        Rect::new(rect.x + 26.0, rect.y + 38.0, rect.width - 52.0, 18.0),
        palette.success,
        palette.muted,
    );

    let mut y = rect.y + 78.0;
    let left = rect.x + 30.0;
    let width = rect.width - 60.0;
    let mut in_code = false;

    for row in doc.visible_rows(0, 22) {
        match row {
            OutputRow::Blank => y += 14.0,
            OutputRow::Heading { level, text } => {
                draw_text_line(&format!("H{level}"), left, y, 13.0, to_mq(palette.accent));
                draw_text_line(text, left + 42.0, y, 24.0, to_mq(palette.text));
                y += 36.0;
            }
            OutputRow::Paragraph { text, .. } => {
                y = draw_wrapped_text(
                    text,
                    left + 42.0,
                    y,
                    width - 42.0,
                    16.0,
                    22.0,
                    to_mq(palette.text),
                );
            }
            OutputRow::Bullet { text, .. } => {
                draw_text_line("•", left + 8.0, y, 19.0, to_mq(palette.warning));
                y = draw_wrapped_text(
                    text,
                    left + 42.0,
                    y,
                    width - 42.0,
                    16.0,
                    22.0,
                    to_mq(palette.text),
                );
            }
            OutputRow::Quote { text, .. } => {
                draw_line(
                    left + 8.0,
                    y - 16.0,
                    left + 8.0,
                    y + 10.0,
                    2.0,
                    to_mq(palette.accent),
                );
                y = draw_wrapped_text(
                    text,
                    left + 28.0,
                    y,
                    width - 28.0,
                    16.0,
                    22.0,
                    to_mq(palette.muted),
                );
            }
            OutputRow::CodeFenceStart { language } => {
                in_code = true;
                draw_pill(
                    Rect::new(left, y - 18.0, 104.0, 26.0),
                    language,
                    palette.accent,
                    palette.text,
                );
                y += 18.0;
            }
            OutputRow::Code { gutter, tokens, .. } => {
                let line_rect = Rect::new(left, y - 18.0, width, 26.0);
                draw_rounded_rect(
                    line_rect,
                    8.0,
                    rgba8(1, 5, 16, if in_code { 150 } else { 80 }),
                );
                draw_text_line(
                    gutter.as_deref().unwrap_or(""),
                    left + 12.0,
                    y,
                    14.0,
                    to_mq(palette.muted),
                );
                let mut x = left + 68.0;
                for token in tokens {
                    let color = token_color(token.kind, palette);
                    draw_text_line(&token.text, x, y, 14.0, to_mq(color));
                    x += measure_text(&token.text, None, 14, 1.0).width;
                }
                y += 29.0;
            }
            OutputRow::CodeFenceEnd => {
                in_code = false;
                y += 12.0;
            }
        }
    }
}

fn draw_copy_panel(viewport: Rect, palette: &Palette, doc: &OutputDocument) {
    let rect = Rect::new(
        viewport.width * 0.70,
        126.0,
        viewport.width * 0.30 - 28.0,
        viewport.height - 154.0,
    );
    draw_glass_panel(rect, 30.0, palette, palette.warning, false);
    draw_section_header(
        "SANITIZATION",
        Rect::new(rect.x + 24.0, rect.y + 38.0, rect.width - 48.0, 18.0),
        palette.warning,
        palette.muted,
    );

    let metric_w = (rect.width - 66.0) / 2.0;
    draw_metric(
        Rect::new(rect.x + 24.0, rect.y + 78.0, metric_w, 70.0),
        "escapes",
        &doc.removed_escape_sequences.to_string(),
        palette.warning,
        palette,
    );
    draw_metric(
        Rect::new(rect.x + 42.0 + metric_w, rect.y + 78.0, metric_w, 70.0),
        "controls",
        &doc.removed_control_chars.to_string(),
        palette.accent,
        palette,
    );
    draw_metric(
        Rect::new(rect.x + 24.0, rect.y + 164.0, metric_w, 70.0),
        "zero-width",
        &doc.removed_zero_width_chars.to_string(),
        palette.success,
        palette,
    );
    draw_metric(
        Rect::new(rect.x + 42.0 + metric_w, rect.y + 164.0, metric_w, 70.0),
        "truncated",
        if doc.truncated { "yes" } else { "no" },
        Rgba::rgb8(205, 130, 255),
        palette,
    );

    let copy = Rect::new(
        rect.x + 24.0,
        rect.y + 262.0,
        rect.width - 48.0,
        rect.height - 286.0,
    );
    draw_rounded_rect(copy, 20.0, rgba8(1, 5, 16, 150));
    draw_text_line(
        "copy-safe markdown",
        copy.x + 16.0,
        copy.y + 28.0,
        16.0,
        to_mq(palette.accent),
    );
    draw_wrapped_text(
        &doc.copy_text,
        copy.x + 16.0,
        copy.y + 58.0,
        copy.width - 32.0,
        13.0,
        18.0,
        to_mq(palette.muted),
    );
}

fn token_color(kind: CodeTokenKind, palette: &Palette) -> Rgba {
    match kind {
        CodeTokenKind::Text => palette.text,
        CodeTokenKind::Keyword => palette.accent,
        CodeTokenKind::String => palette.success,
        CodeTokenKind::Number => palette.warning,
        CodeTokenKind::Comment => palette.muted,
        CodeTokenKind::Punctuation => Rgba::rgb8(205, 130, 255),
    }
}