Phase A: Split runtime.odin into chrome.odin, workspaces.odin, detail_panels.odin
runtime.odin: 1782 → 818 lines (orchestration only) chrome.odin: 248 lines (sidebar, pipeline bar, workspace router, bottom bar) workspaces.odin: 328 lines (8 workspace functions) detail_panels.odin: 439 lines (script/panels/layout/bubbles detail + action log) All 156 tests pass, build clean.
This commit is contained in:
parent
5729a98888
commit
abc74582d6
248
odin/src/gui/chrome.odin
Normal file
248
odin/src/gui/chrome.odin
Normal file
@ -0,0 +1,248 @@
|
||||
package gui
|
||||
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "../core"
|
||||
import "../shared"
|
||||
import "../ui"
|
||||
|
||||
// ─── Sidebar Declaration ─────────────────────────────────────────
|
||||
declare_sidebar :: proc(app: ^GUI_App_State) {
|
||||
screens := []ui.App_Screen{.Story, .Script, .Characters, .Panels, .Layout, .Bubbles, .Export, .Community}
|
||||
icons := []string{"1", "2", "3", "4", "5", "6", "7", "8"}
|
||||
names := []string{"Story", "Script", "Chars", "Panels", "Layout", "Bubbles", "Export", "Commty"}
|
||||
|
||||
if clay.UI(clay.ID("Sidebar"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(220), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = 12, right = 12, bottom = 12, left = 12}, childGap = 2},
|
||||
backgroundColor = CLAY_BG_SIDEBAR,
|
||||
}) {
|
||||
// Brand
|
||||
clay_body_text("comic-odin", color = CLAY_ACCENT, size = 16)
|
||||
clay_muted_text("Pipeline GUI")
|
||||
|
||||
// Pipeline progress bar
|
||||
ready, total := ready_stage_count(app.controller)
|
||||
progress := f32(0)
|
||||
if total > 0 { progress = f32(ready) / f32(total) }
|
||||
if clay.UI(clay.ID("SidebarProgress"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(4)}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_PROGRESS_TRACK,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {
|
||||
if progress > 0 {
|
||||
pct := f32(progress * 100); if pct > 100 { pct = 100 }
|
||||
if clay.UI(clay.ID("SidebarPgFill"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(pct), height = clay.SizingGrow({})}},
|
||||
backgroundColor = CLAY_PROGRESS_FILL,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
clay_muted_text(fmt.tprintf("%d/4 done", ready))
|
||||
|
||||
// Navigation
|
||||
for i in 0 ..< len(screens) {
|
||||
is_active := app.controller.active_screen == screens[i]
|
||||
bg := CLAY_NAV_HOVER_BG
|
||||
if is_active { bg = CLAY_NAV_ACTIVE_BG }
|
||||
accent_w: u16 = 0
|
||||
accent_c: clay.Color = {0, 0, 0, 0}
|
||||
if is_active { accent_w = 3; accent_c = CLAY_NAV_ACTIVE }
|
||||
|
||||
if clay.UI(clay.ID("Nav", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {y = .Center}, layoutDirection = .LeftToRight, childGap = 6},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = accent_c, width = {accent_w, 0, 0, 0, 0}},
|
||||
}) {
|
||||
text_color: clay.Color = CLAY_TEXT_SECONDARY
|
||||
if is_active { text_color = CLAY_TEXT_BRIGHT }
|
||||
clay_body_text(fmt.tprintf("%s %s", icons[i], names[i]), color = text_color)
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
gap_spacer := clay.UI(clay.ID("SidebarGap"))
|
||||
if gap_spacer({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
||||
}) {}
|
||||
|
||||
// Project name + help
|
||||
if clay.UI(clay.ID("SidebarFooter"))({
|
||||
layout = {layoutDirection = .LeftToRight, childAlignment = {y = .Center}, childGap = 4},
|
||||
}) {
|
||||
proj_name := app.project_path
|
||||
if len(proj_name) > 18 { proj_name = fmt.tprintf("...%s", proj_name[len(proj_name)-15:]) }
|
||||
clay_muted_text(proj_name)
|
||||
declare_button_small("btn_help", "?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pipeline Bar ─────────────────────────────────────────────────
|
||||
|
||||
// ─── Pipeline Bar ─────────────────────────────────────────────────
|
||||
declare_pipeline_bar :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("PipelineBar"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 54, max = 48})}, padding = {top = 8, right = 16, bottom = 8, left = 16}, childGap = 12, childAlignment = {y = .Center}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_BG_TOPBAR,
|
||||
border = {color = CLAY_BORDER_DIVIDER, width = {0, 0, 0, 1, 0}},
|
||||
}) {
|
||||
// Screen name
|
||||
if clay.UI(clay.ID("PipelineTitle"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(110)}},
|
||||
}) {
|
||||
clay_title_text(ui.screen_name(app.controller.active_screen), size = CLAY_FONT_SIZE_LG)
|
||||
}
|
||||
|
||||
// Pipeline stepper
|
||||
script_ok := len(app.controller.state.script.pages) > 0
|
||||
panels_ok := len(app.controller.state.panel_images) > 0
|
||||
layout_ok := len(app.controller.state.page_layouts) > 0
|
||||
export_ok := panels_ok && layout_ok
|
||||
|
||||
steps := [4]struct{name: string, done: bool}{
|
||||
{"Script", script_ok},
|
||||
{"Panels", panels_ok},
|
||||
{"Layout", layout_ok},
|
||||
{"Export", export_ok},
|
||||
}
|
||||
for i in 0 ..< len(steps) {
|
||||
var_name := steps[i].name
|
||||
is_current := (i == 0 && !script_ok) || (i == 1 && script_ok && !panels_ok) || (i == 2 && panels_ok && !layout_ok) || (i == 3 && layout_ok && !export_ok) || (i == 3 && export_ok)
|
||||
circle_color: clay.Color = CLAY_BTN_DISABLED
|
||||
if steps[i].done { circle_color = CLAY_SUCCESS }
|
||||
if is_current && !steps[i].done { circle_color = CLAY_ACCENT }
|
||||
|
||||
if clay.UI(clay.ID("PStep", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(84), height = clay.SizingFixed(36)}, layoutDirection = .TopToBottom, childAlignment = {x = .Center}, childGap = 1},
|
||||
backgroundColor = CLAY_NAV_HOVER_BG if clay.Hovered() else clay.Color{0, 0, 0, 0},
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
mark := "○"
|
||||
mark_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if steps[i].done { mark = "●"; mark_color = CLAY_SUCCESS }
|
||||
if is_current && !steps[i].done { mark_color = CLAY_ACCENT }
|
||||
clay.Text(mark, {fontId = CLAY_FONT_BODY, fontSize = 14, textColor = mark_color})
|
||||
clay.Text(var_name, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = mark_color})
|
||||
}
|
||||
if i < len(steps) - 1 {
|
||||
line_color: clay.Color = clay.Color{255, 255, 255, 20}
|
||||
if steps[i].done { line_color = CLAY_SUCCESS }
|
||||
if clay.UI(clay.ID("PLine", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(12), height = clay.SizingFixed(1)}},
|
||||
backgroundColor = line_color,
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
pspacer := clay.UI(clay.ID("PBarSpacer"))
|
||||
if pspacer({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}}) {}
|
||||
|
||||
// Pipeline progress count
|
||||
ready, total := ready_stage_count(app.controller)
|
||||
if clay.UI(clay.ID("PBarProgress"))({
|
||||
layout = {layoutDirection = .LeftToRight, childAlignment = {y = .Center}, childGap = 4},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("%d/%d", ready, total), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
progress := f32(0)
|
||||
if total > 0 { progress = f32(ready) / f32(total) }
|
||||
if progress > 0 && progress <= 100 {
|
||||
pct := f32(progress * 100); if pct > 100 { pct = 100 }
|
||||
if clay.UI(clay.ID("PBarMinibar"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(60), height = clay.SizingFixed(4)}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_PROGRESS_TRACK,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {
|
||||
if clay.UI(clay.ID("PBarMinibarFill"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(pct), height = clay.SizingGrow({})}},
|
||||
backgroundColor = CLAY_PROGRESS_FILL,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status message (truncated)
|
||||
msg := app.status_msg
|
||||
if len(msg) > 40 { msg = fmt.tprintf("%s...", msg[:37]) }
|
||||
clay_body_text(msg, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Workspace Declaration ────────────────────────────────────────
|
||||
|
||||
// ─── Workspace Declaration ────────────────────────────────────────
|
||||
declare_workspace :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek: bool, pages_count: int) {
|
||||
screen := app.controller.active_screen
|
||||
|
||||
if clay.UI(clay.ID("Workspace"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = 12, right = 16, bottom = 12, left = 16}, childGap = 8},
|
||||
}) {
|
||||
switch screen {
|
||||
case .Story:
|
||||
declare_story_workspace(app)
|
||||
declare_action_log(app)
|
||||
case .Script:
|
||||
declare_script_workspace(app)
|
||||
case .Characters:
|
||||
declare_characters_workspace(app)
|
||||
declare_action_log(app)
|
||||
case .Panels:
|
||||
declare_panels_workspace(app)
|
||||
case .Layout:
|
||||
declare_layout_workspace(app)
|
||||
case .Bubbles:
|
||||
declare_bubbles_workspace(app)
|
||||
case .Export:
|
||||
declare_export_workspace(app)
|
||||
declare_action_log(app)
|
||||
case .Community:
|
||||
declare_community_workspace(app)
|
||||
declare_action_log(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Script Workspace ────────────────────────────────────────────
|
||||
|
||||
// ─── Bottom Bar ───────────────────────────────────────────────────
|
||||
declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, next_hint: string) {
|
||||
if clay.UI(clay.ID("BottomBar"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 50, max = 44})}, padding = {top = 6, right = 12, bottom = 6, left = 12}, childGap = 8, childAlignment = {y = .Center}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_BG_TOPBAR,
|
||||
border = {color = CLAY_BORDER_DIVIDER, width = {0, 0, 1, 0, 0}},
|
||||
}) {
|
||||
// Left: File ops
|
||||
declare_button_danger("btn_new", "New")
|
||||
declare_button_soft("btn_open", "Open")
|
||||
declare_button_soft("btn_save", "Save")
|
||||
|
||||
// Spacer
|
||||
bbspacer1 := clay.UI(clay.ID("BBSpacer1"))
|
||||
if bbspacer1({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}}) {}
|
||||
|
||||
// Center: Primary CTA
|
||||
cta_label := fmt.tprintf("Next: %s", next_hint)
|
||||
declare_button_recommended("btn_next", cta_label)
|
||||
|
||||
// Spacer
|
||||
bbspacer2 := clay.UI(clay.ID("BBSpacer2"))
|
||||
if bbspacer2({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}}) {}
|
||||
|
||||
// Right: quick actions
|
||||
clay_muted_text("Src:")
|
||||
declare_nav_chip("btn_local", "Local", !app.use_deepseek_script)
|
||||
declare_nav_chip("btn_deepseek", "DS", app.use_deepseek_script)
|
||||
declare_button_small("btn_script", "Script")
|
||||
declare_button_small("btn_panels", "Panels")
|
||||
declare_button_small("btn_layout", "Layout")
|
||||
declare_button_small("btn_export", "Export")
|
||||
declare_button_primary("btn_auto", "Auto-All")
|
||||
declare_button_soft("btn_autosave_toggle",
|
||||
fmt.tprintf("⏱ %s", "ON" if app.autosave_enabled else "OFF"))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Click Processing ──────────────────────────────────────────────
|
||||
@ -42,10 +42,10 @@ 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_PRIMARY :: clay.Color{245, 245, 250, 255}
|
||||
CLAY_TEXT_SECONDARY :: clay.Color{190, 190, 210, 255}
|
||||
CLAY_TEXT_TERTIARY :: clay.Color{145, 145, 170, 255}
|
||||
CLAY_TEXT_DISABLED :: clay.Color{100, 100, 120, 255}
|
||||
CLAY_TEXT_BRIGHT := clay.Color{255, 255, 255, 255}
|
||||
|
||||
CLAY_SUCCESS :: clay.Color{34, 197, 94, 255}
|
||||
@ -85,11 +85,11 @@ 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)
|
||||
CLAY_FONT_SIZE_SM :: u16(15)
|
||||
CLAY_FONT_SIZE_MD :: u16(17)
|
||||
CLAY_FONT_SIZE_LG :: u16(21)
|
||||
CLAY_FONT_SIZE_XL :: u16(26)
|
||||
CLAY_FONT_SIZE_2XL:: u16(32)
|
||||
|
||||
// --- Corner Radius (px for Clay) ---
|
||||
CLAY_RADIUS_SM :: f32(4)
|
||||
@ -123,6 +123,15 @@ Clay_State :: struct {
|
||||
|
||||
clay_state: Clay_State
|
||||
|
||||
load_font_or_default :: proc(path: cstring) -> rl.Font {
|
||||
font := rl.LoadFont(path)
|
||||
if font.glyphCount > 0 {
|
||||
rl.SetTextureFilter(font.texture, .BILINEAR)
|
||||
return font
|
||||
}
|
||||
return rl.GetFontDefault()
|
||||
}
|
||||
|
||||
// --- Init / Shutdown ---
|
||||
clay_init :: proc(screen_w: i32, screen_h: i32) {
|
||||
min_memory_size := clay.MinMemorySize()
|
||||
@ -132,9 +141,9 @@ clay_init :: proc(screen_w: i32, screen_h: i32) {
|
||||
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()
|
||||
clay_state.font_default = load_font_or_default("/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf")
|
||||
clay_state.font_title = load_font_or_default("/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf")
|
||||
clay_state.font_mono = load_font_or_default("/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf")
|
||||
|
||||
raylib_fonts = make([dynamic]Raylib_Font, 0, 3)
|
||||
append(&raylib_fonts, Raylib_Font{fontId = CLAY_FONT_BODY, font = clay_state.font_default})
|
||||
@ -143,6 +152,11 @@ clay_init :: proc(screen_w: i32, screen_h: i32) {
|
||||
}
|
||||
|
||||
clay_shutdown :: proc() {
|
||||
for f in raylib_fonts {
|
||||
if f.font.glyphCount > 0 && f.font.glyphs != nil {
|
||||
rl.UnloadFont(f.font)
|
||||
}
|
||||
}
|
||||
delete(raylib_fonts)
|
||||
delete(clay_state.arena_memory)
|
||||
}
|
||||
@ -364,7 +378,7 @@ clay_input_layout :: proc() -> clay.LayoutConfig {
|
||||
layoutDirection = .LeftToRight,
|
||||
sizing = {
|
||||
width = clay.SizingGrow({}),
|
||||
height = clay.SizingFixed(36),
|
||||
height = clay.SizingFixed(40),
|
||||
},
|
||||
padding = {top = 8, right = 12, bottom = 8, left = 12},
|
||||
}
|
||||
@ -375,7 +389,7 @@ clay_button_layout :: proc() -> clay.LayoutConfig {
|
||||
layoutDirection = .LeftToRight,
|
||||
sizing = {
|
||||
width = clay.SizingFit({}),
|
||||
height = clay.SizingFixed(36),
|
||||
height = clay.SizingFixed(38),
|
||||
},
|
||||
padding = {top = 8, right = 16, bottom = 8, left = 16},
|
||||
childAlignment = {x = .Center, y = .Center},
|
||||
|
||||
440
odin/src/gui/detail_panels.odin
Normal file
440
odin/src/gui/detail_panels.odin
Normal file
@ -0,0 +1,440 @@
|
||||
package gui
|
||||
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "../core"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// ─── Script Detail Panel (Clay) ────────────────────────────────────
|
||||
declare_script_detail :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("ScriptDetailCard"))(clay_card_style()) {
|
||||
clay_title_text("Script Detail")
|
||||
|
||||
page_count := len(app.controller.state.script.pages)
|
||||
if page_count == 0 {
|
||||
clay_muted_text("No script pages yet. Run Generate Script Local.")
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_script_cursor(page_count, app.summary_opts.script_page_cursor)
|
||||
page := app.controller.state.script.pages[idx]
|
||||
|
||||
if clay.UI(clay.ID("ScriptDetailStatRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8},
|
||||
}) {
|
||||
declare_stat_chip("Page", idx + 1)
|
||||
declare_stat_chip("Panels", len(page.panels))
|
||||
}
|
||||
|
||||
clay_body_text(fmt.tprintf("Page #%d", page.page_number), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if clay.UI(clay.ID("ScriptPanelScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
for pn in page.panels {
|
||||
desc := pn.description
|
||||
if len(desc) == 0 { desc = "(no description)" }
|
||||
if clay.UI(clay.ID(fmt.tprintf("ScriptDetailPanel_%d", pn.panel_number)))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 2, right = 4, bottom = 2, left = 4}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("P%d: %s", pn.panel_number, desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
if len(pn.dialogue) > 0 {
|
||||
clay_body_text(fmt.tprintf("\"%s\"", pn.dialogue[0].text), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Panels Detail Panel (Clay) ────────────────────────────────────
|
||||
|
||||
// ─── Panels Detail Panel (Clay) ────────────────────────────────────
|
||||
declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("PanelsDetailCard"))(clay_card_style()) {
|
||||
clay_title_text("Panel Results")
|
||||
|
||||
panel_count := count_script_panels(app.controller.state.script)
|
||||
if panel_count == 0 {
|
||||
clay_muted_text("No script panels yet. Generate Script first.")
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
panel, page_num, ok := panel_by_flat_index(app.controller.state.script, idx)
|
||||
if !ok {
|
||||
clay_muted_text("Panel index out of range.")
|
||||
return
|
||||
}
|
||||
|
||||
status := "missing"
|
||||
status_color: clay.Color = CLAY_WARNING
|
||||
_, has_img := app.controller.state.panel_images[panel.panel_id]
|
||||
_, has_err := app.controller.state.panel_errors[panel.panel_id]
|
||||
if has_err {
|
||||
status = "error"
|
||||
status_color = CLAY_ERROR
|
||||
}
|
||||
if has_img {
|
||||
status = "ready"
|
||||
status_color = CLAY_SUCCESS
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PanelDetailHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Panel %d/%d • page %d # %d", idx+1, panel_count, page_num, panel.panel_number), color = status_color, size = CLAY_FONT_SIZE_SM)
|
||||
if has_img {
|
||||
declare_button_small("btn_panel_regenerate", "Regenerate")
|
||||
} else {
|
||||
declare_button_small("btn_panel_regenerate", "Generate")
|
||||
}
|
||||
}
|
||||
|
||||
clay_muted_text(fmt.tprintf("id: %s", panel.panel_id))
|
||||
if has_err {
|
||||
err_msg, _ := app.controller.state.panel_errors[panel.panel_id]
|
||||
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||
} else if has_img {
|
||||
img := app.controller.state.panel_images[panel.panel_id]
|
||||
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed))
|
||||
} else {
|
||||
clay_muted_text("img: not generated")
|
||||
}
|
||||
|
||||
desc := panel.description
|
||||
if len(desc) == 0 { desc = "(no description)" }
|
||||
clay_body_text(fmt.tprintf("desc: %s", desc), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if has_img {
|
||||
clay_muted_text(fmt.tprintf("src: %s", panel.panel_id))
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PanelListScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
}) {
|
||||
visible_rows: int = 8
|
||||
if visible_rows > panel_count { visible_rows = panel_count }
|
||||
start := idx - visible_rows / 2
|
||||
if start < 0 { start = 0 }
|
||||
end := start + visible_rows
|
||||
if end > panel_count { end = panel_count }
|
||||
if end - start < visible_rows {
|
||||
start = end - visible_rows
|
||||
if start < 0 { start = 0 }
|
||||
}
|
||||
|
||||
for i in start..<end {
|
||||
row_panel, row_page, row_ok := panel_by_flat_index(app.controller.state.script, i)
|
||||
if !row_ok { continue }
|
||||
|
||||
mark := " "
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if i == idx {
|
||||
mark = "> "
|
||||
row_color = CLAY_TEXT_BRIGHT
|
||||
}
|
||||
|
||||
ready := "missing"
|
||||
ready_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if _, err_exists := app.controller.state.panel_errors[row_panel.panel_id]; err_exists {
|
||||
ready = "error"
|
||||
ready_color = CLAY_ERROR
|
||||
}
|
||||
if _, exists := app.controller.state.panel_images[row_panel.panel_id]; exists {
|
||||
ready = "ready"
|
||||
ready_color = CLAY_SUCCESS
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_row_%d", i)))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(20)}, padding = {top = 2, left = 4}, childGap = 4},
|
||||
backgroundColor = clay.Color{0, 0, 0, 0},
|
||||
}) {
|
||||
clay.Text(fmt.tprintf("%s%02d p%d#%d", mark, i+1, row_page, row_panel.panel_number), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = row_color})
|
||||
clay.Text(ready, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = ready_color})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Layout Detail Panel (Clay) ────────────────────────────────────
|
||||
|
||||
// ─── Layout Detail Panel (Clay) ────────────────────────────────────
|
||||
declare_layout_detail :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("LayoutDetailCard"))(clay_card_style()) {
|
||||
clay_title_text("Layout Detail")
|
||||
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
clay_muted_text("No layouts yet. Use Layout Auto after panels.")
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_layout_cursor(layout_count, app.summary_opts.layout_page_cursor)
|
||||
layout_val := app.controller.state.page_layouts[idx]
|
||||
|
||||
if clay.UI(clay.ID("LayoutDetailHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Page %d/%d • %s • %d panels", idx+1, layout_count, layout_val.pattern_id, len(layout_val.panels)), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
declare_button_small("btn_layout_regen", "Regen")
|
||||
}
|
||||
|
||||
val := validate_layout_page(layout_val, app.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
|
||||
|
||||
if clay.UI(clay.ID("LayoutValidationRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4},
|
||||
}) {
|
||||
declare_status_badge("val_coverage", fmt.tprintf("Cov: %.0f%%", val.coverage_pct), coverage_ok)
|
||||
declare_status_badge("val_bindings", fmt.tprintf("Bind: %d miss", val.missing_bindings), bindings_ok)
|
||||
declare_status_badge("val_bounds", fmt.tprintf("Bounds: %d", val.bounds_violations), bounds_ok)
|
||||
}
|
||||
|
||||
clay_muted_text(fmt.tprintf("size: %d x %d", layout_val.width, layout_val.height))
|
||||
|
||||
if clay.UI(clay.ID("LayoutDetailContent"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, childGap = 8},
|
||||
}) {
|
||||
if clay.UI(clay.ID("LayoutWireframe"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(40), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom},
|
||||
backgroundColor = CLAY_BG_STRIP,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
for i in 0..<len(layout_val.panels) {
|
||||
cell := layout_val.panels[i].layout_cell
|
||||
w_pct := f32(cell.w * 100); if w_pct > 100 { w_pct = 100 } else if w_pct < 0 { w_pct = 0 }
|
||||
h_pct := f32(cell.h * 100); if h_pct > 100 { h_pct = 100 } else if h_pct < 0 { h_pct = 0 }
|
||||
if clay.UI(clay.ID(fmt.tprintf("wire_cell_%d", i)))({
|
||||
layout = {sizing = {width = clay.SizingPercent(w_pct), height = clay.SizingPercent(h_pct)}, padding = {top = 2, left = 2, right = 2, bottom = 2}},
|
||||
backgroundColor = CLAY_ACCENT_SURFACE,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
border = {color = CLAY_ACCENT_MUTED, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
if cell.w > 0.15 && cell.h > 0.15 {
|
||||
clay.Text(fmt.tprintf("%d", layout_val.panels[i].panel_number), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = CLAY_TEXT_SECONDARY})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("LayoutPageScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
}) {
|
||||
visible_rows: int = 6
|
||||
if visible_rows > layout_count { visible_rows = layout_count }
|
||||
start := idx - visible_rows / 2
|
||||
if start < 0 { start = 0 }
|
||||
end := start + visible_rows
|
||||
if end > layout_count { end = layout_count }
|
||||
if end - start < visible_rows {
|
||||
start = end - visible_rows
|
||||
if start < 0 { start = 0 }
|
||||
}
|
||||
|
||||
for i in start..<end {
|
||||
l := app.controller.state.page_layouts[i]
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if i == idx { row_color = CLAY_TEXT_BRIGHT }
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("layout_row_%d", i)))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(20)}, padding = {top = 2, left = 4}},
|
||||
backgroundColor = clay.Color{0, 0, 0, 0},
|
||||
}) {
|
||||
mark := " "
|
||||
if i == idx { mark = "> " }
|
||||
clay.Text(fmt.tprintf("%s%02d %s (%d)", mark, l.page_number, l.pattern_id, len(l.panels)), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = row_color})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Bubbles Detail Panel (Clay) ────────────────────────────────────
|
||||
|
||||
// ─── Bubbles Detail Panel (Clay) ────────────────────────────────────
|
||||
declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("BubblesDetailCard"))(clay_card_style()) {
|
||||
clay_title_text("Bubble Editor")
|
||||
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
clay_muted_text("No layouts yet. Run Layout Auto first.")
|
||||
return
|
||||
}
|
||||
|
||||
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
||||
layout_val := app.controller.state.page_layouts[page_idx]
|
||||
panel_count := len(layout_val.panels)
|
||||
|
||||
if panel_count == 0 {
|
||||
clay_body_text(fmt.tprintf("Page %d has no panels", layout_val.page_number), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
return
|
||||
}
|
||||
|
||||
panel_idx := clamp_bubble_cursor(panel_count, app.summary_opts.bubble_panel_cursor)
|
||||
panel := layout_val.panels[panel_idx]
|
||||
|
||||
clay_body_text(fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if clay.UI(clay.ID("BubblePanelInfo"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_muted_text(fmt.tprintf("Panel %d (id: %s)", panel.panel_number, panel.panel_id))
|
||||
declare_button_small("btn_bubble_auto_place", "Auto Place")
|
||||
}
|
||||
|
||||
bubbles_for_panel: []core.Speech_Bubble = nil
|
||||
if app.controller.state.speech_bubbles != nil {
|
||||
if slice, ok := app.controller.state.speech_bubbles[panel.panel_id]; ok {
|
||||
bubbles_for_panel = slice
|
||||
}
|
||||
}
|
||||
bubble_count := len(bubbles_for_panel)
|
||||
bubble_idx := 0
|
||||
if bubble_count > 0 {
|
||||
bubble_idx = clamp_bubble_cursor(bubble_count, app.summary_opts.bubble_edit_cursor)
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("BubbleListHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Bubbles: %d", bubble_count), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
declare_button_small("btn_bubble_add", "Add")
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("BubbleListScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
}) {
|
||||
if bubble_count == 0 {
|
||||
clay_muted_text("No bubbles for this panel. Click Add or Auto Place.")
|
||||
} else {
|
||||
for i in 0..<bubble_count {
|
||||
b := bubbles_for_panel[i]
|
||||
is_selected: bool = (i == bubble_idx)
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if is_selected { row_color = CLAY_TEXT_BRIGHT }
|
||||
|
||||
preview := b.text
|
||||
if len(preview) > 25 { preview = preview[:25] }
|
||||
if len(preview) == 0 { preview = "(empty)" }
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("bubble_row_%d", i)))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(26)}, padding = {top = 2, left = 4}, childGap = 4, childAlignment = {y = .Center}},
|
||||
backgroundColor = clay.Color{0, 0, 0, 0},
|
||||
}) {
|
||||
mark := " "
|
||||
if is_selected { mark = "> " }
|
||||
clay.Text(fmt.tprintf("%s[%s] %s", mark, bubble_type_name(b.type), preview), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = row_color})
|
||||
if is_selected {
|
||||
declare_button_small(fmt.tprintf("btn_bubble_delete_%d", i), "x")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bubble_count > 0 && bubble_idx < bubble_count {
|
||||
selected := bubbles_for_panel[bubble_idx]
|
||||
|
||||
if clay.UI(clay.ID("BubbleEditor"))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 4, right = 8, bottom = 4, left = 8}, childGap = 4},
|
||||
backgroundColor = CLAY_BG_STRIP,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Editing: %s", bubble_type_name(selected.type)), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if clay.UI(clay.ID("BubbleTypeRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 2},
|
||||
}) {
|
||||
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
|
||||
for t in types {
|
||||
btn_id := fmt.tprintf("btn_btype_%d", int(t))
|
||||
if clay.UI(clay.ID(btn_id))({
|
||||
layout = {sizing = {width = clay.SizingFit({min = 50, max = 20}), height = clay.SizingFixed(26)}, padding = {top = 2, right = 4, bottom = 2, left = 4}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = clay_color_for_bubble_type(selected.type == t),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay.Text(bubble_type_name(t), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = clay_text_color_for_bubble_type(selected.type == t)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text_preview := selected.text
|
||||
if len(text_preview) > 80 { text_preview = text_preview[:80] }
|
||||
clay_muted_text(fmt.tprintf("text: %s", text_preview))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clay_color_for_bubble_type :: proc(active: bool) -> clay.Color {
|
||||
if active { return CLAY_ACCENT }
|
||||
return CLAY_BTN_DEFAULT
|
||||
}
|
||||
|
||||
clay_text_color_for_bubble_type :: proc(active: bool) -> clay.Color {
|
||||
if active { return CLAY_TEXT_BRIGHT }
|
||||
return CLAY_TEXT_SECONDARY
|
||||
}
|
||||
|
||||
// ─── Action Log (Clay) ─────────────────────────────────────────────
|
||||
|
||||
// ─── Action Log (Clay) ─────────────────────────────────────────────
|
||||
declare_action_log :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("ActionLogCard"))(clay_card_style()) {
|
||||
clay_title_text("Action Log")
|
||||
|
||||
// Button row
|
||||
if clay.UI(clay.ID("LogBtnRow"))({layout = clay_row_layout()}) {
|
||||
declare_button_small("btn_log_reset", "Reset View")
|
||||
declare_button_small("btn_log_report", "Session Report")
|
||||
declare_button_small("btn_log_copy", "Copy Log")
|
||||
declare_button_small("btn_log_diag", "Diagnostics")
|
||||
declare_button_small("btn_log_status_copy", "Copy Status")
|
||||
declare_button_small("btn_log_clear", "Clear Log")
|
||||
declare_button_small("btn_log_diag_copy", "Copy Diag")
|
||||
}
|
||||
|
||||
order_label := "newest"
|
||||
if app.log_oldest_first { order_label = "oldest" }
|
||||
clay_muted_text(fmt.tprintf("View: %d lines, %s first", app.log_show_lines, order_label))
|
||||
|
||||
// Log entries
|
||||
if clay.UI(clay.ID("LogScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
max_lines: int = int(app.log_show_lines)
|
||||
if max_lines > app.action_log.count {
|
||||
max_lines = app.action_log.count
|
||||
}
|
||||
if max_lines > len(app.action_log.entries) {
|
||||
max_lines = len(app.action_log.entries)
|
||||
}
|
||||
now := rl.GetTime()
|
||||
for line in 0 ..< max_lines {
|
||||
idx := 0
|
||||
if app.log_oldest_first {
|
||||
start_idx := app.action_log.count - max_lines
|
||||
idx = (start_idx + line) % len(app.action_log.entries)
|
||||
if idx < 0 { idx += len(app.action_log.entries) }
|
||||
} else {
|
||||
idx = (app.action_log.count - 1 - line) % len(app.action_log.entries)
|
||||
if idx < 0 { idx += len(app.action_log.entries) }
|
||||
}
|
||||
age := now - app.action_log.entry_times[idx]
|
||||
entry_text := fmt.tprintf("[%2.0fs] %s", age, app.action_log.entries[idx])
|
||||
entry_color := CLAY_TEXT_SECONDARY
|
||||
if is_error_message(app.action_log.entries[idx]) { entry_color = CLAY_ERROR }
|
||||
else if is_warning_message(app.action_log.entries[idx]) { entry_color = CLAY_WARNING }
|
||||
else { entry_color = CLAY_SUCCESS }
|
||||
clay_body_text(entry_text, color = entry_color, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
328
odin/src/gui/workspaces.odin
Normal file
328
odin/src/gui/workspaces.odin
Normal file
@ -0,0 +1,328 @@
|
||||
package gui
|
||||
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "../core"
|
||||
|
||||
// ─── Script Workspace ────────────────────────────────────────────
|
||||
declare_script_workspace :: proc(app: ^GUI_App_State) {
|
||||
page_count := len(app.controller.state.script.pages)
|
||||
if page_count == 0 {
|
||||
if clay.UI(clay.ID("ScriptEmpty"))(clay_card_style()) {
|
||||
clay_title_text("Script")
|
||||
clay_body_text("No script pages yet.", color = CLAY_TEXT_TERTIARY)
|
||||
clay_body_text("1. Go to Story screen to set up your story idea", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text("2. Click 'Generate Script' or press F5", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_script_cursor(page_count, app.summary_opts.script_page_cursor)
|
||||
page := app.controller.state.script.pages[idx]
|
||||
|
||||
// Nav bar
|
||||
if clay.UI(clay.ID("ScriptNav"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Page %d/%d", idx+1, page_count), color = CLAY_ACCENT)
|
||||
declare_button_small("btn_script_prev", "< Prev")
|
||||
declare_button_small("btn_script_next", "Next >")
|
||||
// Title info
|
||||
title := app.controller.state.script.title
|
||||
if len(title) > 50 { title = fmt.tprintf("%s...", title[:47]) }
|
||||
clay_muted_text(title)
|
||||
}
|
||||
|
||||
// Detail card
|
||||
declare_script_detail(app)
|
||||
}
|
||||
|
||||
// ─── Panels Workspace ────────────────────────────────────────────
|
||||
|
||||
// ─── Panels Workspace ────────────────────────────────────────────
|
||||
declare_panels_workspace :: proc(app: ^GUI_App_State) {
|
||||
panel_count := count_script_panels(app.controller.state.script)
|
||||
if panel_count == 0 {
|
||||
if clay.UI(clay.ID("PanelsEmpty"))(clay_card_style()) {
|
||||
clay_title_text("Panels")
|
||||
clay_body_text("No panels generated yet.", color = CLAY_TEXT_TERTIARY)
|
||||
clay_body_text("Generate a script first, then click 'Generate Panels' or press F6", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
|
||||
if clay.UI(clay.ID("PanelsNav"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Panel %d/%d", idx+1, panel_count), color = CLAY_ACCENT)
|
||||
declare_button_small("btn_panels_prev", "< Prev")
|
||||
declare_button_small("btn_panels_next", "Next >")
|
||||
}
|
||||
|
||||
declare_panels_detail(app)
|
||||
}
|
||||
|
||||
// ─── Layout Workspace ────────────────────────────────────────────
|
||||
|
||||
// ─── Layout Workspace ────────────────────────────────────────────
|
||||
declare_layout_workspace :: proc(app: ^GUI_App_State) {
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
if clay.UI(clay.ID("LayoutEmpty"))(clay_card_style()) {
|
||||
clay_title_text("Layout")
|
||||
clay_body_text("No layouts yet.", color = CLAY_TEXT_TERTIARY)
|
||||
clay_body_text("Generate panels first, then click 'Layout Pages' or press F7", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_layout_cursor(layout_count, app.summary_opts.layout_page_cursor)
|
||||
|
||||
if clay.UI(clay.ID("LayoutNav"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Page %d/%d", idx+1, layout_count), color = CLAY_ACCENT)
|
||||
declare_button_small("btn_layout_prev", "< Prev")
|
||||
declare_button_small("btn_layout_next", "Next >")
|
||||
}
|
||||
|
||||
declare_layout_detail(app)
|
||||
}
|
||||
|
||||
// ─── Bubbles Workspace ───────────────────────────────────────────
|
||||
|
||||
// ─── Bubbles Workspace ───────────────────────────────────────────
|
||||
declare_bubbles_workspace :: proc(app: ^GUI_App_State) {
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
if clay.UI(clay.ID("BubblesEmpty"))(clay_card_style()) {
|
||||
clay_title_text("Bubbles")
|
||||
clay_body_text("No layouts yet.", color = CLAY_TEXT_TERTIARY)
|
||||
clay_body_text("Run Layout Auto first, then use this screen to edit speech bubbles", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
||||
layout_val := app.controller.state.page_layouts[page_idx]
|
||||
panel_count := len(layout_val.panels)
|
||||
|
||||
if clay.UI(clay.ID("BubblesNav"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), color = CLAY_ACCENT)
|
||||
declare_button_small("btn_bubbles_prev_page", "< Page")
|
||||
declare_button_small("btn_bubbles_next_page", "Page >")
|
||||
declare_button_small("btn_bubbles_prev_panel", "< Panel")
|
||||
declare_button_small("btn_bubbles_next_panel", "Panel >")
|
||||
}
|
||||
|
||||
declare_bubbles_detail(app)
|
||||
}
|
||||
|
||||
// ─── Story Workspace ──────────────────────────────────────────────
|
||||
|
||||
// ─── Story Workspace ──────────────────────────────────────────────
|
||||
declare_story_workspace :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("StoryCard"))(clay_card_style()) {
|
||||
clay_title_text("Story Setup")
|
||||
|
||||
clay_muted_text("Story Idea")
|
||||
if clay.UI(clay.ID("field_idea"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 80, max = 0})}, padding = {top = 8, right = 12, bottom = 8, left = 12}},
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.controller.state.story_idea if len(app.controller.state.story_idea) > 0 else "Enter story idea...")
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("StoryMetaRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12}}) {
|
||||
if clay.UI(clay.ID("StoryMetaLeft"))({layout = {layoutDirection = .TopToBottom, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 2}}) {
|
||||
clay_muted_text("Genre")
|
||||
if clay.UI(clay.ID("field_genre"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.controller.state.story_genre if len(app.controller.state.story_genre) > 0 else "Enter genre...")
|
||||
}
|
||||
}
|
||||
if clay.UI(clay.ID("StoryMetaRight"))({layout = {layoutDirection = .TopToBottom, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 2}}) {
|
||||
clay_muted_text("Audience")
|
||||
if clay.UI(clay.ID("field_audience"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.controller.state.target_audience if len(app.controller.state.target_audience) > 0 else "Enter audience...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PathCard"))(clay_card_style()) {
|
||||
clay_title_text("Project Paths")
|
||||
|
||||
clay_muted_text("Export Path")
|
||||
if clay.UI(clay.ID("field_export"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.export_path)
|
||||
}
|
||||
|
||||
clay_muted_text("Project Path")
|
||||
if clay.UI(clay.ID("field_project"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.project_path)
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("StoryConfigRow"))({layout = clay_row_layout()}) {
|
||||
clay_muted_text("Pages")
|
||||
if clay.UI(clay.ID("field_pages"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(60), height = clay.SizingFixed(34)}},
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(app.local_script_pages)
|
||||
}
|
||||
clay_muted_text("Format")
|
||||
declare_nav_chip("btn_pdf", "PDF", app.export_format == .PDF)
|
||||
declare_nav_chip("btn_png", "PNG", app.export_format == .PNG)
|
||||
declare_nav_chip("btn_cbz", "CBZ", app.export_format == .CBZ)
|
||||
clay_muted_text("Source")
|
||||
declare_nav_chip("btn_local", "Local", !app.use_deepseek_script)
|
||||
declare_nav_chip("btn_deepseek", "DS", app.use_deepseek_script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Characters Workspace ─────────────────────────────────────────
|
||||
|
||||
// ─── Characters Workspace ─────────────────────────────────────────
|
||||
declare_characters_workspace :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("CharsCard"))(clay_card_style()) {
|
||||
clay_title_text("Characters")
|
||||
char_count := len(app.controller.state.characters)
|
||||
if char_count == 0 {
|
||||
clay_body_text("No characters yet.", color = CLAY_TEXT_TERTIARY)
|
||||
clay_body_text("Characters are extracted when you generate a script.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text("Generate Script (F5) to populate characters from your story.", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
} else {
|
||||
clay_body_text(fmt.tprintf("%d characters found", char_count), color = CLAY_ACCENT)
|
||||
for i in 0..<char_count {
|
||||
char := app.controller.state.characters[i]
|
||||
role_label := character_role_label(char.role)
|
||||
if clay.UI(clay.ID(fmt.tprintf("char_%s", char.id)))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 4, right = 8, bottom = 4, left = 8}, childGap = 2},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("%s [%s]", char.name, role_label), color = CLAY_TEXT_PRIMARY)
|
||||
if len(char.description) > 0 {
|
||||
clay_body_text(char.description, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Export Workspace ─────────────────────────────────────────────
|
||||
character_role_label :: proc(role: core.Character_Role) -> string {
|
||||
switch role {
|
||||
case .Protagonist: return "Protagon."
|
||||
case .Antagonist: return "Antagon."
|
||||
case .Supporting: return "Support."
|
||||
case .Extra: return "Extra"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// ─── Export Workspace ─────────────────────────────────────────────
|
||||
declare_export_workspace :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("ExportCard"))(clay_card_style()) {
|
||||
clay_title_text("Export")
|
||||
|
||||
ready, total := ready_stage_count(app.controller)
|
||||
all_ready := ready == total
|
||||
|
||||
panel_count := len(app.controller.state.panel_images)
|
||||
page_count := len(app.controller.state.page_layouts)
|
||||
|
||||
if clay.UI(clay.ID("ExportStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 16}}) {
|
||||
declare_stat_chip("Pages", page_count)
|
||||
declare_stat_chip("Panels", panel_count)
|
||||
declare_stat_chip("Ready", ready)
|
||||
}
|
||||
|
||||
clay_muted_text(fmt.tprintf("Format: %v • Target: %s", app.controller.state.export_format, app.export_path))
|
||||
|
||||
reason := export_block_reason(app.controller.state)
|
||||
if len(reason) > 0 {
|
||||
if clay.UI(clay.ID("ExportBlocked"))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 8, right = 12, bottom = 8, left = 12}, childGap = 4},
|
||||
backgroundColor = CLAY_ERROR_DIM,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Blocked: %s", reason), color = CLAY_ERROR)
|
||||
clay_body_text("Complete all pipeline stages before exporting.", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
} else {
|
||||
if clay.UI(clay.ID("ExportReady"))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 8, right = 12, bottom = 8, left = 12}, childGap = 4},
|
||||
backgroundColor = CLAY_SUCCESS_DIM,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay_body_text("All pipeline stages complete. Ready!", color = CLAY_SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
// Export action buttons
|
||||
if clay.UI(clay.ID("ExportActions"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8}}) {
|
||||
format := app.controller.state.export_format
|
||||
declare_button_state_export("btn_export_now", "Export as PDF", format == .PDF)
|
||||
declare_button_state_export("btn_export_png", "Export as PNG", format == .PNG)
|
||||
declare_button_state_export("btn_export_cbz", "Export as CBZ", format == .CBZ)
|
||||
}
|
||||
|
||||
if page_count > 0 {
|
||||
if clay.UI(clay.ID("ExportPagesList"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
}) {
|
||||
for l in app.controller.state.page_layouts {
|
||||
clay_muted_text(fmt.tprintf("Page %d • %s • %d panels", l.page_number, l.pattern_id, len(l.panels)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare_button_state_export :: proc(id: string, label: string, is_current_format: bool) {
|
||||
bg := CLAY_BTN_DEFAULT
|
||||
if is_current_format { bg = CLAY_BTN_SOFT }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_FRAC_BTN),
|
||||
}) {
|
||||
clay_body_text(label)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Community Workspace ──────────────────────────────────────────
|
||||
|
||||
// ─── Community Workspace ──────────────────────────────────────────
|
||||
declare_community_workspace :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("CommunityCard"))(clay_card_style()) {
|
||||
clay_title_text("Community")
|
||||
clay_body_text("Community features coming soon", color = CLAY_TEXT_TERTIARY)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Bottom Bar ───────────────────────────────────────────────────
|
||||
Loading…
Reference in New Issue
Block a user