mod support;
use macroquad::prelude::*;
use rae::{
hit_resize_handle, CommandItem, CommandPalette, CursorKind, FocusChain, FocusDirection,
FocusTarget, HitMap, HitRegion, KeyModifier, Palette, Rect, ResizeHandle, Rgba, ScrollAnchor,
ScrollState, Shortcut, SnippetSet, TextSnippet, Vec2,
};
use support::*;
fn window_conf() -> Conf {
Conf {
window_title: "rae interaction primitives".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 mut command_palette = demo_command_palette();
command_palette.set_query("pal");
let snippets = demo_snippets();
let mut scroll = ScrollState::new().with_anchor(ScrollAnchor::Bottom);
let max_frames = max_frames_from_env();
let mut frames = 0u64;
loop {
frames = frames.saturating_add(1);
if should_stop(frames, max_frames) {
break;
}
if is_key_pressed(KeyCode::Down) {
scroll.scroll_by(1);
}
if is_key_pressed(KeyCode::Up) {
scroll.scroll_by(-1);
}
let time = get_time() as f32;
let viewport = Rect::new(0.0, 0.0, screen_width(), screen_height());
let pointer = mouse_position();
let pointer = Vec2::new(pointer.0, pointer.1);
draw_scene_backdrop(&palette, time);
draw_header(viewport, &palette);
draw_interaction_board(
viewport,
&palette,
pointer,
&mut scroll,
&command_palette,
&snippets,
time,
);
maybe_save_screenshot("demo_interaction", frames);
next_frame().await;
}
}
fn demo_command_palette() -> CommandPalette {
CommandPalette::new([
CommandItem::new("open", "Open File")
.with_keywords(["document", "load"])
.with_shortcut(Shortcut::new("O").with_modifiers([KeyModifier::Ctrl])),
CommandItem::new("toggle.theme", "Toggle Theme")
.with_keywords(["palette", "color"])
.with_shortcut(
Shortcut::new("T").with_modifiers([KeyModifier::Ctrl, KeyModifier::Shift]),
),
CommandItem::new("copy.safe", "Copy Safe Text").with_keywords(["clipboard", "sanitize"]),
CommandItem::new("resize.panel", "Resize Panel")
.with_keywords(["drag", "handle", "layout"]),
])
}
fn demo_snippets() -> SnippetSet {
SnippetSet::new([
TextSnippet::new("hdr", "# Section").with_keywords(["markdown", "heading"]),
TextSnippet::new("code", "```rust\nfn main() {}\n```").with_description("Rust code fence"),
TextSnippet::new("note", "> Renderer-neutral note").with_keywords(["quote", "markdown"]),
])
}
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(
"Interaction",
header.x + 26.0,
header.y + 42.0,
31.0,
to_mq(palette.text),
);
draw_text_line(
"hit regions, focus traversal, resize handles, scroll state, commands, and snippets",
header.x + 274.0,
header.y + 40.0,
17.0,
to_mq(palette.muted),
);
}
fn draw_interaction_board(
viewport: Rect,
palette: &Palette,
pointer: Vec2,
scroll: &mut ScrollState,
command_palette: &CommandPalette,
snippets: &SnippetSet,
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.58 - 12.0, area.height);
let right = Rect::new(
left.right() + 24.0,
area.y,
area.width * 0.42 - 12.0,
area.height,
);
draw_hit_panel(left, palette, pointer, time);
draw_data_panel(right, palette, scroll, command_palette, snippets);
}
fn draw_hit_panel(rect: Rect, palette: &Palette, pointer: Vec2, time: f32) {
draw_glass_panel(rect, 30.0, palette, palette.accent, true);
draw_text_line(
"Hit Map",
rect.x + 26.0,
rect.y + 38.0,
24.0,
to_mq(palette.text),
);
let panel = Rect::new(
rect.x + 58.0,
rect.y + 112.0,
rect.width - 116.0,
rect.height - 190.0,
);
let nav = Rect::new(panel.x + 28.0, panel.y + 42.0, 126.0, panel.height - 128.0);
let editor = Rect::new(
nav.right() + 24.0,
panel.y + 54.0,
panel.width * 0.46,
216.0,
);
let transcript = Rect::new(editor.x + 42.0, editor.y + 72.0, editor.width * 0.78, 118.0);
let prompt = Rect::new(
panel.x + 34.0,
panel.bottom() - 76.0,
panel.width - 68.0,
42.0,
);
let overlay = Rect::new(
panel.x + panel.width * 0.46,
panel.y + panel.height * 0.36,
panel.width * 0.34,
138.0,
);
let action = Rect::new(panel.right() - 170.0, panel.y + 28.0, 136.0, 46.0);
let region_views = [
("panel", panel, palette.accent, 0, CursorKind::Grab),
("navigator", nav, palette.accent, 2, CursorKind::Pointer),
("editor", editor, palette.success, 4, CursorKind::Text),
(
"transcript",
transcript,
palette.warning,
6,
CursorKind::Grab,
),
(
"palette-overlay",
overlay,
Rgba::rgb8(205, 130, 255),
8,
CursorKind::Pointer,
),
(
"prompt",
prompt,
Rgba::rgb8(100, 224, 255),
9,
CursorKind::Text,
),
(
"primary-action",
action,
palette.success,
10,
CursorKind::Pointer,
),
];
let mut map = HitMap::new();
for (id, region, _, z_index, cursor) in region_views {
map.push(
HitRegion::new(id, region)
.with_z_index(z_index)
.with_cursor(cursor),
);
}
let hovered_id = map.hit_test(pointer).map(|hit| hit.id.clone());
let hit_count = map.all_at(pointer).len();
let resize = hit_resize_handle(panel, pointer, 9.0);
draw_glow(
panel.x + panel.width * 0.5,
panel.y + panel.height * 0.5,
220.0,
palette.accent.with_alpha(0.10 + time.sin().abs() * 0.04),
8,
);
draw_rounded_rect(panel, 28.0, rgba8(1, 5, 16, 158));
draw_rectangle_lines(
panel.x,
panel.y,
panel.width,
panel.height,
1.4,
to_mq(palette.accent.with_alpha(0.46)),
);
for line in 0..8 {
let y = panel.y + 42.0 + line as f32 * ((panel.height - 84.0) / 7.0);
draw_line(
panel.x + 24.0,
y,
panel.right() - 24.0,
y,
1.0,
to_mq(palette.accent.with_alpha(0.050)),
);
}
for line in 0..10 {
let x = panel.x + 28.0 + line as f32 * ((panel.width - 56.0) / 9.0);
draw_line(
x,
panel.y + 28.0,
x,
panel.bottom() - 28.0,
1.0,
to_mq(palette.accent.with_alpha(0.035)),
);
}
for (id, region, color, z_index, cursor) in region_views {
let active = hovered_id.as_deref() == Some(id);
let fill_alpha = if id == "panel" {
if active {
0.08
} else {
0.055
}
} else if active {
0.30
} else {
0.12
};
let line_alpha = if id == "panel" {
if active {
0.54
} else {
0.34
}
} else if active {
0.62
} else {
0.32
};
draw_rounded_rect(
region,
if id == "prompt" { 16.0 } else { 20.0 },
to_mq(color.with_alpha(fill_alpha)),
);
draw_rounded_rect_lines(
region,
if id == "prompt" { 16.0 } else { 20.0 },
if active { 1.8 } else { 1.0 },
to_mq(color.with_alpha(line_alpha)),
);
draw_text_line(id, region.x + 14.0, region.y + 26.0, 13.0, to_mq(color));
draw_text_line(
&format!("z{z_index} {:?}", cursor),
region.x + 14.0,
region.y + 46.0,
11.0,
to_mq(palette.muted),
);
if active {
draw_glow(
region.x + region.width * 0.5,
region.y + region.height * 0.5,
region.width.min(region.height) * 0.58,
color.with_alpha(0.13),
5,
);
}
}
let flow_points = [
vec2(nav.right(), nav.y + nav.height * 0.28),
vec2(editor.x, editor.y + editor.height * 0.42),
vec2(transcript.right(), transcript.y + transcript.height * 0.50),
vec2(overlay.x, overlay.y + overlay.height * 0.46),
vec2(action.x, action.y + action.height * 0.50),
];
for pair in flow_points.windows(2) {
draw_line(
pair[0].x,
pair[0].y,
pair[1].x,
pair[1].y,
1.4,
to_mq(palette.warning.with_alpha(0.22)),
);
}
let stack = Rect::new(panel.right() - 208.0, panel.bottom() - 178.0, 174.0, 124.0);
draw_rounded_rect(stack, 20.0, rgba8(0, 3, 12, 132));
draw_rounded_rect_lines(stack, 20.0, 1.0, to_mq(palette.warning.with_alpha(0.28)));
draw_text_line(
"z stack",
stack.x + 14.0,
stack.y + 24.0,
13.0,
to_mq(palette.warning),
);
for (index, (id, _, color, z_index, _)) in region_views.iter().rev().take(5).enumerate() {
let y = stack.y + 48.0 + index as f32 * 14.0;
draw_circle(stack.x + 16.0, y - 4.0, 3.0, to_mq(color.with_alpha(0.58)));
draw_text_line(
&format!("z{z_index} {id}"),
stack.x + 28.0,
y,
10.5,
to_mq(palette.text.with_alpha(0.82)),
);
}
for handle in resize_handles(panel) {
let active = resize == Some(handle.0);
draw_circle(
handle.1.x,
handle.1.y,
if active { 9.0 } else { 5.0 },
to_mq(if active {
palette.warning
} else {
palette.accent.with_alpha(0.50)
}),
);
}
draw_circle(pointer.x, pointer.y, 7.0, to_mq(palette.warning));
let label = hovered_id.as_deref().unwrap_or("none");
draw_pill(
Rect::new(rect.x + 26.0, rect.bottom() - 50.0, 170.0, 30.0),
&format!("hit: {label}"),
palette.accent,
palette.text,
);
draw_pill(
Rect::new(rect.x + 214.0, rect.bottom() - 50.0, 210.0, 30.0),
&format!("resize: {:?}", resize),
palette.warning,
palette.text,
);
draw_pill(
Rect::new(rect.x + 442.0, rect.bottom() - 50.0, 144.0, 30.0),
&format!("stack: {hit_count}"),
palette.success,
palette.text,
);
}
fn resize_handles(panel: Rect) -> [(ResizeHandle, Vec2); 8] {
[
(ResizeHandle::TopLeft, Vec2::new(panel.x, panel.y)),
(
ResizeHandle::Top,
Vec2::new(panel.x + panel.width * 0.5, panel.y),
),
(ResizeHandle::TopRight, Vec2::new(panel.right(), panel.y)),
(
ResizeHandle::Right,
Vec2::new(panel.right(), panel.y + panel.height * 0.5),
),
(
ResizeHandle::BottomRight,
Vec2::new(panel.right(), panel.bottom()),
),
(
ResizeHandle::Bottom,
Vec2::new(panel.x + panel.width * 0.5, panel.bottom()),
),
(ResizeHandle::BottomLeft, Vec2::new(panel.x, panel.bottom())),
(
ResizeHandle::Left,
Vec2::new(panel.x, panel.y + panel.height * 0.5),
),
]
}
fn draw_data_panel(
rect: Rect,
palette: &Palette,
scroll: &mut ScrollState,
command_palette: &CommandPalette,
snippets: &SnippetSet,
) {
draw_glass_panel(rect, 30.0, palette, palette.success, false);
draw_text_line(
"State Models",
rect.x + 26.0,
rect.y + 38.0,
24.0,
to_mq(palette.text),
);
let mut y = rect.y + 82.0;
scroll.set_bounds(133, 16);
scroll.ensure_visible(120);
draw_metric(
Rect::new(rect.x + 26.0, y, rect.width - 52.0, 70.0),
"scroll visible",
&format!("{:?}", scroll.visible_range()),
palette.accent,
palette,
);
y += 92.0;
draw_section_header(
"COMMAND PALETTE",
Rect::new(rect.x + 26.0, y, rect.width - 52.0, 18.0),
palette.warning,
palette.muted,
);
y += 32.0;
for item in command_palette.filtered_items_limited(3) {
let card = Rect::new(rect.x + 26.0, y, rect.width - 52.0, 58.0);
draw_rounded_rect(card, 18.0, rgba8(1, 5, 16, 150));
draw_text_line(
&item.title,
card.x + 16.0,
card.y + 25.0,
16.0,
to_mq(palette.text),
);
if let Some(shortcut) = &item.shortcut {
draw_text_line(
&shortcut.label(),
card.right() - 112.0,
card.y + 25.0,
13.0,
to_mq(palette.accent),
);
}
y += 70.0;
}
y += 8.0;
draw_section_header(
"SNIPPETS",
Rect::new(rect.x + 26.0, y, rect.width - 52.0, 18.0),
palette.success,
palette.muted,
);
y += 32.0;
for snippet in snippets.matches_limited("rust", 2) {
draw_pill(
Rect::new(rect.x + 26.0, y, rect.width - 52.0, 30.0),
&format!("{} -> {}", snippet.trigger, snippet.body.replace('\n', " ")),
palette.success,
palette.text,
);
y += 42.0;
}
y += 12.0;
draw_section_header(
"MODEL OUTPUT",
Rect::new(rect.x + 26.0, y, rect.width - 52.0, 18.0),
palette.accent,
palette.muted,
);
y += 32.0;
draw_model_summary(
Rect::new(
rect.x + 26.0,
y,
rect.width - 52.0,
rect.bottom() - y - 26.0,
),
palette,
);
}
fn draw_model_summary(rect: Rect, palette: &Palette) {
if rect.height < 128.0 {
return;
}
let mut focus = FocusChain::new([
FocusTarget::new("navigator"),
FocusTarget::new("disabled-button").disabled(true),
FocusTarget::new("editor"),
FocusTarget::new("prompt"),
]);
focus.set_active("editor");
let active_focus = focus.active_id().unwrap_or("none").to_string();
let next_focus = focus
.move_focus(FocusDirection::Forward)
.map(|target| target.id.clone())
.unwrap_or_else(|| "none".to_string());
let focus_detail = format!("{active_focus} -> {next_focus}");
let gap = 14.0;
let card_w = (rect.width - gap) * 0.5;
let card_h = 66.0;
let cards = [
("hit z-index", "topmost enabled", "10", palette.success),
(
"focus chain",
focus_detail.as_str(),
"wrap",
palette.warning,
),
("resize handles", "corners + edges", "8", palette.accent),
(
"scroll anchor",
"bottom pinned",
"end",
Rgba::rgb8(91, 255, 211),
),
(
"cursor kind",
"grab / pointer / text",
"3",
Rgba::rgb8(205, 130, 255),
),
(
"disabled skip",
"focus omits inactive",
"1",
palette.warning,
),
];
for (index, (title, detail, value, color)) in cards.into_iter().enumerate() {
let col = index % 2;
let row = index / 2;
let card = Rect::new(
rect.x + col as f32 * (card_w + gap),
rect.y + row as f32 * (card_h + gap),
card_w,
card_h,
);
draw_rounded_rect(card, 18.0, rgba8(1, 5, 16, 142));
draw_rounded_rect_lines(card, 18.0, 1.0, to_mq(color.with_alpha(0.28)));
draw_glow(
card.x + card.width - 28.0,
card.y + 32.0,
42.0,
color.with_alpha(0.10),
4,
);
draw_text_line(title, card.x + 16.0, card.y + 25.0, 14.0, to_mq(color));
draw_text_line(
detail,
card.x + 16.0,
card.y + 48.0,
12.0,
to_mq(palette.muted),
);
draw_text_line(
value,
card.right() - if value.len() > 2 { 58.0 } else { 42.0 },
card.y + 47.0,
if value.len() > 2 { 20.0 } else { 22.0 },
to_mq(palette.text),
);
}
let ruler = Rect::new(rect.x, rect.bottom() - 42.0, rect.width, 26.0);
draw_rounded_rect(ruler, 13.0, rgba8(1, 5, 16, 118));
draw_rounded_rect(
Rect::new(ruler.x, ruler.y, ruler.width * 0.88, ruler.height),
13.0,
to_mq(palette.accent.with_alpha(0.22)),
);
draw_text_line(
"focus, hit, and scroll models resolve before render-time drawing",
ruler.x + 14.0,
ruler.y + 18.0,
12.0,
to_mq(palette.text.with_alpha(0.78)),
);
}