another checkpoint

This commit is contained in:
echo 2026-05-22 08:54:22 +02:00
parent b0f9acdb47
commit 2160449f43
18 changed files with 2103 additions and 3014 deletions

View File

@ -20,6 +20,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Odin
uses: laytan/setup-odin@v2
@ -30,7 +32,9 @@ jobs:
run: ./build.sh
- name: Test
run: odin test tests
run: |
CLAY_DIR="$(pwd)/vendor/clay/bindings/odin/clay-odin"
odin test tests -collection:clay="$CLAY_DIR"
- name: Package
run: ./scripts/package.sh

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "odin/vendor/clay"]
path = odin/vendor/clay
url = https://github.com/nicbarker/clay.git

View File

@ -1,5 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CLAY_DIR="$SCRIPT_DIR/vendor/clay/bindings/odin/clay-odin"
mkdir -p bin
odin build src/app -out:bin/comic_odin -debug
odin build src/app \
-out:bin/comic_odin \
-debug \
-collection:clay="$CLAY_DIR"

View File

@ -9,528 +9,31 @@
"last_modified_iso": ""
},
"user_mode": 0,
"story_idea": "car race in night time tokyo",
"story_idea": "2 cars racing in down town newyork",
"story_genre": "action",
"target_audience": "general",
"art_style": "manga",
"script": {
"title": "Midnight Run",
"synopsis": "Generated comic synopsis",
"title": "",
"synopsis": "",
"characters": [
],
"pages": [
{
"page_number": 1,
"layout_type": 0,
"panels": [
{
"panel_id": "panel_001_001",
"panel_number": 1,
"shot_type": 2,
"description": "Wide shot of Tokyo skyline at night, neon lights reflecting on wet streets. A sleek black Nissan GT-R and a red Mazda RX-7 are at a traffic light, engines revving.",
"characters_present": [
],
"dialogue": [
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_002",
"panel_number": 2,
"shot_type": 2,
"description": "Close-up of the drivers gripping their steering wheels. The black GT-R driver (Kenji) has a focused, intense expression. The red RX-7 driver (Ryo) smirks confidently.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "Ready to lose, Kenji?",
"bubble_type": 0,
"emotion": 4
},
{
"speaker_id": "",
"text": "You wish.",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_003",
"panel_number": 3,
"shot_type": 2,
"description": "The traffic light turns green. Both cars launch forward, tires screeching and leaving rubber marks. Speed lines emphasize acceleration.",
"characters_present": [
],
"dialogue": [
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_004",
"panel_number": 4,
"shot_type": 2,
"description": "Shot from behind the cars as they speed through a tunnel, neon lights blurring. The GT-R is slightly ahead.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "Not bad, but I'm just warming up.",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_005",
"panel_number": 5,
"shot_type": 2,
"description": "The RX-7 drifts around a sharp corner, sparks flying from the exhaust. The GT-R follows closely.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "He's good...",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
}
]
},
{
"page_number": 2,
"layout_type": 0,
"panels": [
{
"panel_id": "panel_002_001",
"panel_number": 1,
"shot_type": 2,
"description": "Both cars race side by side on a straight stretch of elevated highway. Tokyo tower is visible in the background.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "Time to end this!",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_002",
"panel_number": 2,
"shot_type": 2,
"description": "Ryo hits a nitrous boost. The RX-7 surges ahead, engine glowing red. Kenji's eyes widen.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "What?! Nitrous?",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_003",
"panel_number": 3,
"shot_type": 2,
"description": "Kenji shifts gears and his GT-R also boosts, catching up. Their front bumpers are almost touching.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "You're crazy!",
"bubble_type": 0,
"emotion": 4
},
{
"speaker_id": "",
"text": "Let's see who blinks first!",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_004",
"panel_number": 4,
"shot_type": 2,
"description": "An oncoming truck appears in the distance, its headlights blinding. Both cars are in the same lane.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "Truck!",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_005",
"panel_number": 5,
"shot_type": 2,
"description": "At the last second, Kenji swerves left, Ryo swerves right. They split around the truck, inches away. The truck honks loudly.",
"characters_present": [
],
"dialogue": [
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_006",
"panel_number": 6,
"shot_type": 2,
"description": "Both cars cross the finish line (a banner on the road) simultaneously. They slow down, pulling over. Ryo and Kenji step out, panting.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "Tie.",
"bubble_type": 0,
"emotion": 4
},
{
"speaker_id": "",
"text": "Yeah. Next time, I'll win.",
"bubble_type": 0,
"emotion": 4
},
{
"speaker_id": "",
"text": "Keep dreaming.",
"bubble_type": 0,
"emotion": 4
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_007",
"panel_number": 7,
"shot_type": 2,
"description": "They share a grin. The city lights glow behind them. Final panel: their cars parked side by side under a streetlight.",
"characters_present": [
],
"dialogue": [
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
}
]
}
]
},
"characters": [
],
"panel_images": {
"panel_001_001": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_001_panel_001_001.png",
"width": 1024,
"height": 1024,
"seed": 1,
"prompt": "local panel 1"
},
"panel_002_006": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_011_panel_002_006.png",
"width": 1024,
"height": 1024,
"seed": 11,
"prompt": "local panel 11"
},
"panel_002_007": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_012_panel_002_007.png",
"width": 1024,
"height": 1024,
"seed": 12,
"prompt": "local panel 12"
},
"panel_002_001": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_006_panel_002_001.png",
"width": 1024,
"height": 1024,
"seed": 6,
"prompt": "local panel 6"
},
"panel_001_003": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_003_panel_001_003.png",
"width": 1024,
"height": 1024,
"seed": 3,
"prompt": "local panel 3"
},
"panel_002_004": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_009_panel_002_004.png",
"width": 1024,
"height": 1024,
"seed": 9,
"prompt": "local panel 9"
},
"panel_001_005": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_005_panel_001_005.png",
"width": 1024,
"height": 1024,
"seed": 5,
"prompt": "local panel 5"
},
"panel_002_002": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_007_panel_002_002.png",
"width": 1024,
"height": 1024,
"seed": 7,
"prompt": "local panel 7"
},
"panel_001_002": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_002_panel_001_002.png",
"width": 1024,
"height": 1024,
"seed": 2,
"prompt": "local panel 2"
},
"panel_002_005": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_010_panel_002_005.png",
"width": 1024,
"height": 1024,
"seed": 10,
"prompt": "local panel 10"
},
"panel_002_003": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_008_panel_002_003.png",
"width": 1024,
"height": 1024,
"seed": 8,
"prompt": "local panel 8"
},
"panel_001_004": {
"url": "file:///tmp/comic-gui-local-panels-5031376420/panel_004_panel_001_004.png",
"width": 1024,
"height": 1024,
"seed": 4,
"prompt": "local panel 4"
}
},
"panel_errors": {
},
"page_layouts": [
{
"page_number": 1,
"pattern_id": "grid-2x2",
"panels": [
{
"panel_id": "panel_001_001",
"panel_number": 1,
"layout_cell": {
"x": 0.02000000,
"y": 0.02000000,
"w": 0.47000000,
"h": 0.47000000
}
},
{
"panel_id": "panel_001_002",
"panel_number": 2,
"layout_cell": {
"x": 0.50999999,
"y": 0.02000000,
"w": 0.47000000,
"h": 0.47000000
}
},
{
"panel_id": "panel_001_003",
"panel_number": 3,
"layout_cell": {
"x": 0.02000000,
"y": 0.50999999,
"w": 0.47000000,
"h": 0.47000000
}
},
{
"panel_id": "panel_001_004",
"panel_number": 4,
"layout_cell": {
"x": 0.50999999,
"y": 0.50999999,
"w": 0.47000000,
"h": 0.47000000
}
}
],
"width": 2480,
"height": 3508
},
{
"page_number": 2,
"pattern_id": "dialogue-heavy",
"panels": [
{
"panel_id": "panel_001_005",
"panel_number": 5,
"layout_cell": {
"x": 0.02000000,
"y": 0.02000000,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_001",
"panel_number": 1,
"layout_cell": {
"x": 0.50999999,
"y": 0.02000000,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_002",
"panel_number": 2,
"layout_cell": {
"x": 0.02000000,
"y": 0.25999999,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_003",
"panel_number": 3,
"layout_cell": {
"x": 0.50999999,
"y": 0.25999999,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_004",
"panel_number": 4,
"layout_cell": {
"x": 0.02000000,
"y": 0.50000000,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_005",
"panel_number": 5,
"layout_cell": {
"x": 0.50999999,
"y": 0.50000000,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_006",
"panel_number": 6,
"layout_cell": {
"x": 0.02000000,
"y": 0.74000001,
"w": 0.47000000,
"h": 0.22000000
}
},
{
"panel_id": "panel_002_007",
"panel_number": 7,
"layout_cell": {
"x": 0.50999999,
"y": 0.74000001,
"w": 0.47000000,
"h": 0.22000000
}
}
],
"width": 2480,
"height": 3508
}
],
"speech_bubbles": {
@ -539,7 +42,7 @@
"page_size": 0,
"color_profile": 0,
"workflow": {
"current_step": 5,
"current_step": 0,
"completed_steps": [
],

12
odin/ols.json Normal file
View File

@ -0,0 +1,12 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json",
"enable_document_symbols": true,
"enable_hover": true,
"enable_snippets": true,
"collections": [
{
"name": "clay",
"path": "vendor/clay/bindings/odin/clay-odin"
}
]
}

View File

@ -14,7 +14,8 @@ echo "=> Building comic-odin v${VERSION} (${OS}-${ARCH})"
./build.sh
echo "=> Running test suite"
odin test tests
CLAY_DIR="$ROOT_DIR/vendor/clay/bindings/odin/clay-odin"
odin test tests -collection:clay="$CLAY_DIR"
mkdir -p dist
PKG_NAME="comic-odin-${VERSION}-${OS}-${ARCH}"

View File

@ -1,9 +1,6 @@
package gui
import "core:fmt"
import rl "vendor:raylib"
import "../core"
import "../ui"
bubble_type_name :: proc(t: core.Bubble_Type) -> string {
switch t {
@ -42,13 +39,6 @@ clamp_bubble_cursor :: proc(count, cursor: int) -> int {
return cursor
}
collect_layout_panels_for_page :: proc(layouts: []core.Page_Layout, page_cursor: int) -> []core.Page_Layout_Panel {
if len(layouts) == 0 || page_cursor < 0 || page_cursor >= len(layouts) {
return nil
}
return layouts[page_cursor].panels
}
count_bubbles_for_panel :: proc(bubbles: map[string][]core.Speech_Bubble, panel_id: string) -> int {
if bubbles == nil {
return 0
@ -59,197 +49,9 @@ count_bubbles_for_panel :: proc(bubbles: map[string][]core.Speech_Bubble, panel_
return 0
}
draw_bubbles_detail_panel :: proc(
controller: ui.App_Controller,
x, y, w, h: i32,
page_cursor: int,
panel_cursor: int,
bubble_cursor: int,
) -> (
add_clicked: bool,
delete_clicked: bool,
auto_place_clicked: bool,
new_page_cursor: int,
new_panel_cursor: int,
new_bubble_cursor: int,
edited_text: string,
edited_type: core.Bubble_Type,
type_changed: bool,
text_changed: bool,
) {
add_clicked = false
delete_clicked = false
auto_place_clicked = false
new_page_cursor = page_cursor
new_panel_cursor = panel_cursor
new_bubble_cursor = bubble_cursor
type_changed = false
text_changed = false
edited_text = ""
edited_type = .Normal
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Bubble Editor")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
layout_count := len(controller.state.page_layouts)
if layout_count == 0 {
draw_summary_line(x+18, y+46, "No layouts yet. Run Layout Auto first.", SUMMARY_HINT)
return
collect_layout_panels_for_page :: proc(layouts: []core.Page_Layout, page_cursor: int) -> []core.Page_Layout_Panel {
if len(layouts) == 0 || page_cursor < 0 || page_cursor >= len(layouts) {
return nil
}
page_idx := clamp_layout_cursor(layout_count, page_cursor)
if page_idx != page_cursor {
new_page_cursor = page_idx
}
layout := controller.state.page_layouts[page_idx]
panel_list := layout.panels
panel_count := len(panel_list)
if panel_count == 0 {
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d has no panels", layout.page_number), SUMMARY_HINT)
return
}
// Page navigation
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), SUMMARY_ACCENT)
panel_idx := clamp_bubble_cursor(panel_count, panel_cursor)
if panel_idx != panel_cursor {
new_panel_cursor = panel_idx
}
panel := panel_list[panel_idx]
// Panel info + auto-place button
draw_summary_subline(x+18, y+66, fmt.tprintf("Panel %d (id: %s)", panel.panel_number, panel.panel_id), SUMMARY_SUBLINE)
auto_btn_rec := rl.Rectangle{x = f32(x + w - 110), y = f32(y+62), width = 90, height = 22}
draw_small_button_state(auto_btn_rec, "Auto Place", true)
if button_clicked(auto_btn_rec) {
auto_place_clicked = true
}
// Bubble list
bubble_map := controller.state.speech_bubbles
bubbles_for_panel: []core.Speech_Bubble = nil
if bubble_map != nil {
if slice, ok := bubble_map[panel.panel_id]; ok {
bubbles_for_panel = slice
}
}
bubble_count := len(bubbles_for_panel)
bubble_idx := clamp_bubble_cursor(bubble_count, bubble_cursor)
if bubble_count > 0 && bubble_idx != bubble_cursor {
new_bubble_cursor = bubble_idx
}
// Bubble list header
list_y := y + 90
draw_summary_line(x+18, list_y, fmt.tprintf("Bubbles: %d", bubble_count), SUMMARY_ACCENT)
// Add button
add_btn_rec := rl.Rectangle{x = f32(x + w - 70), y = f32(list_y-4), width = 50, height = 20}
draw_small_button_state(add_btn_rec, "Add", true)
if button_clicked(add_btn_rec) {
add_clicked = true
}
// Bubble rows
row_start_y := list_y + 22
row_h: i32 = 20
max_rows: int = int(h - 120) / int(row_h)
if max_rows < 1 {
max_rows = 1
}
// Scroll window for bubble list
scroll_start := 0
if bubble_idx >= max_rows {
scroll_start = bubble_idx - max_rows + 1
}
scroll_end := scroll_start + max_rows
if scroll_end > bubble_count {
scroll_end = bubble_count
scroll_start = scroll_end - max_rows
if scroll_start < 0 {
scroll_start = 0
}
}
row: i32 = 0
for i in scroll_start..<scroll_end {
b := bubbles_for_panel[i]
mark := " "
row_color := TEXT_TERTIARY
if i == bubble_idx {
mark = ">"
row_color = TEXT_PRIMARY
}
row_rec := rl.Rectangle{x = f32(x+18), y = f32(row_start_y+row*row_h), width = f32(w-90), height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
new_bubble_cursor = i
}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
row_color = TEXT_SECONDARY
}
// Delete button on hover/selected
del_rec := rl.Rectangle{x = f32(x+w-28), y = f32(row_start_y+row*row_h), width = 18, height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), del_rec) && rl.IsMouseButtonPressed(.LEFT) && i == bubble_idx {
delete_clicked = true
}
if i == bubble_idx {
draw_text_fitted("x", x+w-26, row_start_y+row*row_h+3, 12, 14, 7, ERROR)
}
preview := b.text
if len(preview) > 30 {
preview = preview[:30]
preview = fmt.tprintf("%s...", preview)
}
if len(preview) == 0 {
preview = "(empty)"
}
draw_summary_subline(x+18, row_start_y+row*row_h+2,
fit_text_for_width(fmt.tprintf("%s [%s] %s", mark, bubble_type_name(b.type), preview), int(w-100), 7), row_color)
row += 1
}
// Selected bubble editor
if bubble_count > 0 && bubble_idx < bubble_count {
editor_y := row_start_y + row*row_h + 8
if editor_y < y+h-80 {
selected := bubbles_for_panel[bubble_idx]
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(editor_y-6), width = f32(w-24), height = 70})
draw_summary_line(x+18, editor_y, fmt.tprintf("Editing: %s", bubble_type_name(selected.type)), SUMMARY_ACCENT)
// Type selector buttons
type_y := editor_y + 20
type_idx: i32 = 0
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
type_w: i32 = 60
type_spacing: i32 = 4
for t in types {
t_rec := rl.Rectangle{x = f32(x)+18+f32(int(type_idx)*int(type_w+type_spacing)), y = f32(type_y), width = f32(type_w), height = 18}
draw_nav_item(t_rec, bubble_type_name(t), selected.type == t)
if button_clicked(t_rec) && selected.type != t {
edited_type = t
type_changed = true
}
type_idx += 1
}
// Text preview
text_y := type_y + 22
draw_summary_subline(x+18, text_y, fit_text_for_width(fmt.tprintf("text: %s", selected.text), int(w-36), 7), SUMMARY_SUBLINE)
}
}
if bubble_count == 0 {
draw_summary_line(x+18, row_start_y, "No bubbles for this panel. Click Add or Auto Place.", SUMMARY_HINT)
}
return
}
return layouts[page_cursor].panels[:]
}

View File

@ -0,0 +1,409 @@
package gui
import clay "clay:."
import "base:runtime"
import "core:fmt"
import "core:math"
import "core:strings"
import rl "vendor:raylib"
import "../shared"
// --- Font IDs (indices into raylib_fonts) ---
CLAY_FONT_BODY :: u16(0)
CLAY_FONT_TITLE :: u16(1)
CLAY_FONT_MONO :: u16(2)
// --- Font registry (shared between layout and renderer) ---
Raylib_Font :: struct {
fontId: u16,
font: rl.Font,
}
raylib_fonts: [dynamic]Raylib_Font
// --- Clay Color Palette (modernized dark theme) ---
// Named with CLAY_ prefix to avoid collision with existing theme.odin
CLAY_BG_BASE :: clay.Color{13, 13, 18, 255}
CLAY_BG_SIDEBAR :: clay.Color{18, 18, 26, 255}
CLAY_BG_TOPBAR :: clay.Color{22, 22, 32, 255}
CLAY_BG_CARD :: clay.Color{28, 28, 40, 255}
CLAY_BG_CARD_ALT :: clay.Color{24, 24, 36, 255}
CLAY_BG_STRIP :: clay.Color{32, 32, 46, 255}
CLAY_BG_OVERLAY :: clay.Color{0, 0, 0, 180}
CLAY_BG_INPUT :: clay.Color{20, 20, 30, 255}
CLAY_BORDER_CARD :: clay.Color{50, 50, 70, 255}
CLAY_BORDER_SUBTLE :: clay.Color{40, 40, 58, 255}
CLAY_BORDER_DIVIDER :: clay.Color{36, 36, 52, 255}
CLAY_ACCENT :: clay.Color{99, 102, 241, 255}
CLAY_ACCENT_HOVER :: clay.Color{124, 127, 255, 255}
CLAY_ACCENT_MUTED :: clay.Color{79, 70, 229, 200}
CLAY_ACCENT_SURFACE:: clay.Color{67, 56, 202, 40}
CLAY_ACCENT_GLOW :: clay.Color{99, 102, 241, 60}
CLAY_TEXT_PRIMARY :: clay.Color{240, 240, 245, 255}
CLAY_TEXT_SECONDARY :: clay.Color{170, 170, 190, 255}
CLAY_TEXT_TERTIARY :: clay.Color{110, 110, 135, 255}
CLAY_TEXT_DISABLED :: clay.Color{70, 70, 90, 255}
CLAY_TEXT_BRIGHT := clay.Color{255, 255, 255, 255}
CLAY_SUCCESS :: clay.Color{34, 197, 94, 255}
CLAY_SUCCESS_DIM :: clay.Color{34, 197, 94, 100}
CLAY_WARNING :: clay.Color{234, 179, 8, 255}
CLAY_WARNING_DIM :: clay.Color{234, 179, 8, 100}
CLAY_ERROR :: clay.Color{239, 68, 68, 255}
CLAY_ERROR_DIM :: clay.Color{239, 68, 68, 100}
CLAY_BTN_DEFAULT :: clay.Color{42, 42, 58, 255}
CLAY_BTN_DEFAULT_HOVER :: clay.Color{55, 55, 72, 255}
CLAY_BTN_SOFT :: clay.Color{55, 50, 120, 255}
CLAY_BTN_SOFT_HOVER :: clay.Color{70, 65, 140, 255}
CLAY_BTN_DANGER :: clay.Color{185, 28, 28, 255}
CLAY_BTN_DANGER_HOVER :: clay.Color{210, 40, 40, 255}
CLAY_BTN_DISABLED :: clay.Color{30, 30, 42, 255}
CLAY_NAV_ACTIVE :: clay.Color{99, 102, 241, 255}
CLAY_NAV_ACTIVE_BG :: clay.Color{67, 56, 202, 25}
CLAY_NAV_HOVER_BG :: clay.Color{40, 40, 58, 255}
CLAY_INPUT_BORDER :: clay.Color{55, 55, 75, 255}
CLAY_INPUT_FOCUS :: clay.Color{99, 102, 241, 255}
CLAY_PROGRESS_TRACK :: clay.Color{30, 30, 42, 255}
CLAY_PROGRESS_FILL := clay.Color{99, 102, 241, 255}
// --- Spacing (4px grid) ---
CLAY_SPACE_4 :: u16(4)
CLAY_SPACE_8 :: u16(8)
CLAY_SPACE_12 :: u16(12)
CLAY_SPACE_16 :: u16(16)
CLAY_SPACE_20 :: u16(20)
CLAY_SPACE_24 :: u16(24)
CLAY_SPACE_32 :: u16(32)
CLAY_SPACE_40 :: u16(40)
CLAY_SPACE_48 :: u16(48)
// --- Font Sizes ---
CLAY_FONT_SIZE_SM :: u16(13)
CLAY_FONT_SIZE_MD :: u16(15)
CLAY_FONT_SIZE_LG :: u16(18)
CLAY_FONT_SIZE_XL :: u16(22)
CLAY_FONT_SIZE_2XL:: u16(28)
// --- Corner Radius (px for Clay) ---
CLAY_RADIUS_SM :: f32(4)
CLAY_RADIUS_MD :: f32(8)
CLAY_RADIUS_LG :: f32(12)
CLAY_RADIUS_XL :: f32(16)
// --- Fractional radius (for DrawRectangleRounded compat) ---
CLAY_RADIUS_FRAC_BTN :: f32(0.32)
CLAY_RADIUS_FRAC_PILL :: f32(0.50)
CLAY_RADIUS_FRAC_CARD :: f32(0.14)
// --- Color conversion ---
clay_color_to_rl :: proc(color: clay.Color) -> rl.Color {
return {u8(color[0]), u8(color[1]), u8(color[2]), u8(color[3])}
}
// --- Clay Error Handler ---
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = runtime.default_context()
fmt.eprintf("CLAY ERROR: %v\n", errorData)
}
// --- Clay State ---
Clay_State :: struct {
arena_memory: [dynamic]u8,
font_default: rl.Font,
font_title: rl.Font,
font_mono: rl.Font,
}
clay_state: Clay_State
// --- Init / Shutdown ---
clay_init :: proc(screen_w: i32, screen_h: i32) {
min_memory_size := clay.MinMemorySize()
clay_state.arena_memory = make([dynamic]u8, min_memory_size)
arena: clay.Arena = clay.CreateArenaWithCapacityAndMemory(uint(min_memory_size), raw_data(clay_state.arena_memory))
clay.Initialize(arena, {f32(screen_w), f32(screen_h)}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(clay_measure_text, nil)
clay_state.font_default = rl.GetFontDefault()
clay_state.font_title = rl.GetFontDefault()
clay_state.font_mono = rl.GetFontDefault()
raylib_fonts = make([dynamic]Raylib_Font, 0, 3)
append(&raylib_fonts, Raylib_Font{fontId = CLAY_FONT_BODY, font = clay_state.font_default})
append(&raylib_fonts, Raylib_Font{fontId = CLAY_FONT_TITLE, font = clay_state.font_title})
append(&raylib_fonts, Raylib_Font{fontId = CLAY_FONT_MONO, font = clay_state.font_mono})
}
clay_shutdown :: proc() {
delete(raylib_fonts)
delete(clay_state.arena_memory)
}
clay_update_dimensions :: proc(screen_w: i32, screen_h: i32) {
clay.SetLayoutDimensions({f32(screen_w), f32(screen_h)})
}
clay_update_input :: proc() {
mouse_pos := rl.GetMousePosition()
is_down := rl.IsMouseButtonDown(.LEFT)
clay.SetPointerState({mouse_pos.x, mouse_pos.y}, is_down)
scroll_delta := rl.GetMouseWheelMove()
clay.UpdateScrollContainers(true, {f32(scroll_delta) * 40, 0}, rl.GetFrameTime())
}
// --- Text Measurement ---
clay_measure_text :: proc "c" (text: clay.StringSlice, config: ^clay.TextElementConfig, userData: rawptr) -> clay.Dimensions {
context = runtime.default_context()
if config.fontId >= u16(len(raylib_fonts)) {
return {width = f32(text.length) * f32(config.fontSize) * 0.6, height = f32(config.fontSize)}
}
line_width: f32 = 0
font := raylib_fonts[config.fontId].font
text_str := string(text.chars[:text.length])
for i in 0 ..< len(text_str) {
ch := text_str[i]
glyph_index := rl.GetGlyphIndex(font, rune(ch))
if glyph_index < 0 || glyph_index >= i32(font.glyphCount) {
line_width += f32(config.fontSize) * 0.6
continue
}
glyph := font.glyphs[glyph_index]
if glyph.advanceX != 0 {
line_width += f32(glyph.advanceX)
} else if glyph_index < i32(font.glyphCount) {
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
}
}
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
total_spacing := f32(len(text_str)) * f32(config.letterSpacing)
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
}
// --- Rendering ---
clay_raylib_render :: proc(render_commands: ^clay.ClayArray(clay.RenderCommand)) {
for i in 0 ..< render_commands.length {
render_command := clay.RenderCommandArray_Get(render_commands, i)
bounds := render_command.boundingBox
switch render_command.commandType {
case .None:
case .Text:
config := render_command.renderData.text
text := string(config.stringContents.chars[:config.stringContents.length])
cstr_text := strings.clone_to_cstring(text)
if config.fontId < u16(len(raylib_fonts)) {
font := raylib_fonts[config.fontId].font
rl.DrawTextEx(
font,
cstr_text,
{bounds.x, bounds.y},
f32(config.fontSize),
f32(config.letterSpacing),
clay_color_to_rl(config.textColor),
)
}
case .Image:
config := render_command.renderData.image
texture := (^rl.Texture2D)(config.imageData)
rl.DrawTextureEx(texture^, {bounds.x, bounds.y}, 0, bounds.width / f32(texture.width), rl.WHITE)
case .ScissorStart:
rl.BeginScissorMode(
i32(math.round(bounds.x)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width)),
i32(math.round(bounds.height)),
)
case .ScissorEnd:
rl.EndScissorMode()
case .Rectangle:
config := render_command.renderData.rectangle
if config.cornerRadius.topLeft > 0 {
radius := (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height)
rl.DrawRectangleRounded(
{bounds.x, bounds.y, bounds.width, bounds.height},
radius, 8,
clay_color_to_rl(config.backgroundColor),
)
} else {
rl.DrawRectangle(
i32(math.round(bounds.x)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width)),
i32(math.round(bounds.height)),
clay_color_to_rl(config.backgroundColor),
)
}
case .Border:
config := render_command.renderData.border
if config.width.left > 0 {
rl.DrawRectangle(
i32(math.round(bounds.x)),
i32(math.round(bounds.y + config.cornerRadius.topLeft)),
i32(config.width.left),
i32(math.round(bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft)),
clay_color_to_rl(config.color),
)
}
if config.width.right > 0 {
rl.DrawRectangle(
i32(math.round(bounds.x + bounds.width - f32(config.width.right))),
i32(math.round(bounds.y + config.cornerRadius.topRight)),
i32(config.width.right),
i32(math.round(bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight)),
clay_color_to_rl(config.color),
)
}
if config.width.top > 0 {
rl.DrawRectangle(
i32(math.round(bounds.x + config.cornerRadius.topLeft)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight)),
i32(config.width.top),
clay_color_to_rl(config.color),
)
}
if config.width.bottom > 0 {
rl.DrawRectangle(
i32(math.round(bounds.x + config.cornerRadius.bottomLeft)),
i32(math.round(bounds.y + bounds.height - f32(config.width.bottom))),
i32(math.round(bounds.width - config.cornerRadius.bottomLeft - config.cornerRadius.bottomRight)),
i32(config.width.bottom),
clay_color_to_rl(config.color),
)
}
if config.cornerRadius.topLeft > 0 {
rl.DrawRing(
{math.round(bounds.x + config.cornerRadius.topLeft), math.round(bounds.y + config.cornerRadius.topLeft)},
math.round(config.cornerRadius.topLeft - f32(config.width.top)),
config.cornerRadius.topLeft,
180, 270, 10, clay_color_to_rl(config.color),
)
}
if config.cornerRadius.topRight > 0 {
rl.DrawRing(
{math.round(bounds.x + bounds.width - config.cornerRadius.topRight), math.round(bounds.y + config.cornerRadius.topRight)},
math.round(config.cornerRadius.topRight - f32(config.width.top)),
config.cornerRadius.topRight,
270, 360, 10, clay_color_to_rl(config.color),
)
}
if config.cornerRadius.bottomLeft > 0 {
rl.DrawRing(
{math.round(bounds.x + config.cornerRadius.bottomLeft), math.round(bounds.y + bounds.height - config.cornerRadius.bottomLeft)},
math.round(config.cornerRadius.bottomLeft - f32(config.width.top)),
config.cornerRadius.bottomLeft,
90, 180, 10, clay_color_to_rl(config.color),
)
}
if config.cornerRadius.bottomRight > 0 {
rl.DrawRing(
{math.round(bounds.x + bounds.width - config.cornerRadius.bottomRight), math.round(bounds.y + bounds.height - config.cornerRadius.bottomRight)},
math.round(config.cornerRadius.bottomRight - f32(config.width.bottom)),
config.cornerRadius.bottomRight,
0.1, 90, 10, clay_color_to_rl(config.color),
)
}
case .OverlayColorStart:
config := render_command.renderData.overlayColor
rl.DrawRectangle(
i32(math.round(bounds.x)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width)),
i32(math.round(bounds.height)),
clay_color_to_rl(config.color),
)
case .OverlayColorEnd:
case .Custom:
}
}
}
// --- Layout helpers (procs because Clay helpers require runtime evaluation) ---
clay_card_layout :: proc() -> clay.LayoutConfig {
return clay.LayoutConfig {
layoutDirection = .TopToBottom,
sizing = {
width = clay.SizingGrow({}),
height = clay.SizingFit({}),
},
padding = clay.PaddingAll(16),
childGap = CLAY_SPACE_8,
}
}
clay_row_layout :: proc() -> clay.LayoutConfig {
return clay.LayoutConfig {
layoutDirection = .LeftToRight,
sizing = {
width = clay.SizingGrow({}),
height = clay.SizingFit({}),
},
childGap = CLAY_SPACE_8,
}
}
clay_input_layout :: proc() -> clay.LayoutConfig {
return clay.LayoutConfig {
layoutDirection = .LeftToRight,
sizing = {
width = clay.SizingGrow({}),
height = clay.SizingFixed(36),
},
padding = {top = 8, right = 12, bottom = 8, left = 12},
}
}
clay_button_layout :: proc() -> clay.LayoutConfig {
return clay.LayoutConfig {
layoutDirection = .LeftToRight,
sizing = {
width = clay.SizingFit({}),
height = clay.SizingFixed(36),
},
padding = {top = 8, right = 16, bottom = 8, left = 16},
childAlignment = {x = .Center, y = .Center},
}
}
// --- Style configs ---
clay_card_style :: proc() -> clay.ElementDeclaration {
return {
layout = clay_card_layout(),
backgroundColor = CLAY_BG_CARD,
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(1)},
}
}
// --- Text helpers ---
clay_body_text :: proc(text: string, color: clay.Color = CLAY_TEXT_PRIMARY, size: u16 = CLAY_FONT_SIZE_MD) {
clay.Text(text, {fontId = CLAY_FONT_BODY, fontSize = size, textColor = color})
}
clay_title_text :: proc(text: string, color: clay.Color = CLAY_TEXT_BRIGHT, size: u16 = CLAY_FONT_SIZE_LG) {
clay.Text(text, {fontId = CLAY_FONT_TITLE, fontSize = size, textColor = color})
}
clay_muted_text :: proc(text: string) {
clay.Text(text, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = CLAY_TEXT_TERTIARY})
}

