From abc74582d62b4c3a76f1d27df5e48dce770633c1 Mon Sep 17 00:00:00 2001 From: echo Date: Fri, 22 May 2026 17:33:57 +0200 Subject: [PATCH] Phase A: Split runtime.odin into chrome.odin, workspaces.odin, detail_panels.odin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- odin/src/gui/chrome.odin | 248 +++++++ odin/src/gui/clay_layout.odin | 42 +- odin/src/gui/detail_panels.odin | 440 +++++++++++++ odin/src/gui/runtime.odin | 1072 +++---------------------------- odin/src/gui/workspaces.odin | 328 ++++++++++ 5 files changed, 1148 insertions(+), 982 deletions(-) create mode 100644 odin/src/gui/chrome.odin create mode 100644 odin/src/gui/detail_panels.odin create mode 100644 odin/src/gui/workspaces.odin diff --git a/odin/src/gui/chrome.odin b/odin/src/gui/chrome.odin new file mode 100644 index 0000000..5616f1f --- /dev/null +++ b/odin/src/gui/chrome.odin @@ -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 ────────────────────────────────────────────── diff --git a/odin/src/gui/clay_layout.odin b/odin/src/gui/clay_layout.odin index b2f3760..74b6263 100644 --- a/odin/src/gui/clay_layout.odin +++ b/odin/src/gui/clay_layout.odin @@ -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}, diff --git a/odin/src/gui/detail_panels.odin b/odin/src/gui/detail_panels.odin new file mode 100644 index 0000000..6e6d441 --- /dev/null +++ b/odin/src/gui/detail_panels.odin @@ -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..= 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.. 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.. 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.. 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) + } + } + } +} \ No newline at end of file diff --git a/odin/src/gui/runtime.odin b/odin/src/gui/runtime.odin index c1e5da1..ebbd988 100644 --- a/odin/src/gui/runtime.odin +++ b/odin/src/gui/runtime.odin @@ -299,8 +299,6 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { push_status_if_nonempty(&app.status_msg, &app.action_log, autosave_tick_with_message(&app.project_path, app.controller.state, app.autosave_enabled, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, app.autosave_interval_s)) // ─── Clay Layout Declaration ────────────────────────────── - main_w := shared.compute_main_width(screen_w) - status_w := (main_w - 2) / 2 clay.BeginLayout() @@ -310,17 +308,16 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { backgroundColor = CLAY_BG_BASE, }) { // ─── Sidebar ─────────────────────────────────────── - declare_sidebar(&app, screen_h) + declare_sidebar(&app) // ─── Main Content ──────────────────────────────── if clay.UI(clay.ID("MainArea"))({ layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom}, }) { - declare_topbar(&app) - declare_project_setup(&app) - declare_actions_row(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key) - declare_status_card(&app) - declare_detail_area(&app, status_w) + declare_pipeline_bar(&app) + declare_workspace(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count) + next_hint := gui_next_hint_with_source(app.controller, app.use_deepseek_script) + declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint) } } @@ -352,312 +349,6 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error { return shared.ok() } -// ─── Sidebar Declaration ───────────────────────────────────────── -declare_sidebar :: proc(app: ^GUI_App_State, screen_h: i32) { - screens := []ui.App_Screen{.Story, .Script, .Characters, .Panels, .Layout, .Bubbles, .Export, .Community} - screen_names := []string{"1 Story", "2 Script", "3 Characters", "4 Panels", "5 Layout", "6 Bubbles", "7 Export", "8 Community"} - - if clay.UI(clay.ID("Sidebar"))({ - layout = {sizing = {width = clay.SizingFixed(282), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = 16, right = 16, bottom = 16, left = 16}, childGap = 2}, - backgroundColor = CLAY_BG_SIDEBAR, - }) { - // Brand - if clay.UI(clay.ID("BrandCard"))(clay_card_style()) { - clay_title_text("comic-odin") - clay_muted_text("Native GUI") - } - - // 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(32)}, padding = {top = 6, right = 12, bottom = 6, left = 12}, childAlignment = {y = .Center}}, - 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(screen_names[i], color = text_color) - } - } - - // Shortcuts card (compact) - if clay.UI(clay.ID("ShortcutsCard"))(clay_card_style()) { - clay_muted_text("F5-F10: Pipeline") - clay_muted_text("Ctrl+S/N/O/E") - clay_muted_text("/: Help Esc: Close") - } - } -} - -// ─── Topbar Declaration ────────────────────────────────────────── -declare_topbar :: proc(app: ^GUI_App_State) { - if clay.UI(clay.ID("Topbar"))({ - layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 72, max = 72})}, padding = {top = 12, right = 20, bottom = 12, left = 20}, childGap = 16, childAlignment = {y = .Center}}, - backgroundColor = CLAY_BG_TOPBAR, - border = {color = CLAY_BORDER_DIVIDER, width = clay.BorderOutside(1)}, - }) { - screen_name := ui.screen_name(app.controller.active_screen) - if clay.UI(clay.ID("ScreenTitle"))({ - layout = {sizing = {width = clay.SizingFixed(200)}}, - }) { - clay_title_text(screen_name) - } - - // 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 := []struct{name: string, done: bool}{{"Script", script_ok}, {"Panels", panels_ok}, {"Layout", layout_ok}, {"Export", export_ok}} - for i in 0 ..< len(steps) { - circle_color := CLAY_BTN_DISABLED - text_color := CLAY_TEXT_TERTIARY - if steps[i].done { - circle_color = CLAY_ACCENT - text_color = CLAY_TEXT_PRIMARY - } - if clay.UI(clay.ID("Step", u32(i)))({ - layout = {sizing = {width = clay.SizingFixed(120), height = clay.SizingFixed(36)}, layoutDirection = .TopToBottom, childAlignment = {x = .Center}, padding = {top = 0, right = 0, bottom = 0, left = 0}, childGap = 2}, - }) { - if clay.UI(clay.ID("StepCircle", u32(i)))({ - layout = {sizing = {width = clay.SizingFixed(16), height = clay.SizingFixed(16)}}, - backgroundColor = circle_color, - cornerRadius = clay.CornerRadiusAll(8), - }) { - if steps[i].done { - // checkmark drawn as text - clay.Text("v", {fontId = CLAY_FONT_BODY, fontSize = 10, textColor = CLAY_BG_BASE}) - } - } - clay.Text(steps[i].name, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = text_color}) - } - if i < len(steps) - 1 { - line_color: clay.Color = clay.Color{255, 255, 255, 30} - if steps[i].done { line_color = CLAY_ACCENT } - if clay.UI(clay.ID("StepLine", u32(i)))({ - layout = {sizing = {width = clay.SizingFixed(20), height = clay.SizingFixed(2)}}, - backgroundColor = line_color, - }) {} - } - } - - // Pages input - if clay.UI(clay.ID("PagesInput"))({ - layout = {sizing = {width = clay.SizingFixed(200)}, childAlignment = {y = .Center}, childGap = 8, layoutDirection = .LeftToRight}, - }) { - clay_body_text("Pages:", color = CLAY_TEXT_SECONDARY) - if clay.UI(clay.ID("field_pages"))({ - layout = clay_input_layout(), - backgroundColor = CLAY_BG_INPUT, - cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM), - }) { - clay_body_text(app.local_script_pages) - } - } - } -} - -// ─── Project Setup Declaration ───────────────────────────────────── -declare_project_setup :: proc(app: ^GUI_App_State) { - if clay.UI(clay.ID("ProjectSetup"))(clay_card_style()) { - clay_title_text("Project Setup") - // Story Idea - clay_muted_text("Story Idea") - if clay.UI(clay.ID("field_idea"))({ - layout = clay_input_layout(), - 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...") - } - // Genre - 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...") - } - // Audience - 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...") - } - // Export Path - 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) - } - // Project 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) - } - // Format row - if clay.UI(clay.ID("FormatRow"))({layout = clay_row_layout()}) { - 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("Script Source") - declare_nav_chip("btn_local", "Local", !app.use_deepseek_script) - declare_nav_chip("btn_deepseek", "DeepSeek", app.use_deepseek_script) - } - } -} - -// ─── Actions Row ────────────────────────────────────────────────── -declare_actions_row :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek: bool) { - next_hint := gui_next_hint_with_source(app.controller, app.use_deepseek_script) - - if clay.UI(clay.ID("ActionsRow"))(clay_card_style()) { - clay_title_text("Actions") - // Row 1: Pipeline actions - if clay.UI(clay.ID("PipelineRow"))({layout = clay_row_layout()}) { - declare_button_danger("btn_new", "New Project") - script_label: string = "Generate Script Local" - if app.use_deepseek_script { script_label = "Generate Script" } - declare_button_state("btn_script", script_label, can_gen_panels, recommended = (next_hint == "Generate Script" || next_hint == "Generate Script Local")) - declare_button_state("btn_panels", "Generate Panels", can_gen_panels, next_hint == "Generate Panels Local") - declare_button_state("btn_layout", "Layout Pages", can_layout, recommended = (next_hint == "Layout")) - declare_button_state("btn_export", "Export", can_export, recommended = (next_hint == "Export")) - } - // Row 2: Session actions - if clay.UI(clay.ID("SessionRow"))({layout = clay_row_layout()}) { - declare_button_soft("btn_save", "Save") - declare_button_soft("btn_open", "Open") - declare_button_primary("btn_next", "Next Step") - declare_button_primary("btn_auto", "Auto-All") - declare_button_primary("btn_autosave", "Auto-All + Save") - } - // Row 3: Utility - if clay.UI(clay.ID("UtilityRow"))({layout = clay_row_layout()}) { - autosave_label: string = "Autosave: no" - if app.autosave_enabled { autosave_label = "Autosave: yes" } - declare_button_soft("btn_autosave_toggle", autosave_label) - clay_muted_text("Interval(s)") - if clay.UI(clay.ID("field_autosave"))({ - layout = {sizing = {width = clay.SizingFixed(60), height = clay.SizingFixed(30)}}, - backgroundColor = CLAY_BG_INPUT, - cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM), - }) { - clay_body_text(app.autosave_interval_text) - } - declare_button_small("btn_as15", "15") - declare_button_small("btn_as30", "30") - declare_button_small("btn_as60", "60") - declare_button_soft("btn_help", "Help (/)") - declare_button_default("btn_clear", "Clear Field") - declare_button_soft("btn_reset", "Reset Helpers") - } - } -} - -// ─── Status Card ────────────────────────────────────────────────── -declare_status_card :: proc(app: ^GUI_App_State) { - ready_count, total_count := ready_stage_count(app.controller) - progress := f32(0) - if total_count > 0 { progress = f32(ready_count) / f32(total_count) } - - if clay.UI(clay.ID("StatusCard"))(clay_card_style()) { - clay_title_text("Status") - clay_body_text(app.status_msg) - - // Readiness row - 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 - - if clay.UI(clay.ID("ReadinessRow"))({layout = clay_row_layout()}) { - declare_status_badge("rdy_script", "Script", script_ok) - declare_status_badge("rdy_panels", "Panels", panels_ok) - declare_status_badge("rdy_layout", "Layout", layout_ok) - declare_status_badge("rdy_export", "Export", export_ok) - } - - // Progress bar - if clay.UI(clay.ID("ProgressTrack"))({ - layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(8)}}, - backgroundColor = CLAY_PROGRESS_TRACK, - cornerRadius = clay.CornerRadiusAll(4), - }) { - if progress > 0 { - if clay.UI(clay.ID("ProgressFill"))({ - layout = {sizing = {width = clay.SizingPercent(progress), height = clay.SizingGrow({})}}, - backgroundColor = CLAY_PROGRESS_FILL, - cornerRadius = clay.CornerRadiusAll(4), - }) {} - } - } - clay_muted_text(fmt.tprintf("Pipeline: %d/%d", ready_count, total_count)) - clay_body_text(fmt.tprintf("Next: %s", gui_next_hint_with_source(app.controller, app.use_deepseek_script)), color = CLAY_TEXT_SECONDARY) - - if clay.UI(clay.ID("StatusMetaRow"))({layout = clay_row_layout()}) { - declare_status_badge("badge_dirty", fmt.tprintf("Dirty: %s", yn(app.is_dirty)), !app.is_dirty) - declare_status_badge("badge_autosave", fmt.tprintf("Autosave: %s (%ds)", yn(app.autosave_enabled), parse_autosave_interval(app.autosave_interval_text, 20)), app.autosave_enabled) - } - } -} - -// ─── Detail Area ────────────────────────────────────────────────── -declare_detail_area :: proc(app: ^GUI_App_State, status_w: i32) { - if clay.UI(clay.ID("DetailArea"))({ - layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .LeftToRight, childGap = 2}, - }) { - // Summary panel (left) — summary content rendered via Clay - if clay.UI(clay.ID("SummaryPanel"))({ - layout = {sizing = {width = clay.SizingFixed(f32(status_w)), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = clay.PaddingAll(8)}, - backgroundColor = CLAY_BG_CARD, - cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD), - }) { - declare_screen_summary(app) - } - - // Detail/log panel (right) — detail panels or action log - if clay.UI(clay.ID("DetailLogPanel"))({ - layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = clay.PaddingAll(8)}, - backgroundColor = CLAY_BG_CARD, - cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD), - }) { - screen := app.controller.active_screen - if screen == .Script { - declare_script_detail(app) - } else if screen == .Panels { - declare_panels_detail(app) - } else if screen == .Layout { - declare_layout_detail(app) - } else if screen == .Bubbles { - declare_bubbles_detail(app) - } else { - declare_action_log(app) - } - } - } -} - // ─── Click Processing ────────────────────────────────────────────── process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool) { // Navigation @@ -668,8 +359,16 @@ process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_expo } } + // Pipeline stepper clicks → navigate to corresponding screen + pipeline_screens := []ui.App_Screen{.Script, .Panels, .Layout, .Export} + for i in 0 ..< len(pipeline_screens) { + if clicked(clay.ID("PStep", u32(i))) { + push_status(&app.status_msg, &app.action_log, navigate_screen_with_status(&app.controller, pipeline_screens[i])) + } + } + // Input field focus - input_ids := []string{"field_idea", "field_genre", "field_audience", "field_export", "field_pages", "field_project", "field_autosave"} + input_ids := []string{"field_idea", "field_genre", "field_audience", "field_export", "field_pages", "field_project"} for i in 0 ..< len(input_ids) { if clicked(clay.ID(input_ids[i])) { app.selected_field = i @@ -695,6 +394,15 @@ process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_expo if clicked(clay.ID("btn_panels")) { push_status(&app.status_msg, &app.action_log, run_panels_action(&app.controller, can_gen_panels, &app.is_dirty)) } if clicked(clay.ID("btn_layout")) { push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty)) } if clicked(clay.ID("btn_export")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) } + if clicked(clay.ID("btn_export_now")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) } + if clicked(clay.ID("btn_export_png")) { + push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PNG, &app.is_dirty)) + push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) + } + if clicked(clay.ID("btn_export_cbz")) { + push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty)) + push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) + } if clicked(clay.ID("btn_next")) { push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at)) } if clicked(clay.ID("btn_auto")) { push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at)) } if clicked(clay.ID("btn_autosave")) { push_status(&app.status_msg, &app.action_log, run_auto_all_save_action(&app.controller, &app.project_path, &app.export_path, app.export_format, pages_count, app.use_deepseek_script, &app.is_dirty, &app.last_export_at, &app.last_autosave_at, &app.last_save_at)) } @@ -705,65 +413,97 @@ process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_expo } if clicked(clay.ID("btn_autosave_toggle")) { push_status(&app.status_msg, &app.action_log, toggle_autosave_with_message(&app.autosave_enabled)) } if clicked(clay.ID("btn_help")) { toggle_help_overlay(&app.show_help_overlay) } - if clicked(clay.ID("btn_clear")) { push_status(&app.status_msg, &app.action_log, clear_selected_field_with_message(app.selected_field, &app.controller.state, &app.export_path, &app.local_script_pages, &app.project_path, &app.autosave_interval_text, &app.is_dirty)) } - if clicked(clay.ID("btn_reset")) { push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, reset_helper_fields_with_message(&app.export_path, &app.local_script_pages, &app.autosave_interval_text, app.export_format)) } - if clicked(clay.ID("btn_as15")) { push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 15)) } - if clicked(clay.ID("btn_as30")) { push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 30)) } - if clicked(clay.ID("btn_as60")) { push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 60)) } + if clicked(clay.ID("btn_log_reset")) { push_status(&app.status_msg, &app.action_log, reset_log_view_with_message(&app.log_show_lines, &app.log_oldest_first)) } + if clicked(clay.ID("btn_log_clear")) { set_status(&app.status_msg, clear_action_log_with_message(&app.action_log)) } + if clicked(clay.ID("btn_log_report")) { + diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) + push_status(&app.status_msg, &app.action_log, write_session_report_with_message(diag_ctx)) + } + if clicked(clay.ID("btn_log_copy")) { + diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) + push_status(&app.status_msg, &app.action_log, copy_action_log_snapshot_with_message(diag_ctx)) + } + if clicked(clay.ID("btn_log_diag")) { + diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) + push_status(&app.status_msg, &app.action_log, write_diagnostics_with_message(diag_ctx)) + } + if clicked(clay.ID("btn_log_status_copy")) { + push_status(&app.status_msg, &app.action_log, copy_text_with_status(app.status_msg, "Copied status to clipboard")) + } + if clicked(clay.ID("btn_log_diag_copy")) { + diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) + push_status(&app.status_msg, &app.action_log, copy_diagnostics_with_message(diag_ctx)) + } - // Summary buttons screen := app.controller.active_screen - if clicked(clay.ID("btn_summary_show")) { - push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_show_if_supported(screen, &app.summary_opts)) - } - if clicked(clay.ID("btn_summary_sort")) { - push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_sort_if_supported(screen, &app.summary_opts)) - } - if clicked(clay.ID("btn_summary_prev")) { - if screen == .Script && len(app.controller.state.script.pages) > 0 { - app.summary_opts.script_page_cursor -= 1 - if app.summary_opts.script_page_cursor < 0 { app.summary_opts.script_page_cursor = len(app.controller.state.script.pages) - 1 } - push_status(&app.status_msg, &app.action_log, fmt.tprintf("Viewing script page %d/%d", app.summary_opts.script_page_cursor+1, len(app.controller.state.script.pages))) - } - } - if clicked(clay.ID("btn_summary_next")) { - if screen == .Script && len(app.controller.state.script.pages) > 0 { - app.summary_opts.script_page_cursor += 1 - if app.summary_opts.script_page_cursor >= len(app.controller.state.script.pages) { app.summary_opts.script_page_cursor = 0 } - push_status(&app.status_msg, &app.action_log, fmt.tprintf("Viewing script page %d/%d", app.summary_opts.script_page_cursor+1, len(app.controller.state.script.pages))) - } - } - // Log action buttons (only active when not on detail screens) - if screen != .Script && screen != .Panels && screen != .Layout && screen != .Bubbles { - if clicked(clay.ID("btn_log_reset")) { - push_status(&app.status_msg, &app.action_log, reset_log_view_with_message(&app.log_show_lines, &app.log_oldest_first)) + // Workspace navigation buttons + if screen == .Script { + page_count := len(app.controller.state.script.pages) + if clicked(clay.ID("btn_script_prev")) && page_count > 0 { + app.summary_opts.script_page_cursor -= 1 + if app.summary_opts.script_page_cursor < 0 { app.summary_opts.script_page_cursor = page_count - 1 } } - if clicked(clay.ID("btn_log_report")) { - diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) - push_status(&app.status_msg, &app.action_log, write_session_report_with_message(diag_ctx)) + if clicked(clay.ID("btn_script_next")) && page_count > 0 { + app.summary_opts.script_page_cursor += 1 + if app.summary_opts.script_page_cursor >= page_count { app.summary_opts.script_page_cursor = 0 } } - if clicked(clay.ID("btn_log_copy")) { - diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) - push_status(&app.status_msg, &app.action_log, copy_action_log_snapshot_with_message(diag_ctx)) + } + if screen == .Panels { + panel_count := count_script_panels(app.controller.state.script) + if clicked(clay.ID("btn_panels_prev")) && panel_count > 0 { + app.summary_opts.panel_cursor -= 1 + if app.summary_opts.panel_cursor < 0 { app.summary_opts.panel_cursor = panel_count - 1 } } - if clicked(clay.ID("btn_log_diag")) { - diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) - push_status(&app.status_msg, &app.action_log, write_diagnostics_with_message(diag_ctx)) + if clicked(clay.ID("btn_panels_next")) && panel_count > 0 { + app.summary_opts.panel_cursor += 1 + if app.summary_opts.panel_cursor >= panel_count { app.summary_opts.panel_cursor = 0 } } - if clicked(clay.ID("btn_log_status_copy")) { - push_status(&app.status_msg, &app.action_log, copy_text_with_status(app.status_msg, "Copied status to clipboard")) + } + if screen == .Layout { + layout_count := len(app.controller.state.page_layouts) + if clicked(clay.ID("btn_layout_prev")) && layout_count > 0 { + app.summary_opts.layout_page_cursor -= 1 + if app.summary_opts.layout_page_cursor < 0 { app.summary_opts.layout_page_cursor = layout_count - 1 } } - if clicked(clay.ID("btn_log_clear")) { - set_status(&app.status_msg, clear_action_log_with_message(&app.action_log)) + if clicked(clay.ID("btn_layout_next")) && layout_count > 0 { + app.summary_opts.layout_page_cursor += 1 + if app.summary_opts.layout_page_cursor >= layout_count { app.summary_opts.layout_page_cursor = 0 } } - if clicked(clay.ID("btn_log_diag_copy")) { - diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first) - push_status(&app.status_msg, &app.action_log, copy_diagnostics_with_message(diag_ctx)) + } + if screen == .Bubbles { + layout_count := len(app.controller.state.page_layouts) + if clicked(clay.ID("btn_bubbles_prev_page")) && layout_count > 0 { + app.summary_opts.bubble_page_cursor -= 1 + if app.summary_opts.bubble_page_cursor < 0 { app.summary_opts.bubble_page_cursor = layout_count - 1 } + app.summary_opts.bubble_panel_cursor = 0 + app.summary_opts.bubble_edit_cursor = 0 + } + if clicked(clay.ID("btn_bubbles_next_page")) && layout_count > 0 { + app.summary_opts.bubble_page_cursor += 1 + if app.summary_opts.bubble_page_cursor >= layout_count { app.summary_opts.bubble_page_cursor = 0 } + app.summary_opts.bubble_panel_cursor = 0 + app.summary_opts.bubble_edit_cursor = 0 + } + if layout_count > 0 { + 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 clicked(clay.ID("btn_bubbles_prev_panel")) && panel_count > 0 { + app.summary_opts.bubble_panel_cursor -= 1 + if app.summary_opts.bubble_panel_cursor < 0 { app.summary_opts.bubble_panel_cursor = panel_count - 1 } + app.summary_opts.bubble_edit_cursor = 0 + } + if clicked(clay.ID("btn_bubbles_next_panel")) && panel_count > 0 { + app.summary_opts.bubble_panel_cursor += 1 + if app.summary_opts.bubble_panel_cursor >= panel_count { app.summary_opts.bubble_panel_cursor = 0 } + app.summary_opts.bubble_edit_cursor = 0 + } } } // Panels detail buttons + screen = app.controller.active_screen if screen == .Panels { if clicked(clay.ID("btn_panel_regenerate")) { panel_count := count_script_panels(app.controller.state.script) @@ -883,7 +623,7 @@ declare_nav_chip :: proc(id: string, label: string, active: bool) { bg: clay.Color = clay.Color{0, 0, 0, 0} if active { bg = CLAY_NAV_HOVER_BG } if clay.UI(clay.ID(id))({ - layout = {sizing = {width = clay.SizingFit({min = 60, max = 30}), height = clay.SizingFixed(30)}, padding = {top = 4, right = 12, bottom = 4, left = 12}, childAlignment = {x = .Center, y = .Center}}, + layout = {sizing = {width = clay.SizingFit({min = 70, max = 34}), height = clay.SizingFixed(34)}, padding = {top = 4, right = 12, bottom = 4, left = 12}, childAlignment = {x = .Center, y = .Center}}, backgroundColor = bg, cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM), }) { @@ -943,29 +683,13 @@ declare_button_recommended :: proc(id: string, label: string) { } } -declare_button_state :: proc(id: string, label: string, enabled: bool, recommended := false) { - if !enabled { - if clay.UI(clay.ID(id))({ - layout = clay_button_layout(), - backgroundColor = CLAY_BTN_DISABLED, - cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_FRAC_BTN), - }) { - clay_body_text(label, color = CLAY_TEXT_DISABLED) - } - } else if recommended { - declare_button_recommended(id, label) - } else { - declare_button_default(id, label) - } -} - declare_status_badge :: proc(id: string, label: string, ok: bool) { badge_color: clay.Color = CLAY_ERROR if ok { badge_color = CLAY_SUCCESS } badge_bg: clay.Color = CLAY_ERROR_DIM if ok { badge_bg = CLAY_SUCCESS_DIM } if clay.UI(clay.ID(id))({ - layout = {sizing = {width = clay.SizingFit({min = 60, max = 22}), height = clay.SizingFixed(22)}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}}, + layout = {sizing = {width = clay.SizingFit({min = 70, max = 28}), height = clay.SizingFixed(26)}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}}, backgroundColor = badge_bg, cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD), border = {color = badge_color, width = clay.BorderOutside(1)}, @@ -1078,175 +802,11 @@ declare_toast :: proc(log: ^Action_Log) { } } -// ─── Screen Summary (Clay) ────────────────────────────────────────── -declare_screen_summary :: proc(app: ^GUI_App_State) { - if clay.UI(clay.ID("SummaryContent"))({ - layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 4}, - }) { - // Title + stat chips row - if clay.UI(clay.ID("SummaryHeader"))({layout = clay_row_layout()}) { - clay_title_text("Screen Summary", size = CLAY_FONT_SIZE_LG) - // Stat chips - declare_stat_chip("Pages", len(app.controller.state.script.pages)) - declare_stat_chip("Panels", len(app.controller.state.panel_images)) - declare_stat_chip("Layout", len(app.controller.state.page_layouts)) - } - - // Screen-specific content - switch app.controller.active_screen { - case .Story: - clay_muted_text(fmt.tprintf("Idea length: %d chars", len(app.controller.state.story_idea))) - clay_muted_text(fmt.tprintf("Genre: %s", app.controller.state.story_genre)) - clay_muted_text(fmt.tprintf("Audience: %s", app.controller.state.target_audience)) - clay_body_text("Use Generate Script Local to begin", color = CLAY_TEXT_TERTIARY) - - case .Script: - clay_muted_text(fmt.tprintf("Title: %s", app.controller.state.script.title)) - page_count := len(app.controller.state.script.pages) - clay_muted_text(fmt.tprintf("Pages: %d | Characters: %d", page_count, len(app.controller.state.script.characters))) - if page_count == 0 { - clay_body_text("No script pages yet. Generate Script Local to continue.", color = CLAY_TEXT_TERTIARY) - } else { - cursor := app.summary_opts.script_page_cursor - if cursor < 0 { cursor = 0 } - if cursor >= page_count { cursor = page_count - 1 } - page := app.controller.state.script.pages[cursor] - clay_body_text(fmt.tprintf("Viewing page %d/%d (script page #%d)", cursor+1, page_count, page.page_number), color = CLAY_ACCENT) - clay_muted_text(fmt.tprintf("Panels on page: %d", len(page.panels))) - 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)" } - clay_muted_text(fmt.tprintf("• P%d: %s", pn.panel_number, desc)) - if len(pn.dialogue) > 0 { - clay_muted_text(fmt.tprintf(" \"%s\"", pn.dialogue[0].text)) - } - } - if len(page.panels) > show_panels { - clay_body_text(fmt.tprintf("+%d more panels", len(page.panels)-show_panels), color = CLAY_ACCENT) - } - } - - case .Characters: - clay_muted_text(fmt.tprintf("Character count: %d", len(app.controller.state.characters))) - clay_body_text("Character editor is scaffolded", color = CLAY_TEXT_TERTIARY) - clay_body_text("Use script generation to populate", color = CLAY_TEXT_TERTIARY) - - case .Panels: - script_panel_count := count_script_panels(app.controller.state.script) - clay_muted_text(fmt.tprintf("Panel images: %d", len(app.controller.state.panel_images))) - clay_muted_text(fmt.tprintf("Script panels: %d", script_panel_count)) - clay_muted_text(fmt.tprintf("Script pages: %d", len(app.controller.state.script.pages))) - if script_panel_count == 0 { - clay_body_text("No script panels yet. Generate Script first.", color = CLAY_TEXT_TERTIARY) - } else { - pidx := clamp_panel_cursor(script_panel_count, app.summary_opts.panel_cursor) - panel, page_num, ok := panel_by_flat_index(app.controller.state.script, pidx) - if ok { - status := "missing" - if _, has_img := app.controller.state.panel_images[panel.panel_id]; has_img { status = "ready" } - clay_body_text(fmt.tprintf("Viewing panel %d/%d • page %d # %d", pidx+1, script_panel_count, page_num, panel.panel_number), color = CLAY_ACCENT) - clay_muted_text(fmt.tprintf("%s • %s", panel.panel_id, status)) - } - } - - case .Layout: - clay_muted_text(fmt.tprintf("Layout pages: %d", len(app.controller.state.page_layouts))) - clay_muted_text(fmt.tprintf("Page size: %v", app.controller.state.page_size)) - layout_count := len(app.controller.state.page_layouts) - if layout_count == 0 { - clay_body_text("No layouts yet. Use Layout Auto after panels are ready.", color = CLAY_TEXT_TERTIARY) - } else { - show_count := layout_count - if !app.summary_opts.layout_show_all && show_count > 3 { show_count = 3 } - for i in 0 ..< show_count { - idx := i - if app.summary_opts.layout_desc { idx = layout_count - 1 - i } - if idx >= 0 && idx < layout_count { - l := app.controller.state.page_layouts[idx] - clay_muted_text(fmt.tprintf("- P%d: %s (%d)", l.page_number, l.pattern_id, len(l.panels))) - } - } - } - - case .Bubbles: - bubble_count := 0 - if app.controller.state.speech_bubbles != nil { - for _, bubbles in app.controller.state.speech_bubbles { - bubble_count += len(bubbles) - } - } - clay_muted_text(fmt.tprintf("Total bubbles: %d", bubble_count)) - clay_muted_text(fmt.tprintf("Layout pages: %d", len(app.controller.state.page_layouts))) - if len(app.controller.state.page_layouts) == 0 { - clay_body_text("No layouts yet. Run Layout Auto first.", color = CLAY_TEXT_TERTIARY) - } else if bubble_count == 0 { - clay_body_text("No bubbles yet. Use Auto Place or Add on Bubbles screen.", color = CLAY_TEXT_TERTIARY) - } else { - clay_body_text(fmt.tprintf("Panels with bubbles: %d", len(app.controller.state.speech_bubbles)), color = CLAY_ACCENT) - clay_body_text("Select a panel in Bubbles screen to edit.", color = CLAY_TEXT_TERTIARY) - } - - case .Export: - clay_muted_text(fmt.tprintf("Format: %v", app.controller.state.export_format)) - clay_muted_text(fmt.tprintf("Layouts: %d | Panels: %d", len(app.controller.state.page_layouts), len(app.controller.state.panel_images))) - clay_muted_text(fmt.tprintf("Target: %s", app.export_path)) - if len(app.controller.state.page_layouts) > 0 { - last := app.controller.state.page_layouts[len(app.controller.state.page_layouts)-1] - clay_muted_text(fmt.tprintf("Last layout pattern: %s", last.pattern_id)) - } - reason := export_block_reason(app.controller.state) - if len(reason) > 0 { - clay_body_text(fmt.tprintf("Export blocked: %s", reason), color = CLAY_ERROR) - } else { - clay_body_text("Use Export button or Ctrl+E", color = CLAY_TEXT_TERTIARY) - } - - case .Community: - clay_body_text("Community features coming soon", color = CLAY_TEXT_SECONDARY) - clay_body_text("Current focus: local GUI workflows", color = CLAY_TEXT_SECONDARY) - } - - // Summary nav buttons - screen := app.controller.active_screen - if screen == .Script || screen == .Layout || screen == .Panels || screen == .Bubbles { - show_txt := "Show:Top" - sort_txt := "Sort:Asc" - if screen == .Script { - if app.summary_opts.script_show_all { show_txt = "Show:All" } - if app.summary_opts.script_desc { sort_txt = "Sort:Desc" } - } else { - if app.summary_opts.layout_show_all { show_txt = "Show:All" } - if app.summary_opts.layout_desc { sort_txt = "Sort:Desc" } - } - if clay.UI(clay.ID("SummaryBtnRow"))({layout = clay_row_layout()}) { - declare_button_small("btn_summary_show", show_txt) - declare_button_small("btn_summary_sort", sort_txt) - if screen == .Script { - declare_button_small("btn_summary_prev", "< Pg") - declare_button_small("btn_summary_next", "Pg >") - } else if screen == .Panels { - declare_button_small("btn_summary_prev", "< Pn") - declare_button_small("btn_summary_next", "Pn >") - } else if screen == .Layout { - declare_button_small("btn_summary_prev", "< Ly") - declare_button_small("btn_summary_next", "Ly >") - } else if screen == .Bubbles { - declare_button_small("btn_summary_prev", "< Pn") - declare_button_small("btn_summary_next", "Pn >") - } - } - } - } -} - // ─── Stat Chip (Clay) ────────────────────────────────────────────── declare_stat_chip :: proc(label: string, value: int) { value_text := fmt.tprintf("%d", value) if clay.UI(clay.ID("StatChip", u32(label[0])))({ - layout = {sizing = {width = clay.SizingFixed(80), height = clay.SizingFixed(28)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {x = .Center, y = .Center}}, + layout = {sizing = {width = clay.SizingFixed(90), height = clay.SizingFixed(34)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {x = .Center, y = .Center}}, backgroundColor = clay.Color{40, 40, 55, 255}, cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM), border = {color = clay.Color{55, 55, 75, 255}, width = clay.BorderOutside(1)}, @@ -1256,427 +816,3 @@ declare_stat_chip :: proc(label: string, value: int) { } } -// ─── 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) ──────────────────────────────────── -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..= 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.. 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.. 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.. 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(22)}, 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(22)}, 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) ───────────────────────────────────────────── -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", "Default") - declare_button_small("btn_log_report", "Report") - declare_button_small("btn_log_copy", "LogCopy") - declare_button_small("btn_log_diag", "DiagFile") - declare_button_small("btn_log_status_copy", "Copy") - declare_button_small("btn_log_clear", "Clear") - declare_button_small("btn_log_diag_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) - } - } - } -} \ No newline at end of file diff --git a/odin/src/gui/workspaces.odin b/odin/src/gui/workspaces.odin new file mode 100644 index 0000000..6d18f37 --- /dev/null +++ b/odin/src/gui/workspaces.odin @@ -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.. 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 ───────────────────────────────────────────────────