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),
);
}