another checkpoint
This commit is contained in:
parent
b0f9acdb47
commit
2160449f43
6
.github/workflows/odin-ci.yml
vendored
6
.github/workflows/odin-ci.yml
vendored
@ -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
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "odin/vendor/clay"]
|
||||
path = odin/vendor/clay
|
||||
url = https://github.com/nicbarker/clay.git
|
||||
@ -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"
|
||||
@ -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
12
odin/ols.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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}"
|
||||
|
||||
@ -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[:]
|
||||
}
|
||||
409
odin/src/gui/clay_layout.odin
Normal file
409
odin/src/gui/clay_layout.odin
Normal 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})
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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")
|
||||
@ -41,111 +9,3 @@ 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
@ -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:
|
||||
|
||||
@ -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 {
|
||||
@ -509,148 +121,3 @@ 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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -16,8 +15,3 @@ fit_text_for_width :: proc(text: string, width_px, px_per_char: int) -> string {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
@ -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
1
odin/vendor/clay
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e6cc36941ab2af5d81107617039d6f527a1c660b
|
||||
Loading…
Reference in New Issue
Block a user