View File

@ -1,177 +0,0 @@
package gui
import "core:fmt"
import rl "vendor:raylib"
button_clicked :: proc(rec: rl.Rectangle) -> bool {
if !rl.IsMouseButtonPressed(.LEFT) {
return false
}
return rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
}
draw_button :: proc(rec: rl.Rectangle, label: string) {
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := BTN_BG
border := BTN_BORDER
text := BTN_TEXT
if hover {
bg = BTN_BG_HOVER
border = BTN_BORDER_HOVER
}
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
}
draw_button_primary :: proc(rec: rl.Rectangle, label: string) {
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := ACCENT
border := ACCENT_MUTED
if hover {
bg = ACCENT_HOVER
border = ACCENT
}
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, TEXT_BRIGHT)
}
draw_button_danger :: proc(rec: rl.Rectangle, label: string) {
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := DANGER_BG
border := DANGER_BORDER
if hover {
bg = DANGER_BG_HOVER
border = DANGER_BORDER
}
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, TEXT_BRIGHT)
}
draw_button_warning :: proc(rec: rl.Rectangle, label: string) {
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := WARN_BTN_BG
border := WARN_BTN_BORDER
text := WARN_BTN_TEXT
if hover {
bg = WARN_BTN_BG_HOVER
border = WARN_BTN_BORDER
}
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
}
draw_button_soft_accent :: proc(rec: rl.Rectangle, label: string) {
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := BTN_SOFT_BG
border := BTN_SOFT_BORDER
text := BTN_SOFT_TEXT
if hover {
bg = BTN_SOFT_BG_HOVER
border = BTN_SOFT_BORDER
}
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
}
draw_button_state :: proc(rec: rl.Rectangle, label: string, enabled: bool) {
if !enabled {
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, BTN_DISABLED_BG)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, BTN_DISABLED_BORDER)
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, BTN_DISABLED_TEXT)
return
}
draw_button(rec, label)
}
draw_small_button_state :: proc(rec: rl.Rectangle, label: string, enabled: bool) {
if !enabled {
rl.DrawRectangleRounded(rec, 0.20, 6, BTN_DISABLED_BG)
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 6, 1.0, BTN_DISABLED_BORDER)
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-12, 7, BTN_DISABLED_TEXT)
return
}
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
bg := SBTN_BG
border := SBTN_BORDER
if hover {
bg = SBTN_BG_HOVER
border = SBTN_BORDER_HOVER
}
rl.DrawRectangleRounded(rec, 0.20, 6, bg)
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 6, 1.0, border)
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-12, 7, SBTN_TEXT)
}
draw_small_button :: proc(rec: rl.Rectangle, label: string) {
draw_small_button_state(rec, label, true)
}
button_readiness_hint :: proc(mouse: rl.Vector2, panels_btn, layout_btn, export_btn: rl.Rectangle, can_generate_panels, can_layout, can_export: bool) -> string {
if rl.CheckCollisionPointRec(mouse, panels_btn) && !can_generate_panels {
return "Panels requires a generated script"
}
if rl.CheckCollisionPointRec(mouse, layout_btn) && !can_layout {
return "Layout requires generated panels"
}
if rl.CheckCollisionPointRec(mouse, export_btn) && !can_export {
return "Export requires panels + layout"
}
return ""
}
draw_button_recommended :: proc(rec: rl.Rectangle, label: string) {
halo := rl.Rectangle{x = rec.x-2, y = rec.y-2, width = rec.width+4, height = rec.height+4}
rl.DrawRectangleRounded(halo, RADIUS_BUTTON, 8, RECOMMEND_HALO_FILL)
rl.DrawRectangleRoundedLinesEx(halo, RADIUS_BUTTON, 8, 1.4, RECOMMEND_HALO_BORDER)
draw_button(rec, label)
}
draw_nav_item :: proc(rec: rl.Rectangle, label: string, active: bool) {
bg := NAV_BG
border := NAV_BORDER
text := NAV_TEXT
if active {
bg = NAV_ACTIVE_BG
border = NAV_ACTIVE_BG
text = NAV_ACTIVE_TEXT
} else if rl.CheckCollisionPointRec(rl.GetMousePosition(), rec) {
bg = NAV_BG_HOVER
border = NAV_BORDER_HOVER
}
rl.DrawRectangleRounded(rec, RADIUS_NAV, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_NAV, 8, 1.0, border)
if active {
rl.DrawRectangleRounded(rl.Rectangle{x = rec.x+2, y = rec.y+4, width = 4, height = rec.height-8}, 0.5, 8, NAV_ACTIVE_BAR)
}
label_x := i32(rec.x) + 8
label_w := int(rec.width) - 16
if active {
label_x = i32(rec.x) + 14
label_w = int(rec.width) - 22
}
draw_text_fitted(label, label_x, i32(rec.y)+6, 18, label_w, 8, text)
}
draw_input_field :: proc(rec: rl.Rectangle, value: string, selected: bool) {
bg := INPUT_BG
border := INPUT_BORDER
if selected {
halo := rl.Rectangle{x = rec.x - 2, y = rec.y - 2, width = rec.width + 4, height = rec.height + 4}
rl.DrawRectangleRounded(halo, RADIUS_INPUT, 8, INPUT_FOCUS_RING)
rl.DrawRectangleRoundedLinesEx(halo, RADIUS_INPUT, 8, 1.0, INPUT_FOCUS_BORDER)
bg = INPUT_FOCUS_BG
border = INPUT_FOCUS_BORDER
}
rl.DrawRectangleRounded(rec, RADIUS_INPUT, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_INPUT, 8, 1.2, border)
if !selected {
draw_text_fitted(value, i32(rec.x)+8, i32(rec.y)+6, 18, int(rec.width)-16, 8, INPUT_TEXT)
return
}
rl.DrawText(fmt.ctprintf("%s", value), i32(rec.x)+8, i32(rec.y)+6, 18, INPUT_TEXT_FOCUS)
}

View File

@ -113,28 +113,3 @@ pop_char :: proc(dst: ^string) {
}
dst^ = dst^[:len(dst^)-1]
}
recommended_label_from_hint :: proc(hint: string) -> string {
switch hint {
case "generate script":
return "Generate Script"
case "generate script local":
return "Generate Script Local"
case "generate panels local":
return "Generate Panels Local"
case "layout auto":
return "Layout"
case "export pdf":
return "Export"
}
return "Next"
}
pending_action_name :: proc(a: Pending_Confirm_Action) -> string {
switch a {
case .Reset_Project: return "reset project"
case .Open_Project: return "open project"
case .None: return "continue"
}
return "continue"
}

View File

@ -1,38 +1,6 @@
package gui
import "core:fmt"
import "core:strings"
import rl "vendor:raylib"
draw_action_log :: proc(log: Action_Log, x, y, max_visible: i32, oldest_first: bool) {
now := rl.GetTime()
max_lines := len(log.entries)
if log.count < max_lines {
max_lines = log.count
}
if max_visible > 0 && int(max_visible) < max_lines {
max_lines = int(max_visible)
}
for line in 0..<max_lines {
idx := 0
if oldest_first {
start := log.count - max_lines
idx = (start + line) % len(log.entries)
} else {
idx = (log.count - 1 - line) % len(log.entries)
}
if idx < 0 {
idx += len(log.entries)
}
row_y := y + i32(line*22)
if line % 2 == 0 {
rl.DrawRectangleRounded(rl.Rectangle{x = f32(x-4), y = f32(row_y-2), width = 442, height = 20}, 0.2, 6, LOG_ROW_ALT)
}
age := now - log.entry_times[idx]
line_text := fmt.tprintf("[%2.0fs] %s", age, log.entries[idx])
draw_text_fitted(line_text, x, row_y, 15, 430, 7, LOG_TEXT)
}
}
is_error_message :: proc(msg: string) -> bool {
return strings.contains(msg, "failed") || strings.contains(msg, "blocked") || strings.contains(msg, "No script")
@ -40,112 +8,4 @@ is_error_message :: proc(msg: string) -> bool {
is_warning_message :: proc(msg: string) -> bool {
return strings.contains(msg, "Unsaved") || strings.contains(msg, "Confirm") || strings.contains(msg, "requires") || strings.contains(msg, "before") || strings.contains(msg, "Cancelled")
}
status_text_color :: proc(msg: string) -> rl.Color {
if is_error_message(msg) {
return ERROR
}
if is_warning_message(msg) {
return WARNING
}
return SUCCESS
}
toast_bg_color :: proc(msg: string) -> rl.Color {
if is_error_message(msg) {
return TOAST_ERROR
}
if is_warning_message(msg) {
return TOAST_WARNING
}
return TOAST_SUCCESS
}
draw_toast :: proc(log: Action_Log, x, y, w: i32) {
if log.count == 0 {
return
}
age := rl.GetTime() - log.last_push_at
if age > 2.8 {
return
}
idx := (log.count - 1) % len(log.entries)
if idx < 0 {
idx += len(log.entries)
}
msg := log.entries[idx]
bg := toast_bg_color(msg)
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 34}
shadow := rl.Rectangle{x = f32(x), y = f32(y + 2), width = f32(w), height = 34}
rl.DrawRectangleRounded(shadow, RADIUS_TOAST, 8, TOAST_SHADOW)
rl.DrawRectangleRounded(rec, RADIUS_TOAST, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_TOAST, 8, 1.0, TOAST_BORDER)
draw_text_fitted(msg, x+10, y+8, 16, int(w-20), 8, TEXT_BRIGHT)
}
draw_help_line :: proc(x, y: i32, text: string) {
draw_text_fitted(text, x, y, 16, 820, 8, HELP_LINE)
}
draw_help_overlay :: proc() {
sw := rl.GetScreenWidth()
sh := rl.GetScreenHeight()
rec := rl.Rectangle{x = f32((sw-860)/2), y = f32((sh-642)/2), width = 860, height = 642}
rl.DrawRectangle(0, 0, sw, sh, BG_OVERLAY)
draw_card(rec)
x := i32(rec.x) + 30
y := i32(rec.y) + 28
rl.DrawText("Keyboard Shortcuts", x, y, 28, HELP_TITLE)
rl.DrawText("Navigation", x, y+44, 20, HELP_SECTION)
draw_help_line(x, y+72, "1..8 screens | TAB fields | click to focus | F11 pages | F12 project")
rl.DrawText("Core Actions", x, y+106, 20, HELP_SECTION)
draw_help_line(x, y+134, "F5 script F6 panels F7 layout F8 export F9 next F10 auto-all")
draw_help_line(x, y+160, "Ctrl+S save | Ctrl+O open | Ctrl+N new | Ctrl+E export | Ctrl+H/J summary")
rl.DrawText("Clipboard + Logs", x, y+196, 20, HELP_SECTION)
draw_help_line(x, y+224, "Ctrl+L clear log | Ctrl+Shift+L copy log | Ctrl+Shift+T/B log view | Ctrl+Shift+Z reset")
draw_help_line(x, y+248, "Ctrl+Shift+C status")
draw_help_line(x, y+272, "Ctrl+Shift+Y diag copy | Ctrl+Shift+R diag file | Ctrl+Shift+W report")
draw_help_line(x, y+296, "Ctrl+0 reset helpers | Ctrl+V paste | Ctrl+Shift+I copy | Ctrl+Backspace clear")
rl.DrawText("Paths", x, y+332, 20, HELP_SECTION)
draw_help_line(x, y+360, "Ctrl+Shift+X copy export | P preset | D exp-from-project | G proj-from-export")
draw_help_line(x, y+386, "Ctrl+Shift+J fix project | F fix export | K/M quick-fix P/E | U fix all")
rl.DrawText("Autosave", x, y+422, 20, HELP_SECTION)
draw_help_line(x, y+450, "Ctrl+Shift+A toggle | Ctrl+-/= adjust | Ctrl+7/8/9 set 15/30/60")
rl.DrawText("Safety", x, y+486, 20, HELP_SECTION)
draw_help_line(x, y+514, "Dirty guard: Shift-click New/Open | Keyboard confirm: Ctrl+Shift+N / Ctrl+Shift+O")
rl.DrawText("Close help: Esc or /", x, y+542, 18, HELP_CLOSE)
}
draw_sidebar_shortcut_line :: proc(x, y: i32, text: string, c: rl.Color) {
draw_text_fitted(text, x, y, 14, 220, 7, c)
}
draw_sidebar_shortcuts :: proc(screen_h: i32) {
base_y := screen_h - 280
if base_y < 120 {
base_y = 120
}
draw_card(rl.Rectangle{x = 14, y = f32(base_y), width = 236, height = 210})
rl.DrawText("Quick Keys", 26, base_y+12, 18, SIDEBAR_TITLE)
draw_sidebar_shortcut_line(26, base_y+36, "F5/F6/F7/F8 generate/layout/export", SIDEBAR_TEXT)
draw_sidebar_shortcut_line(26, base_y+54, "Ctrl+S save Ctrl+O open", SIDEBAR_TEXT)
draw_sidebar_shortcut_line(26, base_y+72, "Ctrl+N new", SIDEBAR_TEXT)
draw_sidebar_shortcut_line(26, base_y+90, "F9 next F10 auto-all", SIDEBAR_TEXT)
draw_sidebar_shortcut_line(26, base_y+108, "/ full shortcut help", SIDEBAR_TEXT)
draw_sidebar_shortcut_line(26, base_y+140, "Press / for all shortcuts", SIDEBAR_FOOTER)
}
draw_confirm_overlay :: proc(action: Pending_Confirm_Action) {
sw := rl.GetScreenWidth()
sh := rl.GetScreenHeight()
rec := rl.Rectangle{x = f32((sw-520)/2), y = f32((sh-230)/2), width = 520, height = 230}
rl.DrawRectangle(0, 0, sw, sh, BG_OVERLAY)
draw_card(rec)
rl.DrawRectangleRounded(rl.Rectangle{x = rec.x, y = rec.y, width = rec.width, height = 8}, 0.08, 8, CONFIRM_ACCENT)
x := i32(rec.x) + 30
y := i32(rec.y) + 34
rl.DrawText("Confirm destructive action", x, y, 26, CONFIRM_TITLE)
draw_text_fitted(fmt.tprintf("You have unsaved changes. Do you want to %s?", pending_action_name(action)), x, y+42, 18, 470, 8, CONFIRM_BODY)
rl.DrawText("Enter/Y confirm • Esc/N cancel", x, y+72, 16, CONFIRM_HINT)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -109,19 +109,6 @@ toggle_log_order_with_message :: proc(log_oldest_first: ^bool) -> string {
return fmt.aprintf("Log order: %s first", order)
}
selected_field_value :: proc(selected_field: int, state: core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: string) -> string {
switch selected_field {
case 0: return state.story_idea
case 1: return state.story_genre
case 2: return state.target_audience
case 3: return export_path
case 4: return local_script_pages
case 5: return project_path
case 6: return autosave_interval_text
}
return ""
}
clear_selected_field :: proc(selected_field: int, state: ^core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: ^string) -> bool {
switch selected_field {
case 0:

View File

@ -1,37 +1,8 @@
package gui
import "core:fmt"
import rl "vendor:raylib"
import "../core"
import "../ui"
draw_stat_chip :: proc(x, y: i32, label: string, value: int) {
rec := rl.Rectangle{x = f32(x), y = f32(y), width = 80, height = 28}
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, CHIP_BG)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, CHIP_BORDER)
draw_text_fitted(label, x+10, y+8, 12, 40, 6, CHIP_TEXT)
draw_text_fitted(fmt.tprintf("%d", value), x+56, y+6, 14, 20, 7, TEXT_PRIMARY)
}
draw_readiness_chip :: proc(x, y, w: i32, label: string, ok: bool) {
bg := UNREADY_BG
border := UNREADY_BORDER
fg := UNREADY_TEXT
if ok {
bg = READY_BG
border = READY_BORDER
fg = READY_TEXT
}
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 26}
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, border)
prefix := "○"
if ok {
prefix = "●"
}
draw_text_fitted(fmt.tprintf("%s %s", prefix, label), x+10, y+7, 12, int(w-20), 6, fg)
}
ready_stage_count :: proc(controller: ui.App_Controller) -> (ready: int, total: int) {
script_ok := len(controller.state.script.pages) > 0
panels_ok := len(controller.state.panel_images) > 0
@ -45,27 +16,6 @@ ready_stage_count :: proc(controller: ui.App_Controller) -> (ready: int, total:
return ready, 4
}
draw_progress_bar :: proc(x, y, w: i32, progress: f32) {
track := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 12}
rl.DrawRectangleRounded(track, RADIUS_BAR, 8, PROGRESS_TRACK)
fill_w := i32(f32(w) * progress)
if fill_w > 0 {
fill := rl.Rectangle{x = f32(x), y = f32(y), width = f32(fill_w), height = 12}
rl.DrawRectangleRounded(fill, RADIUS_BAR, 8, PROGRESS_FILL)
}
}
draw_readiness_row :: proc(controller: ui.App_Controller, x, y: i32) {
script_ok := len(controller.state.script.pages) > 0
panels_ok := len(controller.state.panel_images) > 0
layout_ok := len(controller.state.page_layouts) > 0
export_ok := panels_ok && layout_ok
draw_readiness_chip(x, y, 120, "Script", script_ok)
draw_readiness_chip(x+126, y, 120, "Panels", panels_ok)
draw_readiness_chip(x+252, y, 120, "Layout", layout_ok)
draw_readiness_chip(x+378, y, 120, "Export", export_ok)
}
export_block_reason :: proc(state: core.Comic_State) -> string {
if len(state.panel_images) == 0 && len(state.page_layouts) == 0 {
return "need panels + layout"
@ -79,139 +29,6 @@ export_block_reason :: proc(state: core.Comic_State) -> string {
return ""
}
draw_screen_summary :: proc(controller: ui.App_Controller, export_path: string, x, y, w: i32, opts: Summary_View_Options) {
draw_card(rl.Rectangle{x = f32(x-18), y = f32(y-12), width = f32(w), height = 200})
rl.DrawText("Screen Summary", x, y, 22, SUMMARY_TITLE)
chip_base := x + w - 258
if chip_base < x+210 {
chip_base = x + 210
}
draw_stat_chip(chip_base, y-4, "Pages", len(controller.state.script.pages))
draw_stat_chip(chip_base+86, y-4, "Panels", len(controller.state.panel_images))
draw_stat_chip(chip_base+172, y-4, "Layout", len(controller.state.page_layouts))
switch controller.active_screen {
case .Story:
draw_summary_line(x, y+30, fmt.tprintf("Idea length: %d chars", len(controller.state.story_idea)), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Genre: %s", controller.state.story_genre), rl.DARKGRAY)
draw_summary_line(x, y+78, fmt.tprintf("Audience: %s", controller.state.target_audience), rl.DARKGRAY)
rl.DrawText("Use Generate Script Local to begin", x, y+112, 18, SUMMARY_HINT)
case .Script:
draw_summary_line(x, y+30, fmt.tprintf("Title: %s", controller.state.script.title), rl.DARKGRAY)
page_count := len(controller.state.script.pages)
draw_summary_line(x, y+54, fmt.tprintf("Pages: %d | Characters: %d", page_count, len(controller.state.script.characters)), rl.DARKGRAY)
if page_count == 0 {
rl.DrawText("No script pages yet. Generate Script Local to continue.", x, y+86, 18, SUMMARY_HINT)
} else {
cursor := opts.script_page_cursor
if cursor < 0 {
cursor = 0
}
if cursor >= page_count {
cursor = page_count - 1
}
page := controller.state.script.pages[cursor]
draw_summary_line(x, y+78, fmt.tprintf("Viewing page %d/%d (script page #%d)", cursor+1, page_count, page.page_number), SUMMARY_ACCENT)
draw_summary_line(x, y+100, fmt.tprintf("Panels on page: %d", len(page.panels)), rl.DARKGRAY)
line_y := y + 124
show_panels := len(page.panels)
if show_panels > 2 {
show_panels = 2
}
for i in 0..<show_panels {
pn := page.panels[i]
desc := pn.description
if len(desc) == 0 {
desc = "(no description)"
}
draw_summary_subline(x, line_y+i32(i*28), fit_text_for_width(fmt.tprintf("• P%d: %s", pn.panel_number, desc), int(w-36), 7), SUMMARY_SUBLINE)
if len(pn.dialogue) > 0 {
draw_summary_subline(x+12, line_y+i32(i*28)+14, fit_text_for_width(fmt.tprintf("\"%s\"", pn.dialogue[0].text), int(w-54), 7), SUMMARY_DIM)
}
}
if len(page.panels) > show_panels {
draw_summary_subline(x+w-158, y+166, fmt.tprintf("+%d more panels", len(page.panels)-show_panels), SUMMARY_ACCENT)
}
}
case .Characters:
draw_summary_line(x, y+30, fmt.tprintf("Character count: %d", len(controller.state.characters)), rl.DARKGRAY)
rl.DrawText("Character editor is scaffolded", x, y+54, 18, rl.DARKGRAY)
rl.DrawText("Use script generation to populate", x, y+78, 18, rl.DARKGRAY)
case .Panels:
script_panel_count := count_script_panels(controller.state.script)
draw_summary_line(x, y+30, fmt.tprintf("Panel images: %d", len(controller.state.panel_images)), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Script panels: %d", script_panel_count), rl.DARKGRAY)
draw_summary_line(x, y+78, fmt.tprintf("Script pages: %d", len(controller.state.script.pages)), rl.DARKGRAY)
if script_panel_count == 0 {
rl.DrawText("No script panels yet. Generate Script first.", x, y+112, 18, SUMMARY_HINT)
} else {
pidx := clamp_panel_cursor(script_panel_count, opts.panel_cursor)
panel, page_num, _ := panel_by_flat_index(controller.state.script, pidx)
status := "missing"
if _, has_img := controller.state.panel_images[panel.panel_id]; has_img {
status = "ready"
}
draw_summary_line(x, y+102, fmt.tprintf("Viewing panel %d/%d • page %d # %d", pidx+1, script_panel_count, page_num, panel.panel_number), SUMMARY_ACCENT)
draw_summary_subline(x, y+124, fmt.tprintf("%s • %s", panel.panel_id, status), SUMMARY_SUBLINE)
}
case .Layout:
draw_summary_line(x, y+30, fmt.tprintf("Layout pages: %d", len(controller.state.page_layouts)), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Page size: %v", controller.state.page_size), rl.DARKGRAY)
layout_show := len(controller.state.page_layouts)
if !opts.layout_show_all && layout_show > 3 { layout_show = 3 }
if layout_show == 0 {
rl.DrawText("No layouts yet. Use Layout Auto after panels are ready.", x, y+86, 18, SUMMARY_HINT)
} else {
if opts.layout_desc {
for i in 0..<layout_show {
idx := len(controller.state.page_layouts)-1-i
l := controller.state.page_layouts[idx]
draw_summary_line(x, y+78+i32(i*22), fmt.tprintf("- P%d: %s (%d)", l.page_number, l.pattern_id, len(l.panels)), rl.DARKGRAY)
}
} else {
for i in 0..<layout_show {
l := controller.state.page_layouts[i]
draw_summary_line(x, y+78+i32(i*22), fmt.tprintf("- P%d: %s (%d)", l.page_number, l.pattern_id, len(l.panels)), rl.DARKGRAY)
}
}
}
case .Bubbles:
bubble_count := 0
if controller.state.speech_bubbles != nil {
for _, bubbles in controller.state.speech_bubbles {
bubble_count += len(bubbles)
}
}
draw_summary_line(x, y+30, fmt.tprintf("Total bubbles: %d", bubble_count), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Layout pages: %d", len(controller.state.page_layouts)), rl.DARKGRAY)
if len(controller.state.page_layouts) == 0 {
rl.DrawText("No layouts yet. Run Layout Auto first.", x, y+86, 18, SUMMARY_HINT)
} else if bubble_count == 0 {
rl.DrawText("No bubbles yet. Use Auto Place or Add on Bubbles screen.", x, y+86, 18, SUMMARY_HINT)
} else {
draw_summary_line(x, y+78, fmt.tprintf("Panels with bubbles: %d", len(controller.state.speech_bubbles)), SUMMARY_ACCENT)
rl.DrawText("Select a panel in Bubbles screen to edit.", x, y+102, 18, SUMMARY_HINT)
}
case .Export:
draw_summary_line(x, y+30, fmt.tprintf("Format: %v", controller.state.export_format), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Layouts: %d | Panels: %d", len(controller.state.page_layouts), len(controller.state.panel_images)), rl.DARKGRAY)
draw_summary_line(x, y+78, fmt.tprintf("Target: %s", export_path), rl.DARKGRAY)
if len(controller.state.page_layouts) > 0 {
last := controller.state.page_layouts[len(controller.state.page_layouts)-1]
draw_summary_subline(x, y+102, fmt.tprintf("Last layout pattern: %s", last.pattern_id), SUMMARY_DIM)
}
reason := export_block_reason(controller.state)
if len(reason) > 0 {
draw_summary_line(x, y+124, fmt.tprintf("Export blocked: %s", reason), ERROR)
} else {
rl.DrawText("Use Export button or Ctrl+E", x, y+124, 18, SUMMARY_HINT)
}
case .Community:
rl.DrawText("Community features coming soon", x, y+30, 18, rl.DARKGRAY)
rl.DrawText("Current focus: local GUI workflows", x, y+54, 18, rl.DARKGRAY)
}
}
clamp_script_cursor :: proc(page_count, cursor: int) -> int {
if page_count <= 0 {
return 0
@ -254,208 +71,6 @@ panel_by_flat_index :: proc(script: core.Comic_Script, panel_idx: int) -> (core.
return core.Panel{}, 0, false
}
build_script_page_detail_text :: proc(state: core.Comic_State, cursor: int) -> string {
page_count := len(state.script.pages)
if page_count == 0 {
return fmt.aprintf("No script pages available.")
}
idx := clamp_script_cursor(page_count, cursor)
page := state.script.pages[idx]
out := fmt.aprintf("Title: %s\nPage %d/%d (script page #%d)\nPanels: %d", state.script.title, idx+1, page_count, page.page_number, len(page.panels))
for pn in page.panels {
desc := pn.description
if len(desc) == 0 {
desc = "(no description)"
}
next := fmt.aprintf("%s\n\nPanel %d [%v]\n%s", out, pn.panel_number, pn.shot_type, desc)
delete(out)
out = next
for d in pn.dialogue {
line := fmt.aprintf("%s\n- %s: %s", out, d.speaker_id, d.text)
delete(out)
out = line
}
if len(pn.caption) > 0 {
line := fmt.aprintf("%s\n caption: %s", out, pn.caption)
delete(out)
out = line
}
}
return out
}
build_full_script_text :: proc(state: core.Comic_State) -> string {
page_count := len(state.script.pages)
if page_count == 0 {
return fmt.aprintf("No script pages available.")
}
out := fmt.aprintf("Title: %s\nSynopsis: %s\nCharacters: %d\nPages: %d", state.script.title, state.script.synopsis, len(state.script.characters), page_count)
for page in state.script.pages {
head := fmt.aprintf("%s\n\n=== Page %d (%d panels) ===", out, page.page_number, len(page.panels))
delete(out)
out = head
for pn in page.panels {
desc := pn.description
if len(desc) == 0 {
desc = "(no description)"
}
row := fmt.aprintf("%s\nPanel %d: %s", out, pn.panel_number, desc)
delete(out)
out = row
}
}
return out
}
draw_script_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) {
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Script Detail")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
page_count := len(controller.state.script.pages)
if page_count == 0 {
draw_summary_line(x+18, y+46, "No script pages yet. Run Generate Script Local.", SUMMARY_HINT)
return
}
idx := clamp_script_cursor(page_count, cursor)
page := controller.state.script.pages[idx]
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d (#%d) • panels %d", idx+1, page_count, page.page_number, len(page.panels)), SUMMARY_ACCENT)
line_y := y + 70
line_step: i32 = 20
line_max: i32 = (h - 84) / line_step
lines_used: i32 = 0
for pn in page.panels {
if lines_used >= line_max {
break
}
desc := pn.description
if len(desc) == 0 {
desc = "(no description)"
}
draw_summary_subline(x+18, line_y+lines_used*line_step, fit_text_for_width(fmt.tprintf("• P%d: %s", pn.panel_number, desc), int(w-40), 7), SUMMARY_SUBLINE)
lines_used += 1
if len(pn.dialogue) > 0 && lines_used < line_max {
draw_summary_subline(x+30, line_y+lines_used*line_step, fit_text_for_width(fmt.tprintf("\"%s\"", pn.dialogue[0].text), int(w-52), 7), SUMMARY_DIM)
lines_used += 1
}
}
if len(page.panels) > 0 && lines_used >= line_max {
draw_summary_subline(x+w-140, y+h-18, "…more", SUMMARY_ACCENT)
}
}
draw_panels_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (retry_clicked: bool, new_cursor: int) {
new_cursor = cursor
retry_clicked = false
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Panel Results")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
panel_count := count_script_panels(controller.state.script)
if panel_count == 0 {
draw_summary_line(x+18, y+46, "No script panels yet. Generate Script first.", SUMMARY_HINT)
return
}
idx := clamp_panel_cursor(panel_count, cursor)
panel, page_num, ok := panel_by_flat_index(controller.state.script, idx)
if !ok {
draw_summary_line(x+18, y+46, "Panel index out of range.", ERROR)
return
}
img, has_img := controller.state.panel_images[panel.panel_id]
err_msg, has_err := controller.state.panel_errors[panel.panel_id]
status := "missing"
status_color := WARNING
if has_err {
status = "error"
status_color = ERROR
}
if has_img {
status = "ready"
status_color = SUCCESS
}
draw_summary_line(x+18, y+46, fmt.tprintf("Panel %d/%d • page %d # %d • %s", idx+1, panel_count, page_num, panel.panel_number, status), status_color)
btn_label := "Regenerate"
if status != "ready" {
btn_label = "Generate"
}
btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24}
draw_small_button_state(btn_rec, btn_label, true)
if button_clicked(btn_rec) {
retry_clicked = true
}
draw_summary_subline(x+18, y+66, fit_text_for_width(fmt.tprintf("id: %s", panel.panel_id), int(w-120), 7), SUMMARY_SUBLINE)
if has_err {
draw_summary_subline(x+18, y+84, fit_text_for_width(fmt.tprintf("err: %s", err_msg), int(w-36), 7), ERROR)
} else if has_img {
draw_summary_subline(x+18, y+84, fit_text_for_width(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed), int(w-36), 7), SUMMARY_DIM)
} else {
draw_summary_subline(x+18, y+84, "img: not generated", SUMMARY_DIM)
}
desc := panel.description
if len(desc) == 0 {
desc = "(no description)"
}
draw_summary_subline(x+18, y+104, fit_text_for_width(fmt.tprintf("desc: %s", desc), int(w-36), 7), SUMMARY_SUBLINE)
if has_img {
draw_summary_subline(x+18, y+124, fit_text_for_width(fmt.tprintf("src: %s", img.url), int(w-36), 7), SUMMARY_DIM)
}
list_y := y + 146
row_h: i32 = 18
rows: i32 = (h - 154) / row_h
if rows < 1 {
rows = 1
}
start := idx - int(rows/2)
if start < 0 {
start = 0
}
end := start + int(rows)
if end > panel_count {
end = panel_count
start = end - int(rows)
if start < 0 {
start = 0
}
}
line: i32 = 0
for i in start..<end {
row_panel, row_page, row_ok := panel_by_flat_index(controller.state.script, i)
if !row_ok {
continue
}
mark := " "
row_color := TEXT_TERTIARY
if i == idx {
mark = ">"
row_color = TEXT_PRIMARY
}
ready := "missing"
if _, err_exists := controller.state.panel_errors[row_panel.panel_id]; err_exists {
ready = "error"
}
if _, exists := controller.state.panel_images[row_panel.panel_id]; exists {
ready = "ready"
}
row_rec := rl.Rectangle{x = f32(x+18), y = f32(list_y+line*row_h), width = f32(w-36), height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
new_cursor = i
}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
row_color = TEXT_SECONDARY
}
draw_summary_subline(x+18, list_y+line*row_h+2, fit_text_for_width(fmt.tprintf("%s %02d p%d#%d %s", mark, i+1, row_page, row_panel.panel_number, ready), int(w-36), 7), row_color)
line += 1
}
return
}
clamp_layout_cursor :: proc(layout_count, cursor: int) -> int {
if layout_count <= 0 {
return 0
@ -470,17 +85,16 @@ clamp_layout_cursor :: proc(layout_count, cursor: int) -> int {
}
Layout_Validation_Result :: struct {
coverage_pct: f32,
missing_bindings: int,
coverage_pct: f32,
missing_bindings: int,
bounds_violations: int,
total_panels: int,
total_panels: int,
}
validate_layout_page :: proc(layout: core.Page_Layout, panel_images: map[string]core.Panel_Image) -> Layout_Validation_Result {
result: Layout_Validation_Result
result.total_panels = len(layout.panels)
// Coverage: sum of panel cell areas vs page area
page_area := f32(layout.width) * f32(layout.height)
covered_area: f32 = 0
for p in layout.panels {
@ -492,14 +106,12 @@ validate_layout_page :: proc(layout: core.Page_Layout, panel_images: map[string]
result.coverage_pct = covered_area / page_area * 100
}
// Missing bindings: panels without corresponding images
for p in layout.panels {
if _, has := panel_images[p.panel_id]; !has {
result.missing_bindings += 1
}
}
// Bounds violations: cells that extend outside [0,1] range
for p in layout.panels {
c := p.layout_cell
if c.x < 0 || c.y < 0 || c.x+c.w > 1.001 || c.y+c.h > 1.001 {
@ -508,149 +120,4 @@ validate_layout_page :: proc(layout: core.Page_Layout, panel_images: map[string]
}
return result
}
draw_validation_badge :: proc(x, y, w: i32, label: string, ok: bool) {
bg := UNREADY_BG
border := UNREADY_BORDER
fg := WARNING
if ok {
bg = READY_BG
border = READY_BORDER
fg = SUCCESS
}
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 22}
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 6, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 6, 1.0, border)
draw_text_fitted(label, x+8, y+5, 11, int(w-16), 6, fg)
}
draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (regen_clicked: bool, new_cursor: int) {
new_cursor = cursor
regen_clicked = false
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Layout Detail")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
layout_count := len(controller.state.page_layouts)
if layout_count == 0 {
draw_summary_line(x+18, y+46, "No layouts yet. Use Layout Auto after panels.", SUMMARY_HINT)
return
}
idx := clamp_layout_cursor(layout_count, cursor)
layout := controller.state.page_layouts[idx]
// Header line with status
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • pattern: %s • %d panels", idx+1, layout_count, layout.pattern_id, len(layout.panels)), SUMMARY_ACCENT)
// Validation badges
val := validate_layout_page(layout, controller.state.panel_images)
coverage_ok := val.coverage_pct >= 80 && val.coverage_pct <= 105
bindings_ok := val.missing_bindings == 0
bounds_ok := val.bounds_violations == 0
draw_validation_badge(x+18, y+68, 100, fmt.tprintf("Cov: %.0f%%", val.coverage_pct), coverage_ok)
draw_validation_badge(x+124, y+68, 110, fmt.tprintf("Bind: %d miss", val.missing_bindings), bindings_ok)
draw_validation_badge(x+240, y+68, 100, fmt.tprintf("Bounds: %d", val.bounds_violations), bounds_ok)
// Regenerate button
btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24}
draw_small_button_state(btn_rec, "Regen", true)
if button_clicked(btn_rec) {
regen_clicked = true
}
// Layout dimensions
draw_summary_subline(x+18, y+94, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
// Mini wireframe preview
preview_x := x + 18
preview_y := y + 114
preview_max_w: f32 = f32(w) * 0.4
preview_max_h: f32 = f32(h - 100)
if preview_max_h < 40 {
preview_max_h = 40
}
// Scale to fit
lw := f32(layout.width)
lh := f32(layout.height)
if lw < 1 { lw = 1 }
if lh < 1 { lh = 1 }
scale_x := preview_max_w / lw
scale_y := preview_max_h / lh
scale := scale_x
if scale_y < scale {
scale = scale_y
}
pw := lw * scale
ph := lh * scale
// Draw page outline
page_rec := rl.Rectangle{x = f32(preview_x), y = f32(preview_y), width = pw, height = ph}
rl.DrawRectangleRounded(page_rec, 0.02, 4, BG_STRIP)
rl.DrawRectangleRoundedLinesEx(page_rec, 0.02, 4, 1.0, BORDER_CARD)
// Draw each panel cell
for i in 0..<len(layout.panels) {
cell := layout.panels[i].layout_cell
cx := f32(preview_x) + cell.x * pw
cy := f32(preview_y) + cell.y * ph
cw := cell.w * pw
ch := cell.h * ph
// Inset slightly for gutter
inset: f32 = 1.5
cell_rec := rl.Rectangle{x = cx + inset, y = cy + inset, width = cw - inset*2, height = ch - inset*2}
rl.DrawRectangleRounded(cell_rec, 0.06, 4, ACCENT_SURFACE)
rl.DrawRectangleRoundedLinesEx(cell_rec, 0.06, 4, 1.0, ACCENT_MUTED)
// Panel number label
num_label := fmt.tprintf("%d", layout.panels[i].panel_number)
label_sz: i32 = 10
if cw > 30 && ch > 14 {
draw_text_fitted(num_label, i32(cx + inset + 3), i32(cy + inset + 2), label_sz, int(cw - inset*2 - 4), 5, TEXT_SECONDARY)
}
}
// Page list (right side)
list_x := x + i32(preview_max_w) + 36
list_y := y + 114
list_w := w - i32(preview_max_w) - 54
row_h: i32 = 18
rows: i32 = (h - 126) / row_h
if rows < 1 {
rows = 1
}
start := idx - int(rows/2)
if start < 0 {
start = 0
}
end := start + int(rows)
if end > layout_count {
end = layout_count
start = end - int(rows)
if start < 0 {
start = 0
}
}
line: i32 = 0
for i in start..<end {
l := controller.state.page_layouts[i]
mark := " "
row_color := TEXT_TERTIARY
if i == idx {
mark = ">"
row_color = TEXT_PRIMARY
}
row_rec := rl.Rectangle{x = f32(list_x), y = f32(list_y + line*row_h), width = f32(list_w), height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
new_cursor = i
}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
row_color = TEXT_SECONDARY
}
draw_summary_subline(list_x, list_y+line*row_h+2, fit_text_for_width(fmt.tprintf("%s %02d %s (%d)", mark, l.page_number, l.pattern_id, len(l.panels)), int(list_w), 7), row_color)
line += 1
}
return
}
}

View File

@ -1,7 +1,6 @@
package gui
import "core:fmt"
import rl "vendor:raylib"
fit_text_for_width :: proc(text: string, width_px, px_per_char: int) -> string {
if px_per_char <= 0 {
@ -15,9 +14,4 @@ fit_text_for_width :: proc(text: string, width_px, px_per_char: int) -> string {
return text
}
return fmt.tprintf("%s…", text[:max_chars-1])
}
draw_text_fitted :: proc(text: string, x, y, font_size: i32, width_px, px_per_char: int, color: rl.Color) {
display := fit_text_for_width(text, width_px, px_per_char)
rl.DrawText(fmt.ctprintf("%s", display), x, y, font_size, color)
}
}

View File

@ -2,209 +2,4 @@ package gui
import rl "vendor:raylib"
// Backgrounds
BG_BASE :: rl.Color{13, 13, 18, 255}
BG_SIDEBAR :: rl.Color{18, 18, 24, 255}
BG_TOPBAR :: rl.Color{18, 18, 24, 255}
BG_CARD :: rl.Color{24, 24, 32, 255}
BG_CARD_ALT :: rl.Color{28, 28, 38, 255} // slightly elevated card
BG_STRIP :: rl.Color{22, 22, 30, 255} // subtle strip background
BG_OVERLAY :: rl.Color{8, 8, 12, 180} // modal backdrop
// Borders
BORDER_CARD :: rl.Color{40, 40, 52, 255}
BORDER_SUBTLE :: rl.Color{36, 36, 48, 255}
BORDER_DIVIDER :: rl.Color{36, 36, 48, 255}
// Accent (Indigo-Violet)
ACCENT :: rl.Color{99, 102, 241, 255}
ACCENT_HOVER :: rl.Color{120, 122, 248, 255}
ACCENT_MUTED :: rl.Color{68, 70, 180, 255}
ACCENT_SURFACE :: rl.Color{30, 30, 56, 255}
ACCENT_GLOW :: rl.Color{99, 102, 241, 80}
// Text
TEXT_PRIMARY :: rl.Color{228, 228, 240, 255}
TEXT_SECONDARY :: rl.Color{148, 148, 168, 255}
TEXT_TERTIARY :: rl.Color{98, 98, 118, 255}
TEXT_DISABLED :: rl.Color{68, 68, 88, 255}
TEXT_BRIGHT :: rl.Color{245, 245, 255, 255}
// Semantic: Success
SUCCESS :: rl.Color{52, 211, 153, 255}
SUCCESS_BG :: rl.Color{16, 42, 32, 255}
SUCCESS_BORDER :: rl.Color{40, 100, 74, 255}
SUCCESS_TEXT :: rl.Color{110, 231, 183, 255}
// Semantic: Warning
WARNING :: rl.Color{251, 191, 36, 255}
WARNING_BG :: rl.Color{50, 38, 14, 255}
WARNING_BORDER :: rl.Color{120, 90, 30, 255}
WARNING_TEXT :: rl.Color{253, 224, 120, 255}
// Semantic: Error
ERROR :: rl.Color{248, 113, 113, 255}
ERROR_BG :: rl.Color{50, 18, 18, 255}
ERROR_BORDER :: rl.Color{120, 50, 50, 255}
ERROR_TEXT :: rl.Color{254, 178, 178, 255}
// Semantic: Danger (destructive buttons)
DANGER_BG :: rl.Color{153, 50, 58, 255}
DANGER_BG_HOVER :: rl.Color{172, 60, 68, 255}
DANGER_BORDER :: rl.Color{130, 42, 48, 255}
// Semantic: Warning-style buttons
WARN_BTN_BG :: rl.Color{80, 64, 30, 255}
WARN_BTN_BG_HOVER :: rl.Color{95, 76, 36, 255}
WARN_BTN_BORDER :: rl.Color{110, 88, 40, 255}
WARN_BTN_TEXT :: rl.Color{253, 224, 120, 255}
// Buttons
BTN_BG :: rl.Color{32, 32, 44, 255}
BTN_BG_HOVER :: rl.Color{40, 40, 54, 255}
BTN_BORDER :: rl.Color{52, 52, 68, 255}
BTN_BORDER_HOVER :: rl.Color{80, 82, 140, 255}
BTN_TEXT :: TEXT_PRIMARY
BTN_SOFT_BG :: rl.Color{28, 30, 48, 255}
BTN_SOFT_BG_HOVER :: rl.Color{36, 38, 58, 255}
BTN_SOFT_BORDER :: rl.Color{60, 62, 100, 255}
BTN_SOFT_TEXT :: rl.Color{160, 162, 220, 255}
BTN_DISABLED_BG :: rl.Color{24, 24, 32, 255}
BTN_DISABLED_BORDER :: rl.Color{36, 36, 48, 255}
BTN_DISABLED_TEXT :: rl.Color{68, 68, 88, 255}
// Small Buttons
SBTN_BG :: rl.Color{30, 30, 42, 255}
SBTN_BG_HOVER :: rl.Color{40, 40, 54, 255}
SBTN_BORDER :: rl.Color{50, 50, 66, 255}
SBTN_BORDER_HOVER :: rl.Color{80, 82, 140, 255}
SBTN_TEXT :: rl.Color{188, 188, 210, 255}
// Navigation
NAV_BG :: rl.Color{24, 24, 32, 255}
NAV_BG_HOVER :: rl.Color{32, 32, 44, 255}
NAV_BORDER :: rl.Color{40, 40, 52, 255}
NAV_BORDER_HOVER :: rl.Color{70, 72, 120, 255}
NAV_TEXT :: rl.Color{168, 168, 188, 255}
NAV_ACTIVE_BG :: ACCENT
NAV_ACTIVE_TEXT :: TEXT_BRIGHT
NAV_ACTIVE_BAR :: rl.Color{180, 182, 255, 255}
// Input Fields
INPUT_BG :: rl.Color{16, 16, 24, 255}
INPUT_BORDER :: rl.Color{40, 40, 54, 255}
INPUT_FOCUS_BG :: rl.Color{20, 20, 30, 255}
INPUT_FOCUS_BORDER :: ACCENT
INPUT_FOCUS_RING :: ACCENT_GLOW
INPUT_TEXT :: TEXT_PRIMARY
INPUT_TEXT_FOCUS :: TEXT_BRIGHT
// Chips & Pills
PILL_BG :: rl.Color{28, 28, 38, 255}
PILL_BORDER :: rl.Color{44, 44, 58, 255}
PILL_TEXT :: TEXT_SECONDARY
PILL_ACCENT_BG :: ACCENT_SURFACE
PILL_ACCENT_BORDER :: ACCENT_MUTED
PILL_ACCENT_TEXT :: rl.Color{180, 182, 255, 255}
CHIP_BG :: rl.Color{28, 28, 38, 255}
CHIP_BORDER :: rl.Color{44, 44, 58, 255}
CHIP_TEXT :: TEXT_SECONDARY
CHIP_ACCENT_BG :: ACCENT_SURFACE
CHIP_ACCENT_BORDER :: ACCENT_MUTED
CHIP_ACCENT_TEXT :: PILL_ACCENT_TEXT
// Status Badges
BADGE_OK_BG :: SUCCESS_BG
BADGE_OK_BORDER :: SUCCESS_BORDER
BADGE_OK_TEXT :: SUCCESS_TEXT
BADGE_BAD_BG :: ERROR_BG
BADGE_BAD_BORDER :: ERROR_BORDER
BADGE_BAD_TEXT :: ERROR_TEXT
// Readiness Chips
READY_BG :: SUCCESS_BG
READY_BORDER :: SUCCESS_BORDER
READY_TEXT :: SUCCESS_TEXT
UNREADY_BG :: rl.Color{28, 28, 38, 255}
UNREADY_BORDER :: rl.Color{44, 44, 58, 255}
UNREADY_TEXT :: TEXT_TERTIARY
// Progress Bar
PROGRESS_TRACK :: rl.Color{28, 28, 40, 255}
PROGRESS_FILL :: ACCENT
// Toast
TOAST_SUCCESS :: rl.Color{28, 120, 80, 235}
TOAST_WARNING :: rl.Color{140, 100, 30, 235}
TOAST_ERROR :: rl.Color{150, 50, 50, 235}
TOAST_BORDER :: rl.Color{255, 255, 255, 40}
TOAST_SHADOW :: rl.Color{0, 0, 0, 60}
// Action Log
LOG_ROW_ALT :: rl.Color{22, 22, 30, 255}
LOG_TEXT :: rl.Color{158, 158, 178, 255}
// Section Titles
SECTION_TITLE_COLOR :: rl.Color{148, 150, 210, 255}
SECTION_UNDERLINE :: rl.Color{44, 44, 60, 255}
// Screen Summary
SUMMARY_TITLE :: rl.Color{170, 172, 230, 255}
SUMMARY_ACCENT :: rl.Color{99, 140, 220, 255}
SUMMARY_HINT :: rl.Color{120, 90, 200, 255}
SUMMARY_SUBLINE :: rl.Color{128, 128, 148, 255}
SUMMARY_DIM :: rl.Color{98, 98, 118, 255}
// Pipeline Stepper
STEP_DONE_FILL :: SUCCESS
STEP_DONE_BORDER :: SUCCESS_BORDER
STEP_TODO_FILL :: rl.Color{36, 36, 48, 255}
STEP_TODO_BORDER :: rl.Color{52, 52, 68, 255}
STEP_LINE_DONE :: rl.Color{40, 100, 74, 255}
STEP_LINE_TODO :: rl.Color{40, 40, 52, 255}
STEP_LABEL_DONE :: SUCCESS_TEXT
STEP_LABEL_TODO :: TEXT_TERTIARY
// Help Overlay
HELP_TITLE :: TEXT_BRIGHT
HELP_SECTION :: rl.Color{130, 132, 210, 255}
HELP_LINE :: TEXT_SECONDARY
HELP_CLOSE :: rl.Color{170, 148, 240, 255}
// Confirm Overlay
CONFIRM_ACCENT :: ACCENT
CONFIRM_TITLE :: TEXT_BRIGHT
CONFIRM_BODY :: TEXT_SECONDARY
CONFIRM_HINT :: rl.Color{170, 148, 240, 255}
// Sidebar Shortcuts
SIDEBAR_TITLE :: rl.Color{130, 132, 200, 255}
SIDEBAR_TEXT :: TEXT_TERTIARY
SIDEBAR_FOOTER :: rl.Color{80, 80, 100, 255}
// Brand
BRAND_TITLE :: TEXT_BRIGHT
BRAND_SUBTITLE :: TEXT_TERTIARY
// Roundness Constants
RADIUS_CARD :: f32(0.14)
RADIUS_BUTTON :: f32(0.32)
RADIUS_PILL :: f32(0.50)
RADIUS_INPUT :: f32(0.24)
RADIUS_NAV :: f32(0.28)
RADIUS_CHIP :: f32(0.42)
RADIUS_BADGE :: f32(0.42)
RADIUS_TOAST :: f32(0.40)
RADIUS_BAR :: f32(0.60)
// Recommended Halo
RECOMMEND_HALO_FILL :: rl.Color{50, 50, 100, 255}
RECOMMEND_HALO_BORDER :: rl.Color{120, 122, 248, 255}
// DeepSeek key missing
KEY_MISSING_COLOR :: ERROR
BG_BASE :: rl.Color{13, 13, 18, 255}

View File

@ -1,76 +0,0 @@
package gui
import rl "vendor:raylib"
draw_card :: proc(rec: rl.Rectangle) {
rl.DrawRectangleRounded(rec, RADIUS_CARD, 8, BG_CARD)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CARD, 8, 1.0, BORDER_CARD)
}
draw_subtle_strip :: proc(rec: rl.Rectangle) {
rl.DrawRectangleRounded(rec, 0.20, 8, BG_STRIP)
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 8, 1.0, BORDER_SUBTLE)
}
draw_hint_pill :: proc(rec: rl.Rectangle, label: string, accent: bool) {
bg := PILL_BG
border := PILL_BORDER
fg := PILL_TEXT
if accent {
bg = PILL_ACCENT_BG
border = PILL_ACCENT_BORDER
fg = PILL_ACCENT_TEXT
}
rl.DrawRectangleRounded(rec, RADIUS_PILL, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_PILL, 8, 1.0, border)
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 13, int(rec.width)-16, 7, fg)
}
draw_topbar_chip :: proc(rec: rl.Rectangle, label: string, accent: bool) {
bg := CHIP_BG
border := CHIP_BORDER
fg := CHIP_TEXT
if accent {
bg = CHIP_ACCENT_BG
border = CHIP_ACCENT_BORDER
fg = CHIP_ACCENT_TEXT
}
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, border)
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-16, 8, fg)
}
draw_status_badge :: proc(rec: rl.Rectangle, label: string, ok: bool) {
bg := BADGE_BAD_BG
border := BADGE_BAD_BORDER
fg := BADGE_BAD_TEXT
if ok {
bg = BADGE_OK_BG
border = BADGE_OK_BORDER
fg = BADGE_OK_TEXT
}
rl.DrawRectangleRounded(rec, RADIUS_BADGE, 8, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BADGE, 8, 1.0, border)
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 13, int(rec.width)-16, 7, fg)
}
draw_section_title :: proc(x, y: i32, label: string) {
draw_text_fitted(label, x, y, 17, 180, 8, SECTION_TITLE_COLOR)
rl.DrawLine(x, y+20, x+180, y+20, SECTION_UNDERLINE)
}
draw_summary_line :: proc(x, y: i32, text: string, c: rl.Color) {
fg := c
if int(c.r)+int(c.g)+int(c.b) < 260 {
fg = TEXT_PRIMARY
}
draw_text_fitted(text, x, y, 18, 438, 8, fg)
}
draw_summary_subline :: proc(x, y: i32, text: string, c: rl.Color) {
fg := c
if int(c.r)+int(c.g)+int(c.b) < 260 {
fg = TEXT_SECONDARY
}
draw_text_fitted(text, x, y, 16, 438, 7, fg)
}

1
odin/vendor/clay vendored Submodule

@ -0,0 +1 @@
Subproject commit e6cc36941ab2af5d81107617039d6f527a1c660b