current state
This commit is contained in:
1 parent
42b58b02bf
commit
152ef17610
115 files changed
+2922
-704
No files matched your search
+146
-10
@@ -27,15 +27,73 @@ action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: i
|
||||
}
|
||||
core.dispose_script(&controller.state.script)
|
||||
controller.state.script = script
|
||||
controller.state.characters = controller.state.script.characters
|
||||
|
||||
// Auto-parse character descriptions into structured prompt templates
|
||||
parsed_chars := core.update_all_characters_from_descriptions(script.characters)
|
||||
controller.state.characters = parsed_chars
|
||||
|
||||
controller.active_screen = .Script
|
||||
controller.state.workflow.current_step = .Script_Review
|
||||
return "Generated DeepSeek script"
|
||||
return fmt.aprintf("Generated DeepSeek script with %d characters, %d pages", len(parsed_chars), len(script.pages))
|
||||
}
|
||||
|
||||
action_parse_character_descriptions :: proc(controller: ^ui.App_Controller) -> string {
|
||||
if len(controller.state.characters) == 0 {
|
||||
return "No characters to parse"
|
||||
}
|
||||
|
||||
updated := core.update_all_characters_from_descriptions(controller.state.characters)
|
||||
// Dispose old characters array and replace
|
||||
delete(controller.state.characters)
|
||||
controller.state.characters = updated
|
||||
|
||||
return fmt.aprintf("Parsed %d character descriptions", len(updated))
|
||||
}
|
||||
|
||||
action_regenerate_panel :: proc(controller: ^ui.App_Controller, panel_id: string) -> string {
|
||||
_ = controller; _ = panel_id
|
||||
return "Single panel regen not supported; regenerate all panels via FAL"
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
return "FAL_API_KEY is missing"
|
||||
}
|
||||
|
||||
target_panel: core.Panel
|
||||
found := false
|
||||
for page in controller.state.script.pages {
|
||||
for p in page.panels {
|
||||
if p.panel_id == panel_id {
|
||||
target_panel = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found { break }
|
||||
}
|
||||
if !found { return "Panel not found" }
|
||||
|
||||
img, err := adapters.generate_panel_image_stub(cfg, target_panel, controller.state.characters, controller.state.art_style, "gui-project", controller.state.story_genre, controller.state.target_audience)
|
||||
if !shared.is_ok(err) {
|
||||
return fmt.aprintf("FAL fail: %s", err.message)
|
||||
}
|
||||
|
||||
if controller.state.panel_images == nil {
|
||||
controller.state.panel_images = make(map[string]core.Panel_Image)
|
||||
}
|
||||
// Clone URL to persistent pool to survive frame resets
|
||||
img.url = pool_clone(img.url)
|
||||
controller.state.panel_images[panel_id] = img
|
||||
return fmt.aprintf("Generated panel %s", panel_id)
|
||||
}
|
||||
|
||||
action_update_panel_prompt :: proc(controller: ^ui.App_Controller, panel_id: string, new_desc: string) -> string {
|
||||
for i in 0..<len(controller.state.script.pages) {
|
||||
for j in 0..<len(controller.state.script.pages[i].panels) {
|
||||
if controller.state.script.pages[i].panels[j].panel_id == panel_id {
|
||||
controller.state.script.pages[i].panels[j].description = new_desc
|
||||
return "Panel description updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Panel not found"
|
||||
}
|
||||
|
||||
action_layout_auto :: proc(controller: ^ui.App_Controller) -> string {
|
||||
@@ -247,11 +305,21 @@ save_project_session_with_message :: proc(project_path: ^string, state: core.Com
|
||||
}
|
||||
|
||||
action_export :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format) -> string {
|
||||
controller.state.workflow.is_generating = true
|
||||
controller.state.workflow.generation_progress = 10
|
||||
|
||||
opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
|
||||
controller.state.workflow.generation_progress = 40
|
||||
|
||||
err := adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
|
||||
controller.state.workflow.generation_progress = 90
|
||||
|
||||
if !shared.is_ok(err) {
|
||||
controller.state.workflow.is_generating = false
|
||||
return err.message
|
||||
}
|
||||
controller.state.workflow.generation_progress = 100
|
||||
controller.state.workflow.is_generating = false
|
||||
controller.active_screen = .Export
|
||||
controller.state.workflow.current_step = .Complete
|
||||
controller.state.export_format = export_format
|
||||
@@ -306,9 +374,16 @@ run_panels_action :: proc(controller: ^ui.App_Controller, queue: ^adapters.Fal_G
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
return "FAL_API_KEY not set"
|
||||
}
|
||||
// Create default queue if none provided
|
||||
default_q: adapters.Fal_Generation_Queue
|
||||
q_ptr := queue
|
||||
if q_ptr == nil {
|
||||
default_q = adapters.new_fal_queue(2)
|
||||
q_ptr = &default_q
|
||||
}
|
||||
panels := collect_script_panels(controller.state.script)
|
||||
client := adapters.new_fal_client(queue)
|
||||
images, err := adapters.generate_all_panels_batched(client, cfg, panels, controller.state.characters, "digital art, comic style", "gui-project", nil)
|
||||
client := adapters.new_fal_client(q_ptr)
|
||||
images, err := adapters.generate_all_panels_batched(client, cfg, panels, controller.state.characters, controller.state.art_style, "gui-project", controller.state.story_genre, controller.state.target_audience, nil)
|
||||
if strings.contains(err.message, "error") || strings.contains(err.message, "fail") {
|
||||
return fmt.tprintf("FAL panels failed: %s", err.message)
|
||||
}
|
||||
@@ -318,8 +393,19 @@ run_panels_action :: proc(controller: ^ui.App_Controller, queue: ^adapters.Fal_G
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = images
|
||||
return fmt.tprintf("Generated %d panels via FAL", len(images))
|
||||
// Clone URLs to persistent pool before storing
|
||||
cloned: map[string]core.Panel_Image
|
||||
for pid, img in images {
|
||||
cloned[pid] = core.Panel_Image{
|
||||
url = pool_clone(img.url),
|
||||
width = img.width,
|
||||
height = img.height,
|
||||
seed = img.seed,
|
||||
prompt = pool_clone(img.prompt),
|
||||
}
|
||||
}
|
||||
controller.state.panel_images = cloned
|
||||
return fmt.tprintf("Generated %d panels via FAL", len(cloned))
|
||||
}
|
||||
|
||||
run_layout_action :: proc(controller: ^ui.App_Controller, can_layout: bool, is_dirty: ^bool) -> string {
|
||||
@@ -547,7 +633,7 @@ action_generate_character_reference :: proc(controller: ^ui.App_Controller, char
|
||||
return "Character not found"
|
||||
}
|
||||
|
||||
url, err := adapters.generate_character_reference_stub(cfg, target_char, controller.state.art_style)
|
||||
url, err := adapters.generate_character_reference_stub(cfg, target_char, controller.state.art_style, controller.state.story_genre, controller.state.target_audience)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
@@ -587,7 +673,7 @@ action_generate_character_sheet :: proc(controller: ^ui.App_Controller, characte
|
||||
return "Character not found"
|
||||
}
|
||||
|
||||
sheet_urls, err := adapters.generate_character_sheet_stub(cfg, target_char, controller.state.art_style)
|
||||
sheet_urls, err := adapters.generate_character_sheet_stub(cfg, target_char, controller.state.art_style, controller.state.story_genre, controller.state.target_audience)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
@@ -612,6 +698,56 @@ action_generate_character_sheet :: proc(controller: ^ui.App_Controller, characte
|
||||
return fmt.tprintf("Generated %d-pose sheet for %s", len(sheet_urls), target_char.name)
|
||||
}
|
||||
|
||||
action_generate_all_character_refs :: proc(controller: ^ui.App_Controller) -> string {
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
return "FAL_API_KEY is missing"
|
||||
}
|
||||
if len(controller.state.characters) == 0 {
|
||||
return "No characters to generate"
|
||||
}
|
||||
|
||||
count := 0
|
||||
for c in controller.state.characters {
|
||||
if len(c.reference_image_url) > 0 { continue }
|
||||
url, err := adapters.generate_character_reference_stub(cfg, c, controller.state.art_style, controller.state.story_genre, controller.state.target_audience)
|
||||
if shared.is_ok(err) {
|
||||
new_chars: [dynamic]core.Character
|
||||
for ch in controller.state.characters {
|
||||
nc := ch
|
||||
if ch.id == c.id { nc.reference_image_url = url }
|
||||
append(&new_chars, nc)
|
||||
}
|
||||
delete(controller.state.characters)
|
||||
controller.state.characters = new_chars[:]
|
||||
count += 1
|
||||
} else {
|
||||
return fmt.aprintf("Failed on %s: %s", c.name, err.message)
|
||||
}
|
||||
}
|
||||
return fmt.aprintf("Generated %d character references", count)
|
||||
}
|
||||
|
||||
action_update_character_desc :: proc(controller: ^ui.App_Controller, character_id: string, new_desc: string) -> string {
|
||||
new_chars: [dynamic]core.Character
|
||||
found := false
|
||||
for c in controller.state.characters {
|
||||
new_c := c
|
||||
if c.id == character_id {
|
||||
// Free old description? We don't have deep free logic everywhere, but we can assign the new one.
|
||||
new_c.description = new_desc // Assume new_desc is cloned elsewhere if needed, or just static string inside GUI buffer
|
||||
found = true
|
||||
}
|
||||
append(&new_chars, new_c)
|
||||
}
|
||||
if !found {
|
||||
return "Character not found"
|
||||
}
|
||||
delete(controller.state.characters)
|
||||
controller.state.characters = new_chars[:]
|
||||
return "Character description updated"
|
||||
}
|
||||
|
||||
action_update_bubble_text :: proc(controller: ^ui.App_Controller, bubble_id: string, new_text: string) -> string {
|
||||
if controller.state.speech_bubbles == nil {
|
||||
return "No bubbles to update"
|
||||
|
||||
+173
-107
@@ -6,31 +6,29 @@ import "../core"
|
||||
import "../shared"
|
||||
import "../ui"
|
||||
|
||||
// ─── Sidebar Declaration ─────────────────────────────────────────
|
||||
declare_sidebar :: proc(app: ^GUI_App_State) {
|
||||
declare_sidebar :: proc(app: ^GUI_App_State, bp: shared.Breakpoint) {
|
||||
collapsed := app.sidebar_collapsed || bp == .Compact
|
||||
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"}
|
||||
icons := []string{"S", "Sc", "Ch", "Pn", "Ly", "Bb", "Ex", "Cm"}
|
||||
names := []string{"Story", "Script", "Characters", "Panels", "Layout", "Bubbles", "Export", "Community"}
|
||||
|
||||
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")
|
||||
if collapsed {
|
||||
if clay.UI(clay.ID("SidebarLogoC"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, childAlignment = {x = .Center, y = .Center}},
|
||||
}) {
|
||||
clay_title_text("CO", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
|
||||
// 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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(3)}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_PROGRESS_TRACK,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {
|
||||
if progress > 0 {
|
||||
pct := f32(progress * 100); if pct > 100 { pct = 100 }
|
||||
pct := f32(progress); if pct > 1 { pct = 1 }
|
||||
if clay.UI(clay.ID("SidebarPgFill"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(pct), height = clay.SizingGrow({})}},
|
||||
backgroundColor = CLAY_PROGRESS_FILL,
|
||||
@@ -38,64 +36,133 @@ declare_sidebar :: proc(app: ^GUI_App_State) {
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
clay_muted_text(fmt.tprintf("%d/4 done", ready))
|
||||
|
||||
// Navigation
|
||||
declare_divider("SidebarDividerTop")
|
||||
|
||||
for i in 0 ..< len(screens) {
|
||||
is_active := app.controller.active_screen == screens[i]
|
||||
bg := CLAY_NAV_HOVER_BG
|
||||
is_hov := clay.Hovered()
|
||||
bg := clay.Color{0, 0, 0, 0}
|
||||
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 }
|
||||
else if is_hov { bg = CLAY_NAV_HOVER_BG }
|
||||
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, padding = {top = 4, right = 4, bottom = 4, left = 4}, childAlignment = {x = .Center, y = .Center}, layoutDirection = .LeftToRight},
|
||||
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)
|
||||
text_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if is_active { text_color = CLAY_ACCENT }
|
||||
else if is_hov { text_color = CLAY_TEXT_PRIMARY }
|
||||
clay_body_text(icons[i], color = text_color, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
gap_spacer := clay.UI(clay.ID("SidebarGap"))
|
||||
if gap_spacer({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
||||
}) {}
|
||||
declare_button_small("btn_help", "?")
|
||||
return
|
||||
}
|
||||
|
||||
// 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", "?")
|
||||
if clay.UI(clay.ID("SidebarLogo"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(40)}, childAlignment = {y = .Center}, layoutDirection = .LeftToRight, childGap = 8},
|
||||
}) {
|
||||
clay_title_text("comic-odin", color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_MD)
|
||||
}
|
||||
|
||||
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(3)}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_PROGRESS_TRACK,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {
|
||||
if progress > 0 {
|
||||
pct := f32(progress); if pct > 1 { pct = 1 }
|
||||
if clay.UI(clay.ID("SidebarPgFill"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(pct), height = clay.SizingGrow({})}},
|
||||
backgroundColor = CLAY_PROGRESS_FILL,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("SidebarProgressLabel"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(20)}, childAlignment = {y = .Center}, layoutDirection = .LeftToRight, childGap = 4},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("%d of 4 done", ready), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
|
||||
declare_divider("SidebarDivider1")
|
||||
|
||||
if clay.UI(clay.ID("SidebarNavSection"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
clay_label_text("WORKSPACE")
|
||||
for i in 0 ..< len(screens) {
|
||||
is_active := app.controller.active_screen == screens[i]
|
||||
is_hov := clay.Hovered()
|
||||
bg := clay.Color{0, 0, 0, 0}
|
||||
if is_active { bg = CLAY_NAV_ACTIVE_BG }
|
||||
else if is_hov { bg = CLAY_NAV_HOVER_BG }
|
||||
|
||||
if clay.UI(clay.ID("Nav", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, padding = {top = 4, right = 10, bottom = 4, left = 10}, childAlignment = {y = .Center}, layoutDirection = .LeftToRight, childGap = 8},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
if clay.UI(clay.ID(fmt.tprintf("NavIcon%d", i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(24), height = clay.SizingFixed(24)}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = CLAY_ACCENT_SURFACE if is_active else clay.Color{0, 0, 0, 0},
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XS),
|
||||
}) {
|
||||
icon_color := CLAY_TEXT_TERTIARY
|
||||
if is_active { icon_color = CLAY_ACCENT }
|
||||
clay.Text(icons[i], {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = icon_color})
|
||||
}
|
||||
text_color: clay.Color = CLAY_TEXT_SECONDARY
|
||||
if is_active { text_color = CLAY_TEXT_BRIGHT }
|
||||
else if is_hov { text_color = CLAY_TEXT_PRIMARY }
|
||||
clay_body_text(names[i], color = text_color, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gap_spacer := clay.UI(clay.ID("SidebarGap"))
|
||||
if gap_spacer({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
||||
}) {}
|
||||
|
||||
declare_button_small("btn_toggle_sidebar", "Collapse")
|
||||
clay_body_text("Ctrl+B", color = CLAY_TEXT_DISABLED, size = CLAY_FONT_SIZE_XS)
|
||||
|
||||
declare_divider("SidebarDivider2")
|
||||
|
||||
if clay.UI(clay.ID("SidebarFooter"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 6},
|
||||
}) {
|
||||
proj_name := app.project_path
|
||||
if len(proj_name) > 18 { proj_name = fmt.tprintf("...%s", proj_name[len(proj_name)-15:]) }
|
||||
clay_body_text(proj_name, color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
declare_button_small("btn_help", "?")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pipeline Bar ─────────────────────────────────────────────────
|
||||
|
||||
// ─── Pipeline Bar ─────────────────────────────────────────────────
|
||||
declare_pipeline_bar :: proc(app: ^GUI_App_State) {
|
||||
declare_pipeline_bar :: proc(app: ^GUI_App_State, bp: shared.Breakpoint) {
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 50, max = 56})}, padding = {top = 8, right = 20, bottom = 8, left = 20}, childGap = 16, childAlignment = {y = .Center}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_BG_TOPBAR,
|
||||
border = {color = CLAY_BORDER_DIVIDER, width = {0, 0, 0, 1, 0}},
|
||||
border = {color = CLAY_BORDER_DIVIDER, width = {0, 0, 1, 0, 0}},
|
||||
}) {
|
||||
// Screen name
|
||||
if clay.UI(clay.ID("PipelineTitle"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(110)}},
|
||||
layout = {sizing = {width = clay.SizingFixed(120)}},
|
||||
}) {
|
||||
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
|
||||
@@ -107,54 +174,61 @@ declare_pipeline_bar :: proc(app: ^GUI_App_State) {
|
||||
{"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,
|
||||
}) {}
|
||||
if clay.UI(clay.ID("PipelineSteps"))({
|
||||
layout = {layoutDirection = .LeftToRight, childGap = 0, childAlignment = {y = .Center}, sizing = {width = clay.SizingFit({}), height = clay.SizingFit({})}},
|
||||
}) {
|
||||
for i in 0 ..< len(steps) {
|
||||
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)
|
||||
is_hov := clay.Hovered()
|
||||
bg := clay.Color{0, 0, 0, 0}
|
||||
if is_hov { bg = CLAY_NAV_HOVER_BG }
|
||||
|
||||
if clay.UI(clay.ID("PStep", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(72), height = clay.SizingFixed(32)}, layoutDirection = .TopToBottom, childAlignment = {x = .Center}, childGap = 2},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
dot_color: clay.Color = CLAY_TEXT_DISABLED
|
||||
if steps[i].done { dot_color = CLAY_SUCCESS }
|
||||
if is_current && !steps[i].done { dot_color = CLAY_ACCENT }
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("PStepDot%d", i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(8), height = clay.SizingFixed(8)}},
|
||||
backgroundColor = dot_color,
|
||||
cornerRadius = clay.CornerRadiusAll(4),
|
||||
}) {}
|
||||
|
||||
clay.Text(steps[i].name, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = dot_color})
|
||||
}
|
||||
if i < len(steps) - 1 {
|
||||
line_color: clay.Color = CLAY_BORDER_SUBTLE
|
||||
if steps[i].done { line_color = CLAY_SUCCESS }
|
||||
if clay.UI(clay.ID("PLine", u32(i)))({
|
||||
layout = {sizing = {width = clay.SizingFixed(16), height = clay.SizingFixed(2)}},
|
||||
backgroundColor = line_color,
|
||||
cornerRadius = clay.CornerRadiusAll(1),
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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},
|
||||
layout = {layoutDirection = .LeftToRight, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
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("PBarMinibar"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(64), height = clay.SizingFixed(4)}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_PROGRESS_TRACK,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {
|
||||
pct := f32(progress); if pct > 1 { pct = 1 }
|
||||
if pct > 0 {
|
||||
if clay.UI(clay.ID("PBarMinibarFill"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(pct), height = clay.SizingGrow({})}},
|
||||
backgroundColor = CLAY_PROGRESS_FILL,
|
||||
@@ -162,28 +236,30 @@ declare_pipeline_bar :: proc(app: ^GUI_App_State) {
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
clay_body_text(fmt.tprintf("%d/%d", ready, total), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
|
||||
// Status message (truncated)
|
||||
clay_body_text(fmt.tprintf("C:%d P:%d", len(app.controller.state.characters), len(app.controller.state.panel_images)), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
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)
|
||||
is_casual := app.controller.state.user_mode == .Casual
|
||||
if clay.UI(clay.ID("ModeToggleRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, childGap = 2, padding = {top = 2, right = 2, bottom = 2, left = 2}},
|
||||
backgroundColor = CLAY_BG_STRIP,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
declare_nav_chip("btn_mode_casual", "Casual", is_casual)
|
||||
declare_nav_chip("btn_mode_pro", "Pro", !is_casual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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) {
|
||||
declare_workspace :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek: bool, pages_count: int, bp: shared.Breakpoint, main_w: i32) {
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = CLAY_SPACE_12, right = CLAY_SPACE_20, bottom = CLAY_SPACE_12, left = CLAY_SPACE_20}, childGap = CLAY_SPACE_12},
|
||||
}) {
|
||||
switch screen {
|
||||
case .Story:
|
||||
declare_story_workspace(app)
|
||||
declare_story_workspace(app, bp)
|
||||
declare_action_log(app)
|
||||
case .Script:
|
||||
declare_script_workspace(app)
|
||||
@@ -206,33 +282,25 @@ declare_workspace :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_e
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Script Workspace ────────────────────────────────────────────
|
||||
|
||||
// ─── Bottom Bar ───────────────────────────────────────────────────
|
||||
declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, next_hint: string, has_fal_key: bool) {
|
||||
declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, next_hint: string, has_fal_key: bool, bp: shared.Breakpoint) {
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 48, max = 52})}, padding = {top = 6, right = 16, bottom = 6, left = 16}, childGap = 6, 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")
|
||||
declare_button_default("btn_new", "New")
|
||||
declare_button_default("btn_open", "Open")
|
||||
declare_button_default("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
|
||||
if has_fal_key {
|
||||
declare_nav_chip("btn_fal_panels", "FAL", app.use_fal_panels)
|
||||
}
|
||||
@@ -241,9 +309,7 @@ declare_bottom_bar :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_
|
||||
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"))
|
||||
declare_button_ghost("btn_autosave_toggle",
|
||||
fmt.tprintf("%s", "ON" if app.autosave_enabled else "OFF"))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Click Processing ──────────────────────────────────────────────
|
||||
@@ -4,15 +4,14 @@ import clay "clay:."
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// --- Font IDs (indices into raylib_fonts) ---
|
||||
CLAY_FONT_BODY :: u16(0)
|
||||
CLAY_FONT_TITLE :: u16(1)
|
||||
CLAY_FONT_MONO :: u16(2)
|
||||
|
||||
// --- Font registry (shared between layout and renderer) ---
|
||||
Raylib_Font :: struct {
|
||||
fontId: u16,
|
||||
font: rl.Font,
|
||||
@@ -21,19 +20,16 @@ Raylib_Font :: struct {
|
||||
raylib_fonts: [dynamic]Raylib_Font
|
||||
|
||||
|
||||
// --- Color conversion ---
|
||||
clay_color_to_rl :: proc(color: clay.Color) -> rl.Color {
|
||||
return {u8(color[0]), u8(color[1]), u8(color[2]), u8(color[3])}
|
||||
}
|
||||
|
||||
// --- Clay Error Handler ---
|
||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||||
context = runtime.default_context()
|
||||
if errorData.errorType == .DuplicateId || errorData.errorType == .PercentageOver1 { return }
|
||||
fmt.eprintf("CLAY ERROR: %v\n", errorData)
|
||||
}
|
||||
|
||||
// --- Clay State ---
|
||||
Clay_State :: struct {
|
||||
arena_memory: [dynamic]u8,
|
||||
font_default: rl.Font,
|
||||
@@ -52,7 +48,26 @@ load_font_or_default :: proc(path: cstring) -> rl.Font {
|
||||
return rl.GetFontDefault()
|
||||
}
|
||||
|
||||
// --- Init / Shutdown ---
|
||||
font_path_sans :: proc() -> cstring {
|
||||
if os.exists("assets/fonts/AdwaitaSans-Regular.ttf") {
|
||||
return "assets/fonts/AdwaitaSans-Regular.ttf"
|
||||
}
|
||||
if os.exists("/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf") {
|
||||
return "/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
font_path_mono :: proc() -> cstring {
|
||||
if os.exists("assets/fonts/AdwaitaMono-Regular.ttf") {
|
||||
return "assets/fonts/AdwaitaMono-Regular.ttf"
|
||||
}
|
||||
if os.exists("/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf") {
|
||||
return "/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
clay_init :: proc(screen_w: i32, screen_h: i32) {
|
||||
min_memory_size := clay.MinMemorySize()
|
||||
clay_state.arena_memory = make([dynamic]u8, min_memory_size)
|
||||
@@ -61,9 +76,11 @@ 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 = 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")
|
||||
sans_path := font_path_sans()
|
||||
mono_path := font_path_mono()
|
||||
clay_state.font_default = load_font_or_default(sans_path)
|
||||
clay_state.font_title = load_font_or_default(sans_path)
|
||||
clay_state.font_mono = load_font_or_default(mono_path)
|
||||
|
||||
raylib_fonts = make([dynamic]Raylib_Font, 0, 3)
|
||||
append(&raylib_fonts, Raylib_Font{fontId = CLAY_FONT_BODY, font = clay_state.font_default})
|
||||
@@ -94,7 +111,6 @@ clay_update_input :: proc() {
|
||||
clay.UpdateScrollContainers(true, {f32(scroll_delta) * 40, 0}, rl.GetFrameTime())
|
||||
}
|
||||
|
||||
// --- Text Measurement ---
|
||||
clay_measure_text :: proc "c" (text: clay.StringSlice, config: ^clay.TextElementConfig, userData: rawptr) -> clay.Dimensions {
|
||||
context = runtime.default_context()
|
||||
|
||||
@@ -129,7 +145,22 @@ clay_measure_text :: proc "c" (text: clay.StringSlice, config: ^clay.TextElement
|
||||
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
draw_shadow_rect :: proc(x, y, w, h: f32, radius: f32, shadow: rl.Color) {
|
||||
if shadow.a == 0 { return }
|
||||
offset := f32(4)
|
||||
blur := f32(12)
|
||||
spread := f32(2)
|
||||
sx := x - spread + offset
|
||||
sy := y - spread + offset
|
||||
sw := w + spread * 2 + blur
|
||||
sh := h + spread * 2 + blur
|
||||
if radius > 0 {
|
||||
rl.DrawRectangleRounded({sx, sy, sw, sh}, radius / min(sw, sh), 8, shadow)
|
||||
} else {
|
||||
rl.DrawRectangle(i32(sx), i32(sy), i32(sw), i32(sh), shadow)
|
||||
}
|
||||
}
|
||||
|
||||
clay_raylib_render :: proc(render_commands: ^clay.ClayArray(clay.RenderCommand)) {
|
||||
for i in 0 ..< render_commands.length {
|
||||
render_command := clay.RenderCommandArray_Get(render_commands, i)
|
||||
@@ -155,7 +186,16 @@ clay_raylib_render :: proc(render_commands: ^clay.ClayArray(clay.RenderCommand))
|
||||
case .Image:
|
||||
config := render_command.renderData.image
|
||||
texture := (^rl.Texture2D)(config.imageData)
|
||||
rl.DrawTextureEx(texture^, {bounds.x, bounds.y}, 0, bounds.width / f32(texture.width), rl.WHITE)
|
||||
if texture.width > 0 && texture.height > 0 {
|
||||
scale := min(bounds.width / f32(texture.width), bounds.height / f32(texture.height))
|
||||
draw_w := f32(texture.width) * scale
|
||||
draw_h := f32(texture.height) * scale
|
||||
offset_x := bounds.x + (bounds.width - draw_w) * 0.5
|
||||
offset_y := bounds.y + (bounds.height - draw_h) * 0.5
|
||||
if scale > 0.001 && draw_w > 1 && draw_h > 1 {
|
||||
rl.DrawTextureEx(texture^, {offset_x, offset_y}, 0, scale, rl.WHITE)
|
||||
}
|
||||
}
|
||||
case .ScissorStart:
|
||||
rl.BeginScissorMode(
|
||||
i32(math.round(bounds.x)),
|
||||
@@ -268,8 +308,6 @@ clay_raylib_render :: proc(render_commands: ^clay.ClayArray(clay.RenderCommand))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Layout helpers (procs because Clay helpers require runtime evaluation) ---
|
||||
|
||||
clay_card_layout :: proc() -> clay.LayoutConfig {
|
||||
return clay.LayoutConfig {
|
||||
layoutDirection = .TopToBottom,
|
||||
@@ -282,6 +320,18 @@ clay_card_layout :: proc() -> clay.LayoutConfig {
|
||||
}
|
||||
}
|
||||
|
||||
clay_section_layout :: proc() -> clay.LayoutConfig {
|
||||
return clay.LayoutConfig {
|
||||
layoutDirection = .TopToBottom,
|
||||
sizing = {
|
||||
width = clay.SizingGrow({}),
|
||||
height = clay.SizingFit({}),
|
||||
},
|
||||
padding = {top = CLAY_SPACE_16, right = CLAY_SPACE_16, bottom = CLAY_SPACE_16, left = CLAY_SPACE_16},
|
||||
childGap = CLAY_SPACE_10,
|
||||
}
|
||||
}
|
||||
|
||||
clay_row_layout :: proc() -> clay.LayoutConfig {
|
||||
return clay.LayoutConfig {
|
||||
layoutDirection = .LeftToRight,
|
||||
@@ -298,7 +348,7 @@ clay_input_layout :: proc() -> clay.LayoutConfig {
|
||||
layoutDirection = .LeftToRight,
|
||||
sizing = {
|
||||
width = clay.SizingGrow({}),
|
||||
height = clay.SizingFixed(40),
|
||||
height = clay.SizingFixed(f32(CLAY_HEIGHT_MD)),
|
||||
},
|
||||
padding = {top = 8, right = 12, bottom = 8, left = 12},
|
||||
}
|
||||
@@ -309,25 +359,39 @@ clay_button_layout :: proc() -> clay.LayoutConfig {
|
||||
layoutDirection = .LeftToRight,
|
||||
sizing = {
|
||||
width = clay.SizingFit({}),
|
||||
height = clay.SizingFixed(38),
|
||||
height = clay.SizingFixed(f32(CLAY_HEIGHT_MD)),
|
||||
},
|
||||
padding = {top = 8, right = 16, bottom = 8, left = 16},
|
||||
padding = {top = 6, right = 16, bottom = 6, left = 16},
|
||||
childAlignment = {x = .Center, y = .Center},
|
||||
}
|
||||
}
|
||||
|
||||
// --- Style configs ---
|
||||
|
||||
clay_card_style :: proc() -> clay.ElementDeclaration {
|
||||
return {
|
||||
layout = clay_card_layout(),
|
||||
backgroundColor = CLAY_BG_CARD,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(1)},
|
||||
}
|
||||
}
|
||||
|
||||
// --- Text helpers ---
|
||||
clay_elevated_card_style :: proc() -> clay.ElementDeclaration {
|
||||
return {
|
||||
layout = clay_card_layout(),
|
||||
backgroundColor = CLAY_BG_CARD,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(1)},
|
||||
}
|
||||
}
|
||||
|
||||
clay_section_style :: proc() -> clay.ElementDeclaration {
|
||||
return {
|
||||
layout = clay_section_layout(),
|
||||
backgroundColor = CLAY_BG_CARD,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}
|
||||
}
|
||||
|
||||
clay_body_text :: proc(text: string, color: clay.Color = CLAY_TEXT_PRIMARY, size: u16 = CLAY_FONT_SIZE_MD) {
|
||||
clay.Text(text, {fontId = CLAY_FONT_BODY, fontSize = size, textColor = color})
|
||||
@@ -339,4 +403,8 @@ clay_title_text :: proc(text: string, color: clay.Color = CLAY_TEXT_BRIGHT, size
|
||||
|
||||
clay_muted_text :: proc(text: string) {
|
||||
clay.Text(text, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = CLAY_TEXT_TERTIARY})
|
||||
}
|
||||
}
|
||||
|
||||
clay_label_text :: proc(text: string) {
|
||||
clay.Text(text, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = CLAY_TEXT_TERTIARY, letterSpacing = 1})
|
||||
}
|
||||
+210
-55
@@ -1,65 +1,85 @@
|
||||
package gui
|
||||
|
||||
import clay "clay:."
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// --- Color Palette ---
|
||||
// --- Clay Color Palette (modernized dark theme) ---
|
||||
// Named with CLAY_ prefix to avoid collision with existing theme.odin
|
||||
CLAY_BG_BASE :: clay.Color{10, 10, 15, 255}
|
||||
RL_BG_BASE :: rl.Color{10, 10, 15, 255}
|
||||
|
||||
CLAY_BG_SIDEBAR :: clay.Color{14, 14, 22, 255}
|
||||
RL_BG_SIDEBAR :: rl.Color{14, 14, 22, 255}
|
||||
|
||||
CLAY_BG_TOPBAR :: clay.Color{16, 16, 24, 255}
|
||||
|
||||
CLAY_BG_CARD :: clay.Color{22, 22, 34, 255}
|
||||
|
||||
CLAY_BG_CARD_ALT :: clay.Color{18, 18, 30, 255}
|
||||
|
||||
CLAY_BG_STRIP :: clay.Color{28, 28, 42, 255}
|
||||
|
||||
CLAY_BG_BASE :: clay.Color{13, 13, 18, 255}
|
||||
CLAY_BG_SIDEBAR :: clay.Color{18, 18, 26, 255}
|
||||
CLAY_BG_TOPBAR :: clay.Color{22, 22, 32, 255}
|
||||
CLAY_BG_CARD :: clay.Color{28, 28, 40, 255}
|
||||
CLAY_BG_CARD_ALT :: clay.Color{24, 24, 36, 255}
|
||||
CLAY_BG_STRIP :: clay.Color{32, 32, 46, 255}
|
||||
CLAY_BG_OVERLAY :: clay.Color{0, 0, 0, 180}
|
||||
CLAY_BG_INPUT :: clay.Color{20, 20, 30, 255}
|
||||
|
||||
CLAY_BORDER_CARD :: clay.Color{50, 50, 70, 255}
|
||||
CLAY_BORDER_SUBTLE :: clay.Color{40, 40, 58, 255}
|
||||
CLAY_BORDER_DIVIDER :: clay.Color{36, 36, 52, 255}
|
||||
CLAY_BG_INPUT :: clay.Color{16, 16, 26, 255}
|
||||
|
||||
CLAY_BG_HOVER :: clay.Color{30, 30, 46, 255}
|
||||
|
||||
CLAY_BG_SELECTED :: clay.Color{34, 34, 52, 255}
|
||||
|
||||
CLAY_BORDER_CARD :: clay.Color{38, 38, 56, 255}
|
||||
CLAY_BORDER_SUBTLE :: clay.Color{28, 28, 42, 255}
|
||||
CLAY_BORDER_DIVIDER :: clay.Color{24, 24, 38, 255}
|
||||
CLAY_BORDER_FOCUS :: clay.Color{99, 102, 241, 255}
|
||||
|
||||
CLAY_ACCENT :: clay.Color{99, 102, 241, 255}
|
||||
CLAY_ACCENT_HOVER :: clay.Color{124, 127, 255, 255}
|
||||
CLAY_ACCENT_MUTED :: clay.Color{79, 70, 229, 200}
|
||||
CLAY_ACCENT_SURFACE:: clay.Color{67, 56, 202, 40}
|
||||
CLAY_ACCENT_GLOW :: clay.Color{99, 102, 241, 60}
|
||||
CLAY_ACCENT_HOVER :: clay.Color{129, 132, 255, 255}
|
||||
CLAY_ACCENT_MUTED :: clay.Color{79, 70, 229, 200}
|
||||
CLAY_ACCENT_SURFACE:: clay.Color{67, 56, 202, 35}
|
||||
CLAY_ACCENT_GLOW :: clay.Color{99, 102, 241, 50}
|
||||
|
||||
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_PRIMARY :: clay.Color{240, 240, 248, 255}
|
||||
CLAY_TEXT_SECONDARY :: clay.Color{175, 175, 200, 255}
|
||||
CLAY_TEXT_TERTIARY :: clay.Color{120, 120, 150, 255}
|
||||
CLAY_TEXT_DISABLED :: clay.Color{75, 75, 100, 255}
|
||||
CLAY_TEXT_BRIGHT := clay.Color{255, 255, 255, 255}
|
||||
|
||||
CLAY_SUCCESS :: clay.Color{34, 197, 94, 255}
|
||||
CLAY_SUCCESS_DIM :: clay.Color{34, 197, 94, 100}
|
||||
CLAY_WARNING :: clay.Color{234, 179, 8, 255}
|
||||
CLAY_WARNING_DIM :: clay.Color{234, 179, 8, 100}
|
||||
CLAY_ERROR :: clay.Color{239, 68, 68, 255}
|
||||
CLAY_ERROR_DIM :: clay.Color{239, 68, 68, 100}
|
||||
CLAY_SUCCESS :: clay.Color{52, 211, 153, 255}
|
||||
CLAY_SUCCESS_DIM :: clay.Color{52, 211, 153, 80}
|
||||
CLAY_WARNING :: clay.Color{251, 191, 36, 255}
|
||||
CLAY_WARNING_DIM :: clay.Color{251, 191, 36, 80}
|
||||
CLAY_ERROR :: clay.Color{248, 113, 113, 255}
|
||||
CLAY_ERROR_DIM :: clay.Color{248, 113, 113, 80}
|
||||
CLAY_INFO :: clay.Color{96, 165, 250, 255}
|
||||
CLAY_INFO_DIM :: clay.Color{96, 165, 250, 80}
|
||||
|
||||
CLAY_BTN_DEFAULT :: clay.Color{42, 42, 58, 255}
|
||||
CLAY_BTN_DEFAULT_HOVER :: clay.Color{55, 55, 72, 255}
|
||||
CLAY_BTN_SOFT :: clay.Color{55, 50, 120, 255}
|
||||
CLAY_BTN_SOFT_HOVER :: clay.Color{70, 65, 140, 255}
|
||||
CLAY_BTN_DANGER :: clay.Color{185, 28, 28, 255}
|
||||
CLAY_BTN_DANGER_HOVER :: clay.Color{210, 40, 40, 255}
|
||||
CLAY_BTN_DISABLED :: clay.Color{30, 30, 42, 255}
|
||||
CLAY_BTN_DEFAULT :: clay.Color{32, 32, 48, 255}
|
||||
CLAY_BTN_DEFAULT_HOVER :: clay.Color{44, 44, 62, 255}
|
||||
CLAY_BTN_SOFT :: clay.Color{50, 46, 110, 255}
|
||||
CLAY_BTN_SOFT_HOVER :: clay.Color{66, 60, 132, 255}
|
||||
CLAY_BTN_DANGER :: clay.Color{180, 30, 30, 255}
|
||||
CLAY_BTN_DANGER_HOVER :: clay.Color{210, 45, 45, 255}
|
||||
CLAY_BTN_DISABLED :: clay.Color{24, 24, 36, 255}
|
||||
CLAY_BTN_GHOST :: clay.Color{0, 0, 0, 0}
|
||||
CLAY_BTN_GHOST_HOVER :: clay.Color{255, 255, 255, 12}
|
||||
|
||||
CLAY_NAV_ACTIVE :: clay.Color{99, 102, 241, 255}
|
||||
CLAY_NAV_ACTIVE_BG :: clay.Color{67, 56, 202, 25}
|
||||
CLAY_NAV_HOVER_BG :: clay.Color{40, 40, 58, 255}
|
||||
CLAY_NAV_ACTIVE_BG :: clay.Color{99, 102, 241, 18}
|
||||
CLAY_NAV_HOVER_BG :: clay.Color{255, 255, 255, 8}
|
||||
|
||||
CLAY_INPUT_BORDER :: clay.Color{55, 55, 75, 255}
|
||||
CLAY_INPUT_BORDER :: clay.Color{42, 42, 62, 255}
|
||||
CLAY_INPUT_FOCUS :: clay.Color{99, 102, 241, 255}
|
||||
|
||||
CLAY_PROGRESS_TRACK :: clay.Color{30, 30, 42, 255}
|
||||
CLAY_PROGRESS_FILL := clay.Color{99, 102, 241, 255}
|
||||
CLAY_PROGRESS_TRACK :: clay.Color{24, 24, 38, 255}
|
||||
CLAY_PROGRESS_FILL := clay.Color{99, 102, 241, 255}
|
||||
|
||||
// --- Spacing (4px grid) ---
|
||||
// --- Spacing (4px grid) ---
|
||||
CLAY_SHADOW_SM :: rl.Color{0, 0, 0, 30}
|
||||
CLAY_SHADOW_MD :: rl.Color{0, 0, 0, 50}
|
||||
CLAY_SHADOW_LG :: rl.Color{0, 0, 0, 70}
|
||||
|
||||
CLAY_SPACE_2 :: u16(2)
|
||||
CLAY_SPACE_4 :: u16(4)
|
||||
CLAY_SPACE_6 :: u16(6)
|
||||
CLAY_SPACE_8 :: u16(8)
|
||||
CLAY_SPACE_10 :: u16(10)
|
||||
CLAY_SPACE_12 :: u16(12)
|
||||
CLAY_SPACE_16 :: u16(16)
|
||||
CLAY_SPACE_20 :: u16(20)
|
||||
@@ -67,23 +87,158 @@ CLAY_SPACE_24 :: u16(24)
|
||||
CLAY_SPACE_32 :: u16(32)
|
||||
CLAY_SPACE_40 :: u16(40)
|
||||
CLAY_SPACE_48 :: u16(48)
|
||||
CLAY_SPACE_64 :: u16(64)
|
||||
|
||||
// --- Font Sizes ---
|
||||
// --- Font Sizes ---
|
||||
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)
|
||||
CLAY_FONT_SIZE_XS :: u16(12)
|
||||
CLAY_FONT_SIZE_SM :: u16(14)
|
||||
CLAY_FONT_SIZE_MD :: u16(16)
|
||||
CLAY_FONT_SIZE_LG :: u16(20)
|
||||
CLAY_FONT_SIZE_XL :: u16(24)
|
||||
CLAY_FONT_SIZE_2XL :: u16(30)
|
||||
CLAY_FONT_SIZE_3XL :: u16(36)
|
||||
|
||||
// --- Corner Radius ---
|
||||
// --- Corner Radius (px for Clay) ---
|
||||
CLAY_RADIUS_SM :: f32(4)
|
||||
CLAY_RADIUS_MD :: f32(8)
|
||||
CLAY_RADIUS_LG :: f32(12)
|
||||
CLAY_RADIUS_XL :: f32(16)
|
||||
CLAY_RADIUS_XS :: f32(3)
|
||||
CLAY_RADIUS_SM :: f32(6)
|
||||
CLAY_RADIUS_MD :: f32(10)
|
||||
CLAY_RADIUS_LG :: f32(14)
|
||||
CLAY_RADIUS_XL :: f32(20)
|
||||
CLAY_RADIUS_2XL :: f32(28)
|
||||
|
||||
// --- Fractional radius (for DrawRectangleRounded compat) ---
|
||||
CLAY_RADIUS_FRAC_BTN :: f32(0.32)
|
||||
CLAY_RADIUS_FRAC_PILL :: f32(0.50)
|
||||
CLAY_RADIUS_FRAC_CARD :: f32(0.14)
|
||||
|
||||
CLAY_HEIGHT_SM :: u16(28)
|
||||
CLAY_HEIGHT_MD :: u16(36)
|
||||
CLAY_HEIGHT_LG :: u16(44)
|
||||
CLAY_HEIGHT_XL :: u16(52)
|
||||
|
||||
Design_Tokens :: struct {
|
||||
surfaces: struct {
|
||||
base: clay.Color,
|
||||
sidebar: clay.Color,
|
||||
topbar: clay.Color,
|
||||
card: clay.Color,
|
||||
card_alt: clay.Color,
|
||||
strip: clay.Color,
|
||||
overlay: clay.Color,
|
||||
input: clay.Color,
|
||||
hover: clay.Color,
|
||||
selected: clay.Color,
|
||||
},
|
||||
borders: struct {
|
||||
card: clay.Color,
|
||||
subtle: clay.Color,
|
||||
divider: clay.Color,
|
||||
focus: clay.Color,
|
||||
},
|
||||
accent: struct {
|
||||
primary: clay.Color,
|
||||
hover: clay.Color,
|
||||
muted: clay.Color,
|
||||
surface: clay.Color,
|
||||
glow: clay.Color,
|
||||
},
|
||||
text: struct {
|
||||
primary: clay.Color,
|
||||
secondary: clay.Color,
|
||||
tertiary: clay.Color,
|
||||
disabled: clay.Color,
|
||||
bright: clay.Color,
|
||||
},
|
||||
semantic: struct {
|
||||
success: clay.Color,
|
||||
warning: clay.Color,
|
||||
error: clay.Color,
|
||||
info: clay.Color,
|
||||
},
|
||||
spacing: struct {
|
||||
xs: u16,
|
||||
sm: u16,
|
||||
md: u16,
|
||||
lg: u16,
|
||||
xl: u16,
|
||||
xxl: u16,
|
||||
},
|
||||
typography: struct {
|
||||
xs: u16,
|
||||
sm: u16,
|
||||
md: u16,
|
||||
lg: u16,
|
||||
xl: u16,
|
||||
xxl: u16,
|
||||
},
|
||||
radius: struct {
|
||||
xs: f32,
|
||||
sm: f32,
|
||||
md: f32,
|
||||
lg: f32,
|
||||
xl: f32,
|
||||
},
|
||||
}
|
||||
|
||||
derive_tokens :: proc() -> Design_Tokens {
|
||||
return Design_Tokens{
|
||||
surfaces = {
|
||||
base = CLAY_BG_BASE,
|
||||
sidebar = CLAY_BG_SIDEBAR,
|
||||
topbar = CLAY_BG_TOPBAR,
|
||||
card = CLAY_BG_CARD,
|
||||
card_alt = CLAY_BG_CARD_ALT,
|
||||
strip = CLAY_BG_STRIP,
|
||||
overlay = CLAY_BG_OVERLAY,
|
||||
input = CLAY_BG_INPUT,
|
||||
hover = CLAY_BG_HOVER,
|
||||
selected = CLAY_BG_SELECTED,
|
||||
},
|
||||
borders = {
|
||||
card = CLAY_BORDER_CARD,
|
||||
subtle = CLAY_BORDER_SUBTLE,
|
||||
divider = CLAY_BORDER_DIVIDER,
|
||||
focus = CLAY_BORDER_FOCUS,
|
||||
},
|
||||
accent = {
|
||||
primary = CLAY_ACCENT,
|
||||
hover = CLAY_ACCENT_HOVER,
|
||||
muted = CLAY_ACCENT_MUTED,
|
||||
surface = CLAY_ACCENT_SURFACE,
|
||||
glow = CLAY_ACCENT_GLOW,
|
||||
},
|
||||
text = {
|
||||
primary = CLAY_TEXT_PRIMARY,
|
||||
secondary = CLAY_TEXT_SECONDARY,
|
||||
tertiary = CLAY_TEXT_TERTIARY,
|
||||
disabled = CLAY_TEXT_DISABLED,
|
||||
bright = CLAY_TEXT_BRIGHT,
|
||||
},
|
||||
semantic = {
|
||||
success = CLAY_SUCCESS,
|
||||
warning = CLAY_WARNING,
|
||||
error = CLAY_ERROR,
|
||||
info = CLAY_INFO,
|
||||
},
|
||||
spacing = {
|
||||
xs = CLAY_SPACE_4,
|
||||
sm = CLAY_SPACE_8,
|
||||
md = CLAY_SPACE_16,
|
||||
lg = CLAY_SPACE_24,
|
||||
xl = CLAY_SPACE_32,
|
||||
xxl = CLAY_SPACE_48,
|
||||
},
|
||||
typography = {
|
||||
xs = CLAY_FONT_SIZE_XS,
|
||||
sm = CLAY_FONT_SIZE_SM,
|
||||
md = CLAY_FONT_SIZE_MD,
|
||||
lg = CLAY_FONT_SIZE_LG,
|
||||
xl = CLAY_FONT_SIZE_XL,
|
||||
xxl = CLAY_FONT_SIZE_2XL,
|
||||
},
|
||||
radius = {
|
||||
xs = CLAY_RADIUS_XS,
|
||||
sm = CLAY_RADIUS_SM,
|
||||
md = CLAY_RADIUS_MD,
|
||||
lg = CLAY_RADIUS_LG,
|
||||
xl = CLAY_RADIUS_XL,
|
||||
},
|
||||
}
|
||||
}
|
||||
+291
-105
@@ -2,13 +2,13 @@ package gui
|
||||
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
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")
|
||||
declare_section_header("ScriptDetailHeader", "Script Detail")
|
||||
|
||||
page_count := len(app.controller.state.script.pages)
|
||||
if page_count == 0 {
|
||||
@@ -20,7 +20,7 @@ declare_script_detail :: proc(app: ^GUI_App_State) {
|
||||
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},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12},
|
||||
}) {
|
||||
declare_stat_chip("stat_page", "Page", idx + 1)
|
||||
declare_stat_chip("stat_panels", "Panels", len(page.panels))
|
||||
@@ -29,17 +29,22 @@ declare_script_detail :: proc(app: ^GUI_App_State) {
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 4},
|
||||
}) {
|
||||
for pn in page.panels {
|
||||
desc := pn.description
|
||||
if len(desc) == 0 { desc = "(no description)" }
|
||||
is_hov := clay.Hovered()
|
||||
bg := clay.Color{0, 0, 0, 0}
|
||||
if is_hov { bg = CLAY_BG_HOVER }
|
||||
if clay.UI(clay.ID(fmt.tprintf("ScriptDetailPanel_%d", pn.panel_number)))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 2, right = 4, bottom = 2, left = 4}},
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 6, right = 8, bottom = 6, left = 8}, childGap = 2},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
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)
|
||||
clay_body_text(fmt.tprintf("\"%s\"", pn.dialogue[0].text), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,12 +52,9 @@ declare_script_detail :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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")
|
||||
declare_section_header("PanelsDetailHeader", "Panel Results")
|
||||
|
||||
panel_count := count_script_panels(app.controller.state.script)
|
||||
if panel_count == 0 {
|
||||
@@ -83,7 +85,9 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
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)
|
||||
clay_body_text(fmt.tprintf("Panel %d/%d", idx+1, panel_count), color = status_color, size = CLAY_FONT_SIZE_SM)
|
||||
clay_muted_text(fmt.tprintf("page %d # %d", page_num, panel.panel_number))
|
||||
clay.UI(clay.ID("PanelDetailHdrSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
if has_img {
|
||||
declare_button_small("btn_panel_regenerate", "Regenerate")
|
||||
} else {
|
||||
@@ -91,9 +95,7 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
}
|
||||
|
||||
// Panel image preview + info side-by-side
|
||||
if has_img {
|
||||
// Try to load and show the image
|
||||
panel_img := app.controller.state.panel_images[panel.panel_id]
|
||||
_, loaded := load_panel_texture(&app.panel_textures, panel.panel_id, panel_img.url)
|
||||
img_shown := false
|
||||
@@ -101,12 +103,12 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
tex_ptr := &app.panel_textures[panel.panel_id]
|
||||
if tex_ptr.id != 0 {
|
||||
if clay.UI(clay.ID("PanelImageRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = CLAY_SPACE_12},
|
||||
}) {
|
||||
if clay.UI(clay.ID("PanelImagePreview"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(200), height = clay.SizingFixed(200)}},
|
||||
backgroundColor = CLAY_BG_STRIP,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
image = {imageData = rawptr(tex_ptr)},
|
||||
}) {}
|
||||
if clay.UI(clay.ID("PanelImageInfo"))({layout = {layoutDirection = .TopToBottom, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 2}}) {
|
||||
@@ -116,10 +118,10 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
img := panel_img
|
||||
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed))
|
||||
clay_muted_text(fmt.tprintf("%dx%d seed:%d", img.width, img.height, img.seed))
|
||||
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)
|
||||
clay_body_text(desc, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
img_shown = true
|
||||
@@ -132,32 +134,26 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
clay_body_text(fmt.tprintf("err: %s", err_msg), color = CLAY_ERROR, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
img := panel_img
|
||||
clay_muted_text(fmt.tprintf("img: %dx%d seed:%d (failed to load)", img.width, img.height, img.seed))
|
||||
clay_muted_text(fmt.tprintf("%dx%d seed:%d (failed to load)", img.width, img.height, img.seed))
|
||||
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)
|
||||
clay_body_text(desc, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
} else {
|
||||
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")
|
||||
clay_muted_text("not generated yet")
|
||||
}
|
||||
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))
|
||||
}
|
||||
clay_body_text(desc, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PanelListScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
visible_rows: int = 8
|
||||
if visible_rows > panel_count { visible_rows = panel_count }
|
||||
@@ -173,13 +169,11 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
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 := " "
|
||||
is_cur := i == idx
|
||||
is_hov := clay.Hovered()
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if i == idx {
|
||||
mark = "> "
|
||||
row_color = CLAY_TEXT_BRIGHT
|
||||
}
|
||||
if is_cur { row_color = CLAY_TEXT_BRIGHT }
|
||||
else if is_hov { row_color = CLAY_TEXT_PRIMARY }
|
||||
|
||||
ready := "missing"
|
||||
ready_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
@@ -192,24 +186,26 @@ declare_panels_detail :: proc(app: ^GUI_App_State) {
|
||||
ready_color = CLAY_SUCCESS
|
||||
}
|
||||
|
||||
row_bg := clay.Color{0, 0, 0, 0}
|
||||
if is_cur { row_bg = CLAY_BG_SELECTED }
|
||||
else if is_hov { row_bg = CLAY_BG_HOVER }
|
||||
|
||||
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},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(22)}, padding = {top = 2, right = 6, bottom = 2, left = 6}, childGap = 6, childAlignment = {y = .Center}},
|
||||
backgroundColor = row_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XS),
|
||||
}) {
|
||||
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})
|
||||
clay.Text(fmt.tprintf("%02d p%d#%d", i+1, row_page, row_panel.panel_number), {fontId = CLAY_FONT_MONO, fontSize = CLAY_FONT_SIZE_XS, textColor = row_color})
|
||||
clay.Text(ready, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, 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")
|
||||
declare_section_header("LayoutDetailHeader", "Layout Detail")
|
||||
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
@@ -220,10 +216,12 @@ declare_layout_detail :: proc(app: ^GUI_App_State) {
|
||||
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"))({
|
||||
if clay.UI(clay.ID("LayoutDetailInfo"))({
|
||||
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)
|
||||
clay_body_text(fmt.tprintf("Page %d/%d", idx+1, layout_count), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
clay_muted_text(fmt.tprintf("%s - %d panels", layout_val.pattern_id, len(layout_val.panels)))
|
||||
clay.UI(clay.ID("LayoutDetailInfoSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_small("btn_layout_regen", "Regen")
|
||||
}
|
||||
|
||||
@@ -243,33 +241,66 @@ declare_layout_detail :: proc(app: ^GUI_App_State) {
|
||||
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},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, childGap = CLAY_SPACE_12},
|
||||
}) {
|
||||
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),
|
||||
if clay.UI(clay.ID("LayoutLeftCol"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(0.7), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 8},
|
||||
}) {
|
||||
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 clay.UI(clay.ID("LayoutWireframe"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom},
|
||||
backgroundColor = CLAY_BG_STRIP,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
}) {}
|
||||
|
||||
if app.layout_selected_panel >= 0 && app.layout_selected_panel < len(layout_val.panels) {
|
||||
sel_panel := layout_val.panels[app.layout_selected_panel]
|
||||
if clay.UI(clay.ID("LayoutPanelInfo"))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 8, right = 10, bottom = 8, left = 10}, childGap = 4, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}},
|
||||
backgroundColor = CLAY_BG_CARD_ALT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = CLAY_ACCENT, 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})
|
||||
status_label := "missing"
|
||||
status_color := CLAY_TEXT_TERTIARY
|
||||
if _, has_img := app.controller.state.panel_images[sel_panel.panel_id]; has_img {
|
||||
status_label = "ready"
|
||||
status_color = CLAY_SUCCESS
|
||||
}
|
||||
if _, has_err := app.controller.state.panel_errors[sel_panel.panel_id]; has_err {
|
||||
status_label = "error"
|
||||
status_color = CLAY_ERROR
|
||||
}
|
||||
if clay.UI(clay.ID("LayoutPanelInfoHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 6},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Panel %d", sel_panel.panel_number), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
declare_status_badge("layout_sel_status", status_label, status_label == "ready")
|
||||
}
|
||||
|
||||
panel_data, _, panel_ok := panel_by_flat_index(app.controller.state.script, app.layout_selected_panel)
|
||||
if panel_ok {
|
||||
clay_body_text(shot_type_label(panel_data.shot_type), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
desc := panel_data.description
|
||||
if len(desc) > 80 { desc = fmt.tprintf("%s...", desc[:77]) }
|
||||
clay_body_text(desc, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_XS)
|
||||
if len(panel_data.characters_present) > 0 {
|
||||
if clay.UI(clay.ID("LayoutPanelChars"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4}}) {
|
||||
for cid in panel_data.characters_present {
|
||||
declare_nav_chip(fmt.tprintf("layout_char_%s", cid), cid, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clay_muted_text(sel_panel.panel_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("LayoutPageScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
clay_label_text("PAGES")
|
||||
visible_rows: int = 6
|
||||
if visible_rows > layout_count { visible_rows = layout_count }
|
||||
start := idx - visible_rows / 2
|
||||
@@ -283,16 +314,22 @@ declare_layout_detail :: proc(app: ^GUI_App_State) {
|
||||
|
||||
for i in start..<end {
|
||||
l := app.controller.state.page_layouts[i]
|
||||
is_cur := i == idx
|
||||
is_hov := clay.Hovered()
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if i == idx { row_color = CLAY_TEXT_BRIGHT }
|
||||
if is_cur { row_color = CLAY_TEXT_BRIGHT }
|
||||
else if is_hov { row_color = CLAY_TEXT_PRIMARY }
|
||||
|
||||
row_bg := clay.Color{0, 0, 0, 0}
|
||||
if is_cur { row_bg = CLAY_BG_SELECTED }
|
||||
else if is_hov { row_bg = CLAY_BG_HOVER }
|
||||
|
||||
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},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(24)}, padding = {top = 2, right = 6, bottom = 2, left = 6}, childAlignment = {y = .Center}},
|
||||
backgroundColor = row_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XS),
|
||||
}) {
|
||||
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})
|
||||
clay.Text(fmt.tprintf("%02d %s (%d)", l.page_number, l.pattern_id, len(l.panels)), {fontId = CLAY_FONT_MONO, fontSize = CLAY_FONT_SIZE_XS, textColor = row_color})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,12 +337,9 @@ declare_layout_detail :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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")
|
||||
declare_section_header("BubblesDetailHeader", "Bubble Editor")
|
||||
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 {
|
||||
@@ -318,19 +352,20 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
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)
|
||||
clay_body_text(fmt.tprintf("Page %d has no panels", layout_val.page_number), color = CLAY_TEXT_TERTIARY, 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)
|
||||
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))
|
||||
clay.UI(clay.ID("BubblePanelInfoSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_small("btn_bubble_auto_place", "Auto Place")
|
||||
}
|
||||
|
||||
@@ -350,11 +385,12 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
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)
|
||||
clay.UI(clay.ID("BubbleListSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
if bubble_count == 0 {
|
||||
clay_muted_text("No bubbles for this panel. Click Add or Auto Place.")
|
||||
@@ -362,21 +398,27 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
for i in 0..<bubble_count {
|
||||
b := bubbles_for_panel[i]
|
||||
is_selected: bool = (i == bubble_idx)
|
||||
is_hov := clay.Hovered()
|
||||
row_color: clay.Color = CLAY_TEXT_TERTIARY
|
||||
if is_selected { row_color = CLAY_TEXT_BRIGHT }
|
||||
else if is_hov { row_color = CLAY_TEXT_PRIMARY }
|
||||
|
||||
preview := b.text
|
||||
if len(preview) > 25 { preview = preview[:25] }
|
||||
if len(preview) == 0 { preview = "(empty)" }
|
||||
|
||||
row_bg := clay.Color{0, 0, 0, 0}
|
||||
if is_selected { row_bg = CLAY_BG_SELECTED }
|
||||
else if is_hov { row_bg = CLAY_BG_HOVER }
|
||||
|
||||
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},
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(26)}, padding = {top = 2, right = 6, bottom = 2, left = 6}, childGap = 4, childAlignment = {y = .Center}},
|
||||
backgroundColor = row_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XS),
|
||||
}) {
|
||||
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})
|
||||
clay.Text(fmt.tprintf("[%s] %s", bubble_type_name(b.type), preview), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = row_color})
|
||||
if is_selected {
|
||||
clay.UI(clay.ID(fmt.tprintf("bubble_row_spacer_%d", i)))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_small(fmt.tprintf("btn_bubble_delete_%d", i), "x")
|
||||
}
|
||||
}
|
||||
@@ -388,9 +430,10 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
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),
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 10, right = 12, bottom = 10, left = 12}, childGap = 6, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}},
|
||||
backgroundColor = CLAY_BG_CARD_ALT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Editing: %s", bubble_type_name(selected.type)), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
@@ -400,27 +443,30 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
|
||||
for t in types {
|
||||
btn_id := fmt.tprintf("btn_btype_%d", int(t))
|
||||
is_active := selected.type == t
|
||||
is_hov := clay.Hovered()
|
||||
bg := clay_color_for_bubble_type(is_active)
|
||||
if !is_active && is_hov { bg = CLAY_BTN_DEFAULT_HOVER }
|
||||
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),
|
||||
layout = {sizing = {width = clay.SizingFit({min = 44, max = 20}), height = clay.SizingFixed(24)}, padding = {top = 2, right = 6, bottom = 2, left = 6}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = bg,
|
||||
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)})
|
||||
clay.Text(bubble_type_name(t), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = clay_text_color_for_bubble_type(is_active)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text editing area
|
||||
if clay.UI(clay.ID("BubbleTextRow"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_muted_text("Text:")
|
||||
clay_label_text("TEXT")
|
||||
text_to_show := app.bubble_edit_text if app.selected_field == 7 else selected.text
|
||||
if len(text_to_show) == 0 && app.selected_field != 7 {
|
||||
text_to_show = "(click to edit)"
|
||||
}
|
||||
if clay.UI(clay.ID("field_bubble_text"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, padding = {top = 6, right = 8, bottom = 6, left = 8}},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM))}, padding = {top = 6, right = 8, bottom = 6, left = 8}},
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_INPUT_FOCUS if app.selected_field == 7 else CLAY_INPUT_BORDER, width = clay.BorderOutside(1)},
|
||||
@@ -436,6 +482,151 @@ declare_bubbles_detail :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
|
||||
|
||||
draw_layout_wireframe :: proc(app: ^GUI_App_State) {
|
||||
if app.controller.active_screen != .Layout { return }
|
||||
layout_count := len(app.controller.state.page_layouts)
|
||||
if layout_count == 0 { return }
|
||||
|
||||
wire_data := clay.GetElementData(clay.ID("LayoutWireframe"))
|
||||
if !wire_data.found { return }
|
||||
bounds := wire_data.boundingBox
|
||||
if bounds.width <= 0 || bounds.height <= 0 { return }
|
||||
|
||||
idx := clamp_layout_cursor(layout_count, app.summary_opts.layout_page_cursor)
|
||||
if idx >= layout_count { return }
|
||||
layout_val := app.controller.state.page_layouts[idx]
|
||||
|
||||
padding := f32(8)
|
||||
avail_w := bounds.width - padding * 2
|
||||
avail_h := bounds.height - padding * 2
|
||||
|
||||
page_aspect := f32(layout_val.width) / f32(layout_val.height)
|
||||
if layout_val.width <= 0 || layout_val.height <= 0 { page_aspect = 0.707 }
|
||||
|
||||
inner_w := avail_w
|
||||
inner_h := avail_h
|
||||
if inner_w / inner_h > page_aspect {
|
||||
inner_w = inner_h * page_aspect
|
||||
} else {
|
||||
inner_h = inner_w / page_aspect
|
||||
}
|
||||
|
||||
inner_x := bounds.x + padding + (avail_w - inner_w) * 0.5
|
||||
inner_y := bounds.y + padding + (avail_h - inner_h) * 0.5
|
||||
|
||||
rl.DrawRectangleRounded(
|
||||
{bounds.x, bounds.y, bounds.width, bounds.height},
|
||||
0.03, 6,
|
||||
rl.Color{12, 12, 20, 255},
|
||||
)
|
||||
|
||||
rl.DrawRectangleRounded(
|
||||
{inner_x, inner_y, inner_w, inner_h},
|
||||
0.02, 4,
|
||||
rl.Color{16, 16, 24, 255},
|
||||
)
|
||||
|
||||
app.wireframe_cell_count = 0
|
||||
|
||||
for i in 0..<len(layout_val.panels) {
|
||||
panel_layout := layout_val.panels[i]
|
||||
cell := panel_layout.layout_cell
|
||||
cx := inner_x + cell.x * inner_w
|
||||
cy := inner_y + cell.y * inner_h
|
||||
cw := cell.w * inner_w
|
||||
ch := cell.h * inner_h
|
||||
|
||||
gap := f32(2)
|
||||
cx += gap
|
||||
cy += gap
|
||||
cw -= gap * 2
|
||||
ch -= gap * 2
|
||||
|
||||
if i < len(app.wireframe_cell_rects) {
|
||||
app.wireframe_cell_rects[i] = rl.Rectangle{cx, cy, cw, ch}
|
||||
}
|
||||
app.wireframe_cell_count = min(i + 1, len(app.wireframe_cell_rects))
|
||||
|
||||
is_selected := i == app.layout_selected_panel
|
||||
has_error := false
|
||||
if _, err_ok := app.controller.state.panel_errors[panel_layout.panel_id]; err_ok {
|
||||
has_error = true
|
||||
}
|
||||
|
||||
border_color := rl.Color{55, 65, 100, 200}
|
||||
if is_selected {
|
||||
border_color = clay_color_to_rl(CLAY_ACCENT)
|
||||
} else if has_error {
|
||||
border_color = rl.Color{180, 70, 70, 200}
|
||||
}
|
||||
|
||||
rl.DrawRectangleRounded({cx, cy, cw, ch}, 0.03, 4, rl.Color{8, 8, 14, 255})
|
||||
|
||||
drawn_image := false
|
||||
if panel_img, img_ok := app.controller.state.panel_images[panel_layout.panel_id]; img_ok {
|
||||
img_url := pool_clone(panel_img.url)
|
||||
tex, loaded := load_panel_texture(&app.panel_textures, panel_layout.panel_id, img_url)
|
||||
if loaded && tex.id != 0 && cw > 4 && ch > 4 {
|
||||
scale_x := cw / f32(tex.width)
|
||||
scale_y := ch / f32(tex.height)
|
||||
scale := min(scale_x, scale_y)
|
||||
draw_w := f32(tex.width) * scale
|
||||
draw_h := f32(tex.height) * scale
|
||||
offset_x := cx + (cw - draw_w) * 0.5
|
||||
offset_y := cy + (ch - draw_h) * 0.5
|
||||
|
||||
rl.DrawTextureEx(tex, {offset_x, offset_y}, 0, scale, rl.WHITE)
|
||||
drawn_image = true
|
||||
}
|
||||
}
|
||||
|
||||
if !drawn_image {
|
||||
bg := rl.Color{28, 28, 44, 220}
|
||||
if has_error {
|
||||
bg = rl.Color{50, 25, 25, 220}
|
||||
}
|
||||
rl.DrawRectangleRounded({cx, cy, cw, ch}, 0.03, 4, bg)
|
||||
|
||||
if cw > 40 && ch > 30 {
|
||||
no_img_text := "No image"
|
||||
if has_error {
|
||||
no_img_text = "Error"
|
||||
}
|
||||
rl.DrawText(
|
||||
strings.unsafe_string_to_cstring(no_img_text),
|
||||
i32(cx + (cw - 52) * 0.5), i32(cy + ch * 0.5 - 6),
|
||||
10,
|
||||
rl.Color{100, 100, 130, 180},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if is_selected {
|
||||
rl.DrawRectangleRoundedLines({cx - 1, cy - 1, cw + 2, ch + 2}, 0.03, 4, rl.Color{99, 102, 241, 60})
|
||||
rl.DrawRectangleRoundedLines({cx, cy, cw, ch}, 0.03, 4, border_color)
|
||||
} else {
|
||||
rl.DrawRectangleRoundedLines({cx, cy, cw, ch}, 0.03, 4, border_color)
|
||||
}
|
||||
|
||||
if cw > 20 && ch > 14 {
|
||||
badge_w: i32 = 18
|
||||
if panel_layout.panel_number >= 10 { badge_w = 24 }
|
||||
rl.DrawRectangle(i32(cx + 2), i32(cy + 2), badge_w, 14, rl.Color{0, 0, 0, 170})
|
||||
num_text := fmt.tprintf("%d", panel_layout.panel_number)
|
||||
rl.DrawText(
|
||||
strings.unsafe_string_to_cstring(num_text),
|
||||
i32(cx + 4), i32(cy + 3),
|
||||
10,
|
||||
rl.Color{220, 220, 240, 230},
|
||||
)
|
||||
}
|
||||
|
||||
if has_error && cw > 20 && ch > 14 {
|
||||
rl.DrawText("!", i32(cx + cw - 10), i32(cy + 2), 10, rl.Color{248, 113, 113, 230})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clay_color_for_bubble_type :: proc(active: bool) -> clay.Color {
|
||||
if active { return CLAY_ACCENT }
|
||||
return CLAY_BTN_DEFAULT
|
||||
@@ -446,29 +637,24 @@ clay_text_color_for_bubble_type :: proc(active: bool) -> clay.Color {
|
||||
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()}) {
|
||||
if clay.UI(clay.ID("ActionLogHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
declare_section_header("ActionLogTitle", "Action Log")
|
||||
clay.UI(clay.ID("ActionLogHeaderSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
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")
|
||||
declare_button_small("btn_log_report", "Report")
|
||||
declare_button_small("btn_log_copy", "Copy")
|
||||
declare_button_small("btn_log_diag", "Diag")
|
||||
declare_button_small("btn_log_clear", "Clear")
|
||||
}
|
||||
|
||||
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))
|
||||
clay_body_text(fmt.tprintf("%d lines, %s first", app.log_show_lines, order_label), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
|
||||
// Log entries
|
||||
if clay.UI(clay.ID("LogScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
@@ -496,8 +682,8 @@ declare_action_log :: proc(app: ^GUI_App_State) {
|
||||
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)
|
||||
clay_body_text(entry_text, color = entry_color, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,4 @@ local_panel_id_by_index :: proc(i: int) -> string {
|
||||
return fmt.tprintf("panel_local_overflow_%d", i)
|
||||
}
|
||||
|
||||
append_char :: proc(dst: ^string, ch: rune) {
|
||||
dst^ = fmt.aprintf("%s%c", dst^, ch)
|
||||
}
|
||||
|
||||
pop_char :: proc(dst: ^string) {
|
||||
if len(dst^) == 0 {
|
||||
return
|
||||
}
|
||||
dst^ = dst^[:len(dst^)-1]
|
||||
}
|
||||
+103
-25
@@ -3,31 +3,34 @@ package gui
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
|
||||
// ─── Clay UI Primitives ───────────────────────────────────────────
|
||||
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 active { bg = CLAY_NAV_ACTIVE_BG }
|
||||
is_hov := clay.Hovered()
|
||||
hov_bg: clay.Color = CLAY_NAV_HOVER_BG
|
||||
if is_hov && !active { bg = hov_bg }
|
||||
if clay.UI(clay.ID(id))({
|
||||
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}},
|
||||
layout = {sizing = {width = clay.SizingFit({min = 44, max = 120}), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM))}, padding = {top = 4, right = 10, bottom = 4, left = 10}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
text_color: clay.Color = CLAY_TEXT_SECONDARY
|
||||
if active { text_color = CLAY_TEXT_BRIGHT }
|
||||
clay_body_text(label, color = text_color)
|
||||
else if is_hov { text_color = CLAY_TEXT_PRIMARY }
|
||||
clay_body_text(label, color = text_color, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
declare_button :: proc(id: string, label: string, bg, hover_bg: clay.Color) {
|
||||
is_hovered := clay.Hovered()
|
||||
current_bg: clay.Color = bg
|
||||
if is_hovered { current_bg = hover_bg }
|
||||
current_bg: clay.Color = bg
|
||||
if is_hovered { current_bg = hover_bg }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = current_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_FRAC_BTN),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
}) {
|
||||
clay_body_text(label, color = CLAY_TEXT_PRIMARY)
|
||||
clay_body_text(label, color = CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,24 +50,53 @@ declare_button_primary :: proc(id: string, label: string) {
|
||||
declare_button(id, label, CLAY_ACCENT, CLAY_ACCENT_HOVER)
|
||||
}
|
||||
|
||||
declare_button_small :: proc(id: string, label: string) {
|
||||
declare_button_ghost :: proc(id: string, label: string) {
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_BTN_GHOST
|
||||
if is_hov { bg = CLAY_BTN_GHOST_HOVER }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {sizing = {width = clay.SizingFit({min = 40, max = 24}), height = clay.SizingFixed(24)}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = CLAY_BTN_DEFAULT,
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
}) {
|
||||
clay_body_text(label, color = CLAY_TEXT_SECONDARY if !is_hov else CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
declare_button_small :: proc(id: string, label: string) {
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_BTN_DEFAULT
|
||||
if is_hov { bg = CLAY_BTN_DEFAULT_HOVER }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {sizing = {width = clay.SizingFit({min = 36, max = 24}), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM - 4))}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
}) {
|
||||
clay.Text(label, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = CLAY_TEXT_SECONDARY})
|
||||
clay.Text(label, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = CLAY_TEXT_SECONDARY if !is_hov else CLAY_TEXT_PRIMARY})
|
||||
}
|
||||
}
|
||||
|
||||
declare_button_recommended :: proc(id: string, label: string) {
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_ACCENT
|
||||
if is_hov { bg = CLAY_ACCENT_HOVER }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = CLAY_ACCENT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_FRAC_BTN),
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = CLAY_ACCENT_GLOW, width = clay.BorderOutside(2)},
|
||||
}) {
|
||||
clay_body_text(label, color = CLAY_TEXT_BRIGHT)
|
||||
clay_body_text(label, color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
declare_button_disabled :: proc(id: string, label: string) {
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = CLAY_BTN_DISABLED,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
}) {
|
||||
clay_body_text(label, color = CLAY_TEXT_DISABLED, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,28 +106,74 @@ declare_status_badge :: proc(id: string, label: string, ok: bool) {
|
||||
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 = 70, max = 28}), height = clay.SizingFixed(26)}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
||||
layout = {sizing = {width = clay.SizingFit({min = 60, max = 28}), height = clay.SizingFixed(22)}, padding = {top = 2, right = 8, bottom = 2, left = 8}, childAlignment = {x = .Center, y = .Center}},
|
||||
backgroundColor = badge_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = badge_color, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay.Text(label, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_SM, textColor = badge_color})
|
||||
clay.Text(label, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = badge_color})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
||||
// ─── Stat Chip (Clay) ──────────────────────────────────────────────
|
||||
declare_stat_chip :: proc(id: string, label: string, value: int) {
|
||||
value_text := fmt.tprintf("%d", value)
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_BG_STRIP
|
||||
if is_hov { bg = CLAY_BG_HOVER }
|
||||
if clay.UI(clay.ID(id))({
|
||||
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},
|
||||
layout = {sizing = {width = clay.SizingFixed(86), height = clay.SizingFixed(32)}, padding = {top = 4, right = 8, bottom = 4, left = 8}, childAlignment = {x = .Center, y = .Center}, childGap = 2, layoutDirection = .TopToBottom},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = clay.Color{55, 55, 75, 255}, width = clay.BorderOutside(1)},
|
||||
border = {color = CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_muted_text(label)
|
||||
clay_label_text(label)
|
||||
clay_body_text(value_text, color = CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
declare_section_header :: proc(id: string, title: string, subtitle: string = "") {
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
clay_title_text(title, size = CLAY_FONT_SIZE_XL)
|
||||
if len(subtitle) > 0 {
|
||||
clay_muted_text(subtitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare_divider :: proc(id: string) {
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(1)}},
|
||||
backgroundColor = CLAY_BORDER_DIVIDER,
|
||||
}) {}
|
||||
}
|
||||
|
||||
declare_empty_state :: proc(id: string, title: string, description: string, hint: string = "") {
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, layoutDirection = .TopToBottom, padding = {top = CLAY_SPACE_32, right = CLAY_SPACE_16, bottom = CLAY_SPACE_32, left = CLAY_SPACE_16}, childAlignment = {x = .Center}, childGap = CLAY_SPACE_8},
|
||||
backgroundColor = CLAY_BG_CARD_ALT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_title_text(title, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_LG)
|
||||
clay_body_text(description, color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
if len(hint) > 0 {
|
||||
clay_body_text(hint, color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare_info_callout :: proc(id: string, message: string, ok: bool) {
|
||||
badge_color: clay.Color = CLAY_ERROR
|
||||
badge_bg: clay.Color = CLAY_ERROR_DIM
|
||||
if ok { badge_color = CLAY_SUCCESS; badge_bg = CLAY_SUCCESS_DIM }
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 8, right = 12, bottom = 8, left = 12}, childGap = 4, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}},
|
||||
backgroundColor = badge_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = badge_color, width = {0, 0, 0, 2, 0}},
|
||||
}) {
|
||||
clay_body_text(message, color = badge_color, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
+526
-109
@@ -1,7 +1,11 @@
|
||||
package gui
|
||||
|
||||
import "core:c"
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import "core:os"
|
||||
import filepath "core:path/filepath"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
import "../core"
|
||||
@@ -9,15 +13,22 @@ import "../shared"
|
||||
import "../ui"
|
||||
|
||||
// GUI_App_State holds all mutable GUI-local state for the main loop.
|
||||
FIELD_BUF_SIZE :: 1024
|
||||
|
||||
ANIM_DURATION_SIDEBAR :: 0.25
|
||||
ANIM_DURATION_OVERLAY :: 0.2
|
||||
ANIM_DURATION_SLIDE :: 0.3
|
||||
|
||||
GUI_App_State :: struct {
|
||||
controller: ui.App_Controller,
|
||||
selected_field: int, // 0 idea, 1 genre, 2 audience, 3 export_path, 4 pages, 5 project_path, 6 autosave_interval
|
||||
selected_field: int,
|
||||
export_path: string,
|
||||
project_path: string,
|
||||
local_script_pages: string,
|
||||
autosave_interval_text: string,
|
||||
export_format: core.Export_Format,
|
||||
use_fal_panels: bool,
|
||||
sidebar_collapsed: bool,
|
||||
status_msg: string,
|
||||
is_dirty: bool,
|
||||
autosave_enabled: bool,
|
||||
@@ -33,20 +44,110 @@ GUI_App_State :: struct {
|
||||
show_confirm_overlay: bool,
|
||||
pending_confirm: Pending_Confirm_Action,
|
||||
panel_textures: map[string]rl.Texture2D,
|
||||
wireframe_cell_rects: [16]rl.Rectangle,
|
||||
wireframe_cell_count: int,
|
||||
layout_selected_panel: int,
|
||||
bubble_edit_text: string,
|
||||
editing_char_id: string,
|
||||
char_edit_text: string,
|
||||
editing_panel_id: string,
|
||||
panel_edit_text: string,
|
||||
field_buf: [FIELD_BUF_SIZE]u8,
|
||||
// ─── Animation state ──────────────────────────────────────────
|
||||
sidebar_anim: f32, // current animated sidebar width (px)
|
||||
overlay_alpha: f32, // 0→1 fade progress for active overlay
|
||||
prev_screen: ui.App_Screen,
|
||||
slide_offset: f32, // horizontal offset for screen transitions
|
||||
slide_progress: f32, // 0→1 progress of slide animation
|
||||
}
|
||||
|
||||
clicked :: proc(id: clay.ElementId) -> bool {
|
||||
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
||||
}
|
||||
|
||||
// ─── Persistent String Pool (survives temp-allocator resets) ────
|
||||
@(private)
|
||||
persistent_pool: [256 * 1024]u8
|
||||
@(private)
|
||||
persistent_offset: int
|
||||
|
||||
@(private)
|
||||
pool_clone :: proc(s: string) -> string {
|
||||
if len(s) == 0 { return "" }
|
||||
total := len(s) + 1
|
||||
if persistent_offset + total > len(persistent_pool) {
|
||||
// Pool full — fall back to strings.clone (heap) instead of corrupting old data
|
||||
return strings.clone(s)
|
||||
}
|
||||
start := persistent_offset
|
||||
for c, i in s {
|
||||
persistent_pool[start + i] = u8(c)
|
||||
}
|
||||
persistent_pool[start + len(s)] = 0
|
||||
persistent_offset += total
|
||||
return string(persistent_pool[start:start+len(s)])
|
||||
}
|
||||
|
||||
// ─── Panel Image Loading ─────────────────────────────────────────
|
||||
@(private)
|
||||
file_url_to_path :: proc(url: string) -> string {
|
||||
download_path_cache: map[string]string
|
||||
@(private)
|
||||
download_failed_cache: map[string]bool
|
||||
|
||||
@(private)
|
||||
resolve_image_path :: proc(url: string) -> string {
|
||||
if download_path_cache == nil {
|
||||
download_path_cache = make(map[string]string, context.allocator)
|
||||
}
|
||||
if download_failed_cache == nil {
|
||||
download_failed_cache = make(map[string]bool, context.allocator)
|
||||
}
|
||||
|
||||
if strings.has_prefix(url, "file://") {
|
||||
return url[7:]
|
||||
}
|
||||
return url
|
||||
if !strings.has_prefix(url, "http://") && !strings.has_prefix(url, "https://") {
|
||||
// Reject non-URL garbage (could be corrupted temp-allocator data)
|
||||
if len(url) == 0 || url[0] < 32 || url[0] > 126 { return "" }
|
||||
return url
|
||||
}
|
||||
|
||||
if cached_path, ok := download_path_cache[url]; ok {
|
||||
return pool_clone(cached_path)
|
||||
}
|
||||
|
||||
if _, failed := download_failed_cache[url]; failed {
|
||||
return ""
|
||||
}
|
||||
|
||||
base := url
|
||||
if q := strings.index(base, "?"); q >= 0 {
|
||||
base = base[:q]
|
||||
}
|
||||
filename := filepath.base(base)
|
||||
if len(filename) == 0 {
|
||||
filename = "cached_image.png"
|
||||
}
|
||||
|
||||
cache_dir := "./assets"
|
||||
os.mkdir_all(cache_dir)
|
||||
local_path := fmt.aprintf("%s/%s", cache_dir, filename)
|
||||
|
||||
if os.exists(local_path) {
|
||||
download_path_cache[url] = pool_clone(local_path)
|
||||
return pool_clone(local_path)
|
||||
}
|
||||
|
||||
cmd := [6]string{"curl", "-L", "-sS", "-o", local_path, url}
|
||||
desc := os.Process_Desc{command = cmd[:]}
|
||||
state, _, _, exec_err := os.process_exec(desc, context.temp_allocator)
|
||||
if exec_err != nil || !state.exited || state.exit_code != 0 {
|
||||
download_failed_cache[url] = true
|
||||
return ""
|
||||
}
|
||||
|
||||
download_path_cache[url] = pool_clone(local_path)
|
||||
return pool_clone(local_path)
|
||||
}
|
||||
|
||||
@(private)
|
||||
@@ -54,19 +155,25 @@ load_panel_texture :: proc(cache: ^map[string]rl.Texture2D, panel_id: string, ur
|
||||
if tex, ok := cache[panel_id]; ok {
|
||||
return tex, true
|
||||
}
|
||||
filepath := file_url_to_path(url)
|
||||
filepath := resolve_image_path(url)
|
||||
if len(filepath) == 0 { return {}, false }
|
||||
fpath_c := strings.clone_to_cstring(filepath)
|
||||
defer delete(fpath_c)
|
||||
img := rl.LoadImage(fpath_c)
|
||||
if img.data == nil { return {}, false }
|
||||
defer rl.UnloadImage(img)
|
||||
tex := rl.LoadTextureFromImage(img)
|
||||
tex := rl.LoadTexture(fpath_c)
|
||||
if tex.id == 0 {
|
||||
// Fallback: try LoadImage + LoadTextureFromImage for broader format support
|
||||
img := rl.LoadImage(fpath_c)
|
||||
if img.data != nil {
|
||||
defer rl.UnloadImage(img)
|
||||
tex = rl.LoadTextureFromImage(img)
|
||||
}
|
||||
}
|
||||
if tex.id == 0 { return {}, false }
|
||||
rl.SetTextureFilter(tex, .BILINEAR)
|
||||
cache[panel_id] = tex
|
||||
return tex, true
|
||||
}
|
||||
|
||||
}
|
||||
@(private)
|
||||
unload_panel_textures :: proc(cache: ^map[string]rl.Texture2D) {
|
||||
for _, tex in cache {
|
||||
@@ -94,6 +201,9 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
|
||||
app: GUI_App_State
|
||||
app.controller = controller
|
||||
app.panel_textures = make(map[string]rl.Texture2D, 64)
|
||||
reserve(&app.panel_textures, 256)
|
||||
app.layout_selected_panel = -1
|
||||
app.selected_field = 0
|
||||
app.export_path = "./gui_export.pdf"
|
||||
app.project_path = "./gui_project.comic.json"
|
||||
@@ -108,7 +218,9 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
app.last_save_at = -1
|
||||
app.last_export_at = -1
|
||||
app.log_oldest_first = false
|
||||
app.summary_opts = Summary_View_Options{}
|
||||
app.summary_opts = Summary_View_Options{script_show_all = true, script_desc = true}
|
||||
app.sidebar_anim = f32(shared.LAYOUT.sidebar_width)
|
||||
app.prev_screen = app.controller.active_screen
|
||||
push_status(&app.status_msg, &app.action_log, app.status_msg)
|
||||
defer action_log_dispose(&app.action_log)
|
||||
defer delete(app.status_msg)
|
||||
@@ -117,7 +229,16 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
for !rl.WindowShouldClose() {
|
||||
screen_w := rl.GetScreenWidth()
|
||||
screen_h := rl.GetScreenHeight()
|
||||
bp := shared.breakpoint(screen_w, screen_h)
|
||||
compact_mode := shared.is_compact(screen_h)
|
||||
dt := rl.GetFrameTime()
|
||||
|
||||
update_sidebar_anim(&app, bp, dt)
|
||||
update_overlay_anim(&app, dt)
|
||||
update_slide_anim(&app, dt)
|
||||
|
||||
sidebar_w := sidebar_width(bp, app.sidebar_collapsed, app.sidebar_anim)
|
||||
main_w := shared.compute_main_width(screen_w, sidebar_w)
|
||||
cfg := shared.load_config()
|
||||
has_deepseek_key := len(cfg.deepseek_api_key) > 0
|
||||
has_fal_key := len(cfg.fal_api_key) > 0
|
||||
@@ -131,7 +252,16 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
if rl.IsKeyPressed(.SLASH) { toggle_help_overlay(&app.show_help_overlay) }
|
||||
if rl.IsKeyPressed(.ESCAPE) { close_help_overlay_if_open(&app.show_help_overlay) }
|
||||
|
||||
// Compact mode forces the sidebar collapsed
|
||||
if bp == .Compact { app.sidebar_collapsed = true }
|
||||
|
||||
if !interaction_locked {
|
||||
if rl.IsKeyPressed(.B) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) {
|
||||
app.sidebar_collapsed = !app.sidebar_collapsed
|
||||
status := "Sidebar expanded"
|
||||
if app.sidebar_collapsed { status = "Sidebar collapsed" }
|
||||
push_status(&app.status_msg, &app.action_log, status)
|
||||
}
|
||||
if rl.IsKeyPressed(.ONE) { _ = ui.navigate_to_screen(&app.controller, .Story) }
|
||||
if rl.IsKeyPressed(.TWO) { _ = ui.navigate_to_screen(&app.controller, .Script) }
|
||||
if rl.IsKeyPressed(.THREE) { _ = ui.navigate_to_screen(&app.controller, .Characters) }
|
||||
@@ -180,39 +310,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Text Input ────────────────────────────────────────────
|
||||
if !interaction_locked {
|
||||
for {
|
||||
ch := rl.GetCharPressed()
|
||||
if ch == 0 { break }
|
||||
if ch < 32 || ch > 126 { continue }
|
||||
if app.selected_field == 6 && (ch < '0' || ch > '9') { continue }
|
||||
switch app.selected_field {
|
||||
case 0: append_char(&app.controller.state.story_idea, ch)
|
||||
case 1: append_char(&app.controller.state.story_genre, ch)
|
||||
case 2: append_char(&app.controller.state.target_audience, ch)
|
||||
case 3: append_char(&app.export_path, ch)
|
||||
case 4: append_char(&app.local_script_pages, ch)
|
||||
case 5: append_char(&app.project_path, ch)
|
||||
case 6: append_char(&app.autosave_interval_text, ch)
|
||||
case 7: append_char(&app.bubble_edit_text, ch)
|
||||
}
|
||||
app.is_dirty = true
|
||||
}
|
||||
if rl.IsKeyPressed(.BACKSPACE) {
|
||||
switch app.selected_field {
|
||||
case 0: pop_char(&app.controller.state.story_idea)
|
||||
case 1: pop_char(&app.controller.state.story_genre)
|
||||
case 2: pop_char(&app.controller.state.target_audience)
|
||||
case 3: pop_char(&app.export_path)
|
||||
case 4: pop_char(&app.local_script_pages)
|
||||
case 5: pop_char(&app.project_path)
|
||||
case 6: pop_char(&app.autosave_interval_text)
|
||||
case 7: pop_char(&app.bubble_edit_text)
|
||||
}
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
// ─── Text Input (raygui GuiTextBox overlay) ────────────────
|
||||
// raygui GuiTextBox is called after Clay render below
|
||||
|
||||
// ─── Keyboard Shortcuts ─────────────────────────────────────
|
||||
ctrl_down := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
|
||||
@@ -327,7 +426,8 @@ app.is_dirty = true
|
||||
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 ──────────────────────────────
|
||||
|
||||
fmt.eprintf("LAYOUT: screen=%dx%d sidebar_w=%.0f sidebar_anim=%.0f collapsed=%v bp=%v\n",
|
||||
screen_w, screen_h, f32(sidebar_width(bp, app.sidebar_collapsed, 0)), app.sidebar_anim, app.sidebar_collapsed, bp)
|
||||
clay.BeginLayout()
|
||||
|
||||
// Root: horizontal layout (sidebar + main)
|
||||
@@ -335,39 +435,54 @@ app.is_dirty = true
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .LeftToRight},
|
||||
backgroundColor = CLAY_BG_BASE,
|
||||
}) {
|
||||
// ─── Sidebar ───────────────────────────────────────
|
||||
declare_sidebar(&app)
|
||||
// ─── Sidebar (responsive width) ───────────────────────
|
||||
if clay.UI(clay.ID("Sidebar"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(f32(sidebar_w)), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = 10, right = 10, bottom = 10, left = 10}, childGap = 4},
|
||||
backgroundColor = CLAY_BG_SIDEBAR,
|
||||
}) {
|
||||
declare_sidebar(&app, bp)
|
||||
}
|
||||
|
||||
// ─── Main Content ────────────────────────────────
|
||||
if clay.UI(clay.ID("MainArea"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom},
|
||||
}) {
|
||||
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)
|
||||
declare_pipeline_bar(&app, bp)
|
||||
declare_workspace(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count, bp, main_w)
|
||||
next_hint := gui_next_hint(app.controller)
|
||||
declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint, has_fal_key)
|
||||
declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint, has_fal_key, bp)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Floating Overlays (Clay) ──────────────────────────────
|
||||
if app.show_help_overlay {
|
||||
declare_help_overlay()
|
||||
// ─── Floating Overlays (Clay, animated alpha) ─────────────
|
||||
if app.show_help_overlay || app.overlay_alpha > 0.01 {
|
||||
declare_help_overlay(bp, app.overlay_alpha)
|
||||
}
|
||||
if app.show_confirm_overlay {
|
||||
declare_confirm_overlay(app.pending_confirm)
|
||||
if app.show_confirm_overlay || app.overlay_alpha > 0.01 {
|
||||
declare_confirm_overlay(app.pending_confirm, bp, app.overlay_alpha)
|
||||
}
|
||||
declare_toast(&app.action_log)
|
||||
|
||||
render_commands := clay.EndLayout(rl.GetFrameTime())
|
||||
|
||||
// ─── Click Detection (after layout, before render) ────────
|
||||
process_clicks(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, has_fal_key, pages_count, shift_down, autosave_secs, compact_mode)
|
||||
process_clicks(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, has_fal_key, pages_count, shift_down, autosave_secs, compact_mode, bp)
|
||||
|
||||
// ─── Render ────────────────────────────────────────────────────
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(BG_BASE)
|
||||
rl.EndScissorMode()
|
||||
rl.ClearBackground(RL_BG_BASE)
|
||||
clay_raylib_render(&render_commands)
|
||||
|
||||
// ─── Layout wireframe overlay (raylib, after Clay) ──────────────
|
||||
draw_layout_wireframe(&app)
|
||||
|
||||
// ─── Raygui field overlay (draw after Clay, before end) ─────────
|
||||
if !interaction_locked {
|
||||
render_raygui_fields(&app)
|
||||
rl.EndScissorMode()
|
||||
}
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
@@ -433,7 +548,81 @@ handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, ca
|
||||
else { push_status(&app.status_msg, &app.action_log, open_project_session(&app.controller, &app.project_path, &app.export_path, app.export_format, &app.is_dirty, &app.last_autosave_at)) }
|
||||
}
|
||||
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_mode_casual")) { app.controller.state.user_mode = .Casual; push_status(&app.status_msg, &app.action_log, "Mode: Casual") }
|
||||
if clicked(clay.ID("btn_mode_pro")) { app.controller.state.user_mode = .Professional; push_status(&app.status_msg, &app.action_log, "Mode: Professional") }
|
||||
// Genre chips
|
||||
genres := []string{"action", "comedy", "drama", "scifi", "fantasy", "horror", "romance", "mystery", "slice-of-life"}
|
||||
for g in genres {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_genre_%s", g))) {
|
||||
delete(app.controller.state.story_genre)
|
||||
app.controller.state.story_genre = strings.clone(g)
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Genre: %s", g))
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
// Art style chips
|
||||
styles := []string{"manga", "western-comic", "pixel-art", "watercolor", "noir", "chibi", "sketch", "cyberpunk"}
|
||||
for s in styles {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_style_%s", s))) {
|
||||
delete(app.controller.state.art_style)
|
||||
app.controller.state.art_style = strings.clone(s)
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Style: %s", s))
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
// Generate All Refs button (Characters screen)
|
||||
if clicked(clay.ID("btn_char_gen_all")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_generate_all_character_refs(&app.controller))
|
||||
app.is_dirty = true
|
||||
}
|
||||
// Generate All Panels button
|
||||
if clicked(clay.ID("btn_panels_gen_all")) {
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.fal_api_key) == 0 {
|
||||
push_status(&app.status_msg, &app.action_log, "FAL key missing")
|
||||
} else {
|
||||
push_status(&app.status_msg, &app.action_log, run_panels_action(&app.controller, nil, &app.is_dirty))
|
||||
}
|
||||
}
|
||||
// Page size chips (Layout screen)
|
||||
page_sizes := []core.Page_Size_Name{.A4, .Letter, .Manga, .Webtoon, .Square}
|
||||
for sz, pi in page_sizes {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_pgsize_%d", pi))) {
|
||||
app.controller.state.page_size = sz
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Page size: %v", sz))
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
// Layout pattern cards
|
||||
patterns := []string{"grid-2x2", "manga-3-tier", "action-dynamic", "splash-page", "webtoon-scroll", "western-3x3", "dialogue-heavy", "cinematic-widescreen"}
|
||||
for pat in patterns {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_pattern_%s", pat))) {
|
||||
if len(app.controller.state.panel_images) > 0 {
|
||||
panels := collect_script_panels(app.controller.state.script)
|
||||
core.dispose_page_layouts(&app.controller.state.page_layouts)
|
||||
app.controller.state.page_layouts = core.auto_layout_pages(panels, app.controller.state.page_size, app.controller.state.story_genre, pat)
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Layout: %s", pat))
|
||||
app.is_dirty = true
|
||||
} else {
|
||||
push_status(&app.status_msg, &app.action_log, "Generate panels first")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Audience chips
|
||||
auds := []string{"general", "children", "teen", "mature"}
|
||||
for a in auds {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_aud_%s", a))) {
|
||||
delete(app.controller.state.target_audience)
|
||||
app.controller.state.target_audience = strings.clone(a)
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Audience: %s", a))
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
if clicked(clay.ID("btn_help")) { toggle_help_overlay(&app.show_help_overlay) }
|
||||
if clicked(clay.ID("btn_toggle_sidebar")) {
|
||||
app.sidebar_collapsed = !app.sidebar_collapsed
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Sidebar: %s", "collapsed" if app.sidebar_collapsed else "expanded"))
|
||||
}
|
||||
if clicked(clay.ID("btn_fal_panels")) {
|
||||
if !has_fal_key { push_status(&app.status_msg, &app.action_log, "FAL key missing (set FAL_API_KEY)") }
|
||||
else { app.use_fal_panels = !app.use_fal_panels; push_status(&app.status_msg, &app.action_log, fmt.tprintf("FAL panels: %s", "ON" if app.use_fal_panels else "OFF")) }
|
||||
@@ -453,6 +642,15 @@ handle_workspace_nav :: proc(app: ^GUI_App_State) {
|
||||
screen := app.controller.active_screen
|
||||
if screen == .Script {
|
||||
page_count := len(app.controller.state.script.pages)
|
||||
// Collapsible toggles
|
||||
if clicked(clay.ID("btn_toggle_chars")) {
|
||||
app.summary_opts.script_show_all = !app.summary_opts.script_show_all
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Characters: %s", "shown" if app.summary_opts.script_show_all else "hidden"))
|
||||
}
|
||||
if clicked(clay.ID("btn_toggle_panels")) {
|
||||
app.summary_opts.script_desc = !app.summary_opts.script_desc
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Panels: %s", "shown" if app.summary_opts.script_desc else "hidden"))
|
||||
}
|
||||
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 }
|
||||
@@ -478,10 +676,12 @@ handle_workspace_nav :: proc(app: ^GUI_App_State) {
|
||||
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 }
|
||||
app.layout_selected_panel = -1
|
||||
}
|
||||
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 }
|
||||
app.layout_selected_panel = -1
|
||||
}
|
||||
}
|
||||
if screen == .Bubbles {
|
||||
@@ -519,17 +719,81 @@ handle_workspace_nav :: proc(app: ^GUI_App_State) {
|
||||
handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
||||
screen := app.controller.active_screen
|
||||
|
||||
if screen == .Panels {
|
||||
if clicked(clay.ID("btn_panel_regenerate")) {
|
||||
panel_count := count_script_panels(app.controller.state.script)
|
||||
if panel_count > 0 {
|
||||
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
panel, _, ok := panel_by_flat_index(app.controller.state.script, idx)
|
||||
if ok { push_status(&app.status_msg, &app.action_log, action_regenerate_panel(&app.controller, panel.panel_id)) }
|
||||
if screen == .Characters {
|
||||
char_count := len(app.controller.state.characters)
|
||||
|
||||
// Parse Descriptions button
|
||||
if clicked(clay.ID("btn_char_parse_all")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_parse_character_descriptions(&app.controller))
|
||||
app.is_dirty = true
|
||||
}
|
||||
|
||||
for i in 0..<char_count {
|
||||
char := app.controller.state.characters[i]
|
||||
is_editing := app.editing_char_id == char.id
|
||||
|
||||
if is_editing {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_char_save_%s", char.id))) {
|
||||
push_status(&app.status_msg, &app.action_log, action_update_character_desc(&app.controller, char.id, strings.clone(app.char_edit_text)))
|
||||
app.editing_char_id = ""
|
||||
app.selected_field = 0
|
||||
app.is_dirty = true
|
||||
}
|
||||
} else {
|
||||
if clicked(clay.ID(fmt.tprintf("btn_char_edit_%s", char.id))) {
|
||||
app.editing_char_id = char.id
|
||||
app.char_edit_text = char.description
|
||||
app.selected_field = 8
|
||||
}
|
||||
}
|
||||
|
||||
if clicked(clay.ID(fmt.tprintf("btn_char_ref_%s", char.id))) {
|
||||
push_status(&app.status_msg, &app.action_log, action_generate_character_reference(&app.controller, char.id))
|
||||
}
|
||||
if clicked(clay.ID(fmt.tprintf("btn_char_sheet_%s", char.id))) {
|
||||
push_status(&app.status_msg, &app.action_log, action_generate_character_sheet(&app.controller, char.id))
|
||||
}
|
||||
if clicked(clay.ID("field_char_desc")) {
|
||||
if is_editing {
|
||||
app.selected_field = 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if screen == .Panels {
|
||||
panel_count := count_script_panels(app.controller.state.script)
|
||||
if panel_count > 0 {
|
||||
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
panel, _, ok := panel_by_flat_index(app.controller.state.script, idx)
|
||||
if ok {
|
||||
is_editing := app.editing_panel_id == panel.panel_id
|
||||
|
||||
if is_editing {
|
||||
if clicked(clay.ID("btn_panel_save")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_update_panel_prompt(&app.controller, panel.panel_id, strings.clone(app.panel_edit_text)))
|
||||
app.editing_panel_id = ""
|
||||
app.selected_field = 0
|
||||
app.is_dirty = true
|
||||
}
|
||||
} else {
|
||||
if clicked(clay.ID("btn_panel_edit")) {
|
||||
app.editing_panel_id = panel.panel_id
|
||||
app.panel_edit_text = panel.description
|
||||
app.selected_field = 9
|
||||
}
|
||||
}
|
||||
|
||||
if clicked(clay.ID("btn_panel_regenerate")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_regenerate_panel(&app.controller, panel.panel_id))
|
||||
}
|
||||
|
||||
if clicked(clay.ID("field_panel_desc")) || clicked(clay.ID("field_panel_desc_missing")) || clicked(clay.ID("field_panel_desc_no_img")) {
|
||||
if is_editing {
|
||||
app.selected_field = 9
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..<panel_count {
|
||||
if clicked(clay.ID(fmt.tprintf("panel_row_%d", i))) {
|
||||
app.summary_opts.panel_cursor = i
|
||||
@@ -548,9 +812,24 @@ handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
||||
for i in 0..<layout_count {
|
||||
if clicked(clay.ID(fmt.tprintf("layout_row_%d", i))) {
|
||||
app.summary_opts.layout_page_cursor = i
|
||||
app.layout_selected_panel = -1
|
||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Viewing layout page %d/%d", i+1, layout_count))
|
||||
}
|
||||
}
|
||||
if app.wireframe_cell_count > 0 && rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := rl.GetMousePosition()
|
||||
for i in 0..<app.wireframe_cell_count {
|
||||
r := app.wireframe_cell_rects[i]
|
||||
if mouse.x >= r.x && mouse.x <= r.x + r.width && mouse.y >= r.y && mouse.y <= r.y + r.height {
|
||||
if app.layout_selected_panel == i {
|
||||
app.layout_selected_panel = -1
|
||||
} else {
|
||||
app.layout_selected_panel = i
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if screen == .Bubbles {
|
||||
@@ -634,12 +913,30 @@ handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
b := app.controller.state.speech_bubbles[panel_id][app.summary_opts.bubble_edit_cursor]
|
||||
if clicked(clay.ID("btn_bubble_left")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x - 0.05, b.position.y))
|
||||
app.is_dirty = true
|
||||
}
|
||||
if clicked(clay.ID("btn_bubble_right")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x + 0.05, b.position.y))
|
||||
app.is_dirty = true
|
||||
}
|
||||
if clicked(clay.ID("btn_bubble_up")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x, b.position.y - 0.05))
|
||||
app.is_dirty = true
|
||||
}
|
||||
if clicked(clay.ID("btn_bubble_down")) {
|
||||
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x, b.position.y + 0.05))
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek, has_fal_key: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool) {
|
||||
process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek, has_fal_key: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool, bp: shared.Breakpoint) {
|
||||
handle_nav_clicks(app)
|
||||
handle_field_clicks(app)
|
||||
handle_format_clicks(app, has_deepseek)
|
||||
@@ -669,83 +966,86 @@ process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_expo
|
||||
}
|
||||
}
|
||||
}
|
||||
// ─── Help Overlay (Clay floating) ──────────────────────────────────
|
||||
declare_help_overlay :: proc() {
|
||||
// Backdrop
|
||||
// ─── Help Overlay (Clay floating, animated alpha) ──────────────────
|
||||
declare_help_overlay :: proc(bp: shared.Breakpoint, alpha: f32 = 1) {
|
||||
if clay.UI(clay.ID("HelpBackdrop"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
||||
backgroundColor = {0, 0, 0, 180},
|
||||
backgroundColor = {0, 0, 0, f32(200 * alpha)},
|
||||
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 5},
|
||||
}) {}
|
||||
|
||||
// Card
|
||||
card_w_pct: f32 = 0.7
|
||||
card_h_pct: f32 = 0.8
|
||||
if bp == .Compact { card_w_pct = 0.92; card_h_pct = 0.92 }
|
||||
if clay.UI(clay.ID("HelpCard"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(860), height = clay.SizingFixed(642)}, layoutDirection = .TopToBottom, padding = {top = 28, right = 30, bottom = 28, left = 30}, childGap = 16},
|
||||
layout = {sizing = {width = clay.SizingPercent(card_w_pct), height = clay.SizingPercent(card_h_pct)}, layoutDirection = .TopToBottom, padding = {top = 24, right = 28, bottom = 24, left = 28}, childGap = CLAY_SPACE_12},
|
||||
backgroundColor = CLAY_BG_CARD,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(2)},
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XL),
|
||||
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(1)},
|
||||
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .CenterCenter, parent = .CenterCenter}, zIndex = 10},
|
||||
}) {
|
||||
clay_title_text("Keyboard Shortcuts", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_2XL)
|
||||
if clay.UI(clay.ID("HelpHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_title_text("Keyboard Shortcuts", color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_XL)
|
||||
clay.UI(clay.ID("HelpHeaderSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
clay_body_text("Esc or /", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
|
||||
clay_body_text("Navigation", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("1..8 screens | TAB fields | click to focus | F11 pages | F12 project")
|
||||
declare_divider("HelpDiv1")
|
||||
|
||||
clay_body_text("Core Actions", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("F5 script F6 panels F7 layout F8 export F9 next F10 auto-all")
|
||||
clay_muted_text("Ctrl+S save | Ctrl+O open | Ctrl+N new | Ctrl+E export | Ctrl+H/J summary")
|
||||
clay_label_text("NAVIGATION")
|
||||
clay_body_text("1..8 screens | TAB fields | click to focus | F11 pages | F12 project", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
clay_body_text("Clipboard + Logs", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("Ctrl+L clear log | Ctrl+Shift+L copy log | Ctrl+Shift+T/B log view | Ctrl+Shift+Z reset")
|
||||
clay_muted_text("Ctrl+Shift+C status | Ctrl+Shift+Y diag copy | Ctrl+Shift+R diag file | Ctrl+Shift+W report")
|
||||
clay_muted_text("Ctrl+0 reset helpers | Ctrl+V paste | Ctrl+Shift+I copy | Ctrl+Backspace clear")
|
||||
clay_label_text("CORE ACTIONS")
|
||||
clay_body_text("F5 script F6 panels F7 layout F8 export F9 next F10 auto-all", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text("Ctrl+S save | Ctrl+O open | Ctrl+N new | Ctrl+E export | Ctrl+H/J summary", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
clay_body_text("Paths", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("Ctrl+Shift+X copy export | P preset | D exp-from-project | G proj-from-export")
|
||||
clay_muted_text("Ctrl+Shift+J fix project | F fix export | K/M quick-fix P/E | U fix all")
|
||||
clay_label_text("CLIPBOARD + LOGS")
|
||||
clay_body_text("Ctrl+L clear log | Ctrl+V paste | Ctrl+0 reset helpers | Ctrl+Backspace clear", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
clay_body_text("Autosave", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("Ctrl+Shift+A toggle | Ctrl+-/= adjust | Ctrl+7/8/9 set 15/30/60")
|
||||
clay_label_text("PATHS")
|
||||
clay_body_text("Ctrl+Shift+X copy export | P preset | D exp-from-project | G proj-from-export", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
clay_body_text("Safety", color = CLAY_ACCENT, size = CLAY_FONT_SIZE_MD)
|
||||
clay_muted_text("Dirty guard: Shift-click New/Open | Keyboard confirm: Ctrl+Shift+N / Ctrl+Shift+O")
|
||||
clay_label_text("AUTOSAVE")
|
||||
clay_body_text("Ctrl+Shift+A toggle | Ctrl+-/= adjust | Ctrl+7/8/9 set 15/30/60", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
clay_body_text("Close help: Esc or /", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_MD)
|
||||
clay_label_text("SAFETY")
|
||||
clay_body_text("Dirty guard: Shift-click New/Open | Ctrl+Shift+N / Ctrl+Shift+O", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Confirm Overlay (Clay floating) ────────────────────────────────
|
||||
declare_confirm_overlay :: proc(action: Pending_Confirm_Action) {
|
||||
// Backdrop
|
||||
// ─── Confirm Overlay (Clay floating, animated alpha) ────────────────
|
||||
declare_confirm_overlay :: proc(action: Pending_Confirm_Action, bp: shared.Breakpoint, alpha: f32 = 1) {
|
||||
if clay.UI(clay.ID("ConfirmBackdrop"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
||||
backgroundColor = {0, 0, 0, 180},
|
||||
backgroundColor = {0, 0, 0, f32(200 * alpha)},
|
||||
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 15},
|
||||
}) {}
|
||||
|
||||
// Card
|
||||
if clay.UI(clay.ID("ConfirmCard"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(520), height = clay.SizingFixed(230)}, layoutDirection = .TopToBottom, padding = {top = 34, right = 30, bottom = 24, left = 30}, childGap = 12},
|
||||
layout = {sizing = {width = clay.SizingPercent(0.6), height = clay.SizingFit({min = 160, max = 380})}, layoutDirection = .TopToBottom, padding = {top = 24, right = 28, bottom = 24, left = 28}, childGap = CLAY_SPACE_12},
|
||||
backgroundColor = CLAY_BG_CARD,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_LG),
|
||||
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(2)},
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XL),
|
||||
border = {color = CLAY_ERROR_DIM, width = clay.BorderOutside(2)},
|
||||
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .CenterCenter, parent = .CenterCenter}, zIndex = 20},
|
||||
}) {
|
||||
// Accent bar at top
|
||||
if clay.UI(clay.ID("ConfirmAccentBar"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(8)}},
|
||||
backgroundColor = CLAY_ACCENT,
|
||||
cornerRadius = clay.CornerRadiusAll(4),
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(3)}},
|
||||
backgroundColor = CLAY_ERROR,
|
||||
cornerRadius = clay.CornerRadiusAll(2),
|
||||
}) {}
|
||||
|
||||
clay_title_text("Confirm destructive action", color = CLAY_ERROR, size = CLAY_FONT_SIZE_XL)
|
||||
clay_title_text("Confirm Action", color = CLAY_ERROR, size = CLAY_FONT_SIZE_XL)
|
||||
action_label := "reset"
|
||||
if action == .Open_Project { action_label = "open a different project" }
|
||||
clay_body_text(fmt.tprintf("You have unsaved changes. Do you want to %s?", action_label), color = CLAY_TEXT_SECONDARY)
|
||||
clay_body_text(fmt.tprintf("You have unsaved changes. Do you want to %s?", action_label), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if clay.UI(clay.ID("ConfirmBtnRow"))({layout = clay_row_layout()}) {
|
||||
declare_button_danger("confirm_yes", "Confirm")
|
||||
declare_button_soft("confirm_no", "Cancel")
|
||||
}
|
||||
clay_muted_text("Enter/Y confirm | Esc/N cancel")
|
||||
clay_body_text("Enter/Y confirm | Esc/N cancel", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,13 +1063,130 @@ declare_toast :: proc(log: ^Action_Log) {
|
||||
else if !is_error_message(msg) { bg = CLAY_SUCCESS }
|
||||
|
||||
if clay.UI(clay.ID("Toast"))({
|
||||
layout = {sizing = {width = clay.SizingFit({min = 300, max = 0}), height = clay.SizingFixed(34)}, padding = {top = 6, right = 12, bottom = 6, left = 12}, childAlignment = {y = .Center}},
|
||||
layout = {sizing = {width = clay.SizingFit({min = 280, max = 0}), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM))}, padding = {top = 4, right = 12, bottom = 4, left = 12}, childAlignment = {y = .Center}},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = clay.Color{255, 255, 255, 40}, width = clay.BorderOutside(1)},
|
||||
floating = {offset = {f32(shared.LAYOUT.sidebar_width + 8), 70}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 30},
|
||||
floating = {offset = {f32(228), 70}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 30},
|
||||
}) {
|
||||
clay_body_text(msg, color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text(msg, color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Raygui Field Overlay ──────────────────────────────────────────
|
||||
field_id_for_index :: proc(idx: int) -> string {
|
||||
switch idx {
|
||||
case 0: return "field_idea"
|
||||
case 1: return "field_genre"
|
||||
case 2: return "field_audience"
|
||||
case 3: return "field_export"
|
||||
case 4: return "field_pages"
|
||||
case 5: return "field_project"
|
||||
case 6: return "field_autosave"
|
||||
case 7: return "field_bubble_text"
|
||||
case 8: return "field_char_desc"
|
||||
case 9: return "field_panel_prompt"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
field_ptr_for_index :: proc(app: ^GUI_App_State, idx: int) -> ^string {
|
||||
switch idx {
|
||||
case 0: return &app.controller.state.story_idea
|
||||
case 1: return &app.controller.state.story_genre
|
||||
case 2: return &app.controller.state.target_audience
|
||||
case 3: return &app.export_path
|
||||
case 4: return &app.local_script_pages
|
||||
case 5: return &app.project_path
|
||||
case 6: return &app.autosave_interval_text
|
||||
case 7: return &app.bubble_edit_text
|
||||
case 8: return &app.char_edit_text
|
||||
case 9: return &app.panel_edit_text
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
render_raygui_fields :: proc(app: ^GUI_App_State) {
|
||||
sf := app.selected_field
|
||||
fid := field_id_for_index(sf)
|
||||
if len(fid) == 0 { return }
|
||||
|
||||
eid := clay.GetElementId(clay.MakeString(fid))
|
||||
data := clay.GetElementData(eid)
|
||||
if !data.found { return }
|
||||
|
||||
bounds := rl.Rectangle {
|
||||
x = f32(data.boundingBox.x),
|
||||
y = f32(data.boundingBox.y),
|
||||
width = f32(data.boundingBox.width),
|
||||
height = f32(data.boundingBox.height),
|
||||
}
|
||||
|
||||
fp := field_ptr_for_index(app, sf)
|
||||
if fp == nil { return }
|
||||
|
||||
// Copy field string → buffer for GuiTextBox
|
||||
src := fp^
|
||||
if len(src) >= FIELD_BUF_SIZE { src = src[:FIELD_BUF_SIZE-1] }
|
||||
for i in 0 ..< len(src) {
|
||||
app.field_buf[i] = src[i]
|
||||
}
|
||||
app.field_buf[len(src)] = 0
|
||||
|
||||
// Call raygui GuiTextBox — handles rendering + input, modifies buffer in-place
|
||||
rl.GuiTextBox(bounds, cstring(&app.field_buf[0]), c.int(FIELD_BUF_SIZE), true)
|
||||
|
||||
// Sync buffer → field string (pick up GuiTextBox edits)
|
||||
new_len := 0
|
||||
for app.field_buf[new_len] != 0 && new_len < FIELD_BUF_SIZE-1 {
|
||||
new_len += 1
|
||||
}
|
||||
if new_len != len(src) {
|
||||
fp^ = string(app.field_buf[:new_len])
|
||||
app.is_dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Animation Updates ─────────────────────────────────────────────
|
||||
lerp_f32 :: proc(a, b, t: f32) -> f32 {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
smooth_towards :: proc(current, target, dt, duration: f32) -> f32 {
|
||||
if abs(current - target) < 0.5 { return target }
|
||||
k := 1 - math.pow_f32(0.01, dt / duration)
|
||||
return lerp_f32(current, target, k)
|
||||
}
|
||||
|
||||
update_sidebar_anim :: proc(app: ^GUI_App_State, bp: shared.Breakpoint, dt: f32) {
|
||||
target_w := f32(sidebar_width(bp, app.sidebar_collapsed, 0))
|
||||
app.sidebar_anim = smooth_towards(app.sidebar_anim, target_w, dt, ANIM_DURATION_SIDEBAR)
|
||||
}
|
||||
|
||||
update_overlay_anim :: proc(app: ^GUI_App_State, dt: f32) {
|
||||
target_alpha: f32 = 0
|
||||
if app.show_help_overlay || app.show_confirm_overlay {
|
||||
target_alpha = 1
|
||||
}
|
||||
app.overlay_alpha = smooth_towards(app.overlay_alpha, target_alpha, dt, ANIM_DURATION_OVERLAY)
|
||||
}
|
||||
|
||||
update_slide_anim :: proc(app: ^GUI_App_State, dt: f32) {
|
||||
current := app.controller.active_screen
|
||||
if current != app.prev_screen && app.slide_progress >= 1 {
|
||||
app.slide_progress = 0
|
||||
}
|
||||
if app.slide_progress < 1 {
|
||||
app.slide_progress += dt / ANIM_DURATION_SLIDE
|
||||
if app.slide_progress > 1 {
|
||||
app.slide_progress = 1
|
||||
app.slide_offset = 0
|
||||
app.prev_screen = current
|
||||
} else {
|
||||
// Ease-out cubic: offset goes from 60 → 0
|
||||
t := app.slide_progress
|
||||
eased := 1 - (1 - t) * (1 - t) * (1 - t)
|
||||
app.slide_offset = 60 * (1 - eased)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package gui
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
BG_BASE :: rl.Color{13, 13, 18, 255}
|
||||
@@ -1,5 +1,17 @@
|
||||
package gui
|
||||
|
||||
import "../shared"
|
||||
|
||||
// sidebar_width returns the effective sidebar width, using animated value when available.
|
||||
sidebar_width :: proc(bp: shared.Breakpoint, collapsed: bool, anim: f32 = 0) -> i32 {
|
||||
if collapsed {
|
||||
if anim > 0 { return i32(anim) }
|
||||
return shared.LAYOUT.sidebar_collapsed
|
||||
}
|
||||
if anim > 0 { return i32(anim) }
|
||||
return shared.sidebar_width_for_breakpoint(bp)
|
||||
}
|
||||
|
||||
Summary_View_Options :: struct {
|
||||
script_show_all: bool,
|
||||
script_desc: bool,
|
||||
|
||||
+336
-129
@@ -3,29 +3,25 @@ package gui
|
||||
import clay "clay:."
|
||||
import "core:fmt"
|
||||
import "../core"
|
||||
|
||||
// ─── Shared Helpers ─────────────────────────────────────────────
|
||||
import "../shared"
|
||||
|
||||
workspace_nav :: proc(id_prefix, pos_label: string) {
|
||||
if clay.UI(clay.ID(fmt.tprintf("%sNav", id_prefix)))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(pos_label, color = CLAY_ACCENT)
|
||||
clay_body_text(pos_label, color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
clay.UI(clay.ID(fmt.tprintf("%sNavSpacer", id_prefix)))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_small(fmt.tprintf("btn_%s_prev", id_prefix), "< Prev")
|
||||
declare_button_small(fmt.tprintf("btn_%s_next", id_prefix), "Next >")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
}
|
||||
declare_empty_state("ScriptEmpty", "No Script Yet",
|
||||
"Go to Story screen to set up your story idea",
|
||||
"Generate Script (F5) to create pages")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,49 +35,184 @@ declare_script_workspace :: proc(app: ^GUI_App_State) {
|
||||
declare_script_detail(app)
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
}
|
||||
declare_empty_state("PanelsEmpty", "No Panels Yet",
|
||||
"Generate a script first, then create panel images",
|
||||
"Generate Panels (F6) after having a script")
|
||||
return
|
||||
}
|
||||
|
||||
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
workspace_nav("panels", fmt.tprintf("Panel %d/%d", idx+1, panel_count))
|
||||
declare_panels_detail(app)
|
||||
generated_count := 0
|
||||
for _, _ in app.controller.state.panel_images { generated_count += 1 }
|
||||
|
||||
if clay.UI(clay.ID("PanelsHeader"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("%d of %d panels generated", generated_count, panel_count), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
clay.UI(clay.ID("PanelsHeaderSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_primary("btn_panels_gen_all", "Generate All")
|
||||
}
|
||||
|
||||
idx_cur := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
||||
if clay.UI(clay.ID("PanelGrid"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, childGap = CLAY_SPACE_12},
|
||||
}) {
|
||||
if clay.UI(clay.ID("PanelGridLeft"))({
|
||||
layout = {sizing = {width = clay.SizingPercent(0.55), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 4},
|
||||
}) {
|
||||
workspace_nav("panels", fmt.tprintf("Panel %d/%d", idx_cur+1, panel_count))
|
||||
panel, page_num, rok := panel_by_flat_index(app.controller.state.script, idx_cur)
|
||||
if rok {
|
||||
declare_panel_card(app, panel, page_num, idx_cur, panel_count)
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PanelGridRight"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
clip = {vertical = true, childOffset = clay.GetScrollOffset()},
|
||||
}) {
|
||||
clay_label_text("ALL PANELS")
|
||||
for ri := 0; ri < panel_count; ri += 1 {
|
||||
row_panel, row_page, row_ok := panel_by_flat_index(app.controller.state.script, ri)
|
||||
if !row_ok { continue }
|
||||
is_cur := ri == idx_cur
|
||||
is_hov := clay.Hovered()
|
||||
row_color := CLAY_TEXT_TERTIARY
|
||||
if is_cur { row_color = CLAY_TEXT_BRIGHT }
|
||||
else if is_hov { row_color = CLAY_TEXT_PRIMARY }
|
||||
ready_mark := "✗"
|
||||
ready_color := CLAY_TEXT_TERTIARY
|
||||
if _, ex := app.controller.state.panel_images[row_panel.panel_id]; ex { ready_mark = "✓"; ready_color = CLAY_SUCCESS }
|
||||
if _, er := app.controller.state.panel_errors[row_panel.panel_id]; er { ready_mark = "!"; ready_color = CLAY_ERROR }
|
||||
|
||||
row_bg := clay.Color{0, 0, 0, 0}
|
||||
if is_cur { row_bg = CLAY_BG_SELECTED }
|
||||
else if is_hov { row_bg = CLAY_BG_HOVER }
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_row_%d", ri)))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(24)}, padding = {top = 2, right = 6, bottom = 2, left = 6}, childGap = 6, childAlignment = {y = .Center}},
|
||||
backgroundColor = row_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XS),
|
||||
}) {
|
||||
clay.Text(ready_mark, {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = ready_color})
|
||||
clay.Text(fmt.tprintf("%02d p%d#%d", ri+1, row_page, row_panel.panel_number), {fontId = CLAY_FONT_MONO, fontSize = CLAY_FONT_SIZE_XS, textColor = row_color})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare_panel_card :: proc(app: ^GUI_App_State, panel: core.Panel, page_num, panel_idx, total: int) {
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_%s", panel.panel_id)))(clay_card_style()) {
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_top_%s", panel.panel_id)))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Panel %d/%d", panel_idx+1, total), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
clay_muted_text(fmt.tprintf("Page %d", page_num))
|
||||
}
|
||||
|
||||
status := "missing"
|
||||
status_color := CLAY_WARNING
|
||||
if _, has := app.controller.state.panel_images[panel.panel_id]; has { status = "ready"; status_color = CLAY_SUCCESS }
|
||||
if _, er := app.controller.state.panel_errors[panel.panel_id]; er { status = "error"; status_color = CLAY_ERROR }
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_badges_%s", panel.panel_id)))({layout = clay_row_layout()}) {
|
||||
declare_status_badge(fmt.tprintf("panel_status_%s", panel.panel_id), status, status == "ready")
|
||||
declare_status_badge(fmt.tprintf("panel_shot_%s", panel.panel_id), shot_type_label(panel.shot_type), true)
|
||||
}
|
||||
|
||||
desc := panel.description
|
||||
if len(desc) == 0 { desc = "(no description)" }
|
||||
clay_body_text(desc, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
|
||||
if len(panel.characters_present) > 0 {
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_chars_%s", panel.panel_id)))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4}}) {
|
||||
for cid in panel.characters_present {
|
||||
declare_nav_chip(fmt.tprintf("char_tag_%s", cid), cid, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_actions_%s", panel.panel_id)))({layout = clay_row_layout()}) {
|
||||
if _, has := app.controller.state.panel_images[panel.panel_id]; has {
|
||||
declare_button_small("btn_panel_regenerate", "Regen")
|
||||
} else {
|
||||
declare_button_small("btn_panel_regenerate", "Generate")
|
||||
}
|
||||
if app.editing_panel_id == panel.panel_id {
|
||||
declare_button_small("btn_panel_save", "Save Prompt")
|
||||
} else {
|
||||
declare_button_small("btn_panel_edit", "Edit Prompt")
|
||||
}
|
||||
}
|
||||
|
||||
if panel_img, has_img := app.controller.state.panel_images[panel.panel_id]; has_img {
|
||||
img_url := pool_clone(panel_img.url)
|
||||
_, loaded := load_panel_texture(&app.panel_textures, panel.panel_id, img_url)
|
||||
if loaded {
|
||||
tex_ptr := &app.panel_textures[panel.panel_id]
|
||||
if tex_ptr.id != 0 {
|
||||
if clay.UI(clay.ID(fmt.tprintf("panel_card_img_%s", panel.panel_id)))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(160)}},
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
image = {imageData = rawptr(tex_ptr)},
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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
|
||||
}
|
||||
declare_section_header("LayoutHeader", "Layout", "Arrange panels into comic pages")
|
||||
|
||||
idx := clamp_layout_cursor(layout_count, app.summary_opts.layout_page_cursor)
|
||||
workspace_nav("layout", fmt.tprintf("Page %d/%d", idx+1, layout_count))
|
||||
declare_layout_detail(app)
|
||||
clay_label_text("PAGE SIZE")
|
||||
if clay.UI(clay.ID("PageSizeChips"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4}}) {
|
||||
sizes := []core.Page_Size_Name{.A4, .Letter, .Manga, .Webtoon, .Square}
|
||||
names := []string{"A4", "Letter", "Manga", "Webtoon", "Square"}
|
||||
for s, i in sizes {
|
||||
is_sel := app.controller.state.page_size == s
|
||||
declare_nav_chip(fmt.tprintf("btn_pgsize_%d", i), names[i], is_sel)
|
||||
}
|
||||
}
|
||||
|
||||
clay_label_text("LAYOUT PATTERN")
|
||||
if clay.UI(clay.ID("PatternCards"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 6}}) {
|
||||
patterns := []string{"grid-2x2", "manga-3-tier", "action-dynamic", "splash-page", "webtoon-scroll", "western-3x3", "dialogue-heavy", "cinematic-widescreen"}
|
||||
pnames := []string{"Grid", "Manga", "Action", "Splash", "Webtoon", "Western", "Dialogue", "Cinematic"}
|
||||
for p, j in patterns {
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_BG_STRIP
|
||||
if is_hov { bg = CLAY_BG_HOVER }
|
||||
if clay.UI(clay.ID(fmt.tprintf("btn_pattern_%s", p)))({
|
||||
layout = {layoutDirection = .TopToBottom, sizing = {width = clay.SizingFixed(88), height = clay.SizingFixed(52)}, padding = {top = 6, right = 6, bottom = 6, left = 6}, childAlignment = {x = .Center}, childGap = 2},
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(pnames[j], color = CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay.Text(fmt.tprintf("%d max", core.pattern_max_panels(p)), {fontId = CLAY_FONT_BODY, fontSize = CLAY_FONT_SIZE_XS, textColor = CLAY_TEXT_TERTIARY})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx := clamp_layout_cursor(layout_count, app.summary_opts.layout_page_cursor)
|
||||
workspace_nav("layout", fmt.tprintf("Page %d/%d", idx+1, layout_count))
|
||||
declare_layout_detail(app)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
}
|
||||
declare_empty_state("BubblesEmpty", "No Bubbles Yet",
|
||||
"Run Layout Auto first to create page layouts",
|
||||
"Then use this screen to edit speech bubbles")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,11 +220,12 @@ declare_bubbles_workspace :: proc(app: ^GUI_App_State) {
|
||||
layout_val := app.controller.state.page_layouts[page_idx]
|
||||
panel_count := len(layout_val.panels)
|
||||
|
||||
// Bubbles needs dual nav (page + panel)
|
||||
if clay.UI(clay.ID("BubblesNav"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(36)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, layoutDirection = .LeftToRight, childGap = 6, childAlignment = {y = .Center}},
|
||||
}) {
|
||||
clay_body_text(fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), color = CLAY_ACCENT)
|
||||
clay_body_text(fmt.tprintf("Page %d/%d", page_idx+1, layout_count), color = CLAY_ACCENT, size = CLAY_FONT_SIZE_SM)
|
||||
clay_muted_text(fmt.tprintf("%d panels", panel_count))
|
||||
clay.UI(clay.ID("BubblesNavSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
declare_button_small("btn_bubbles_prev_page", "< Page")
|
||||
declare_button_small("btn_bubbles_next_page", "Page >")
|
||||
declare_button_small("btn_bubbles_prev_panel", "< Panel")
|
||||
@@ -103,89 +235,108 @@ declare_bubbles_workspace :: proc(app: ^GUI_App_State) {
|
||||
declare_bubbles_detail(app)
|
||||
}
|
||||
|
||||
// ─── Story Workspace ──────────────────────────────────────────────
|
||||
|
||||
// ─── Story Workspace ──────────────────────────────────────────────
|
||||
declare_story_workspace :: proc(app: ^GUI_App_State) {
|
||||
declare_story_workspace :: proc(app: ^GUI_App_State, bp: shared.Breakpoint) {
|
||||
if clay.UI(clay.ID("StoryCard"))(clay_card_style()) {
|
||||
clay_title_text("Story Setup")
|
||||
declare_section_header("StoryHeader", "Story Setup", "Define your comic's premise")
|
||||
|
||||
clay_muted_text("Story Idea")
|
||||
clay_label_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}},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({min = 80, max = 0})}, padding = {top = 10, right = 12, bottom = 10, left = 12}},
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_INPUT_BORDER if app.selected_field != 0 else CLAY_BORDER_FOCUS, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(app.controller.state.story_idea if len(app.controller.state.story_idea) > 0 else "Enter story idea...")
|
||||
placeholder := len(app.controller.state.story_idea) == 0
|
||||
clay_body_text(
|
||||
app.controller.state.story_idea if !placeholder else "Enter your story idea...",
|
||||
color = CLAY_TEXT_TERTIARY if placeholder else CLAY_TEXT_PRIMARY,
|
||||
size = CLAY_FONT_SIZE_SM,
|
||||
)
|
||||
}
|
||||
|
||||
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...")
|
||||
}
|
||||
clay_label_text("GENRE")
|
||||
if clay.UI(clay.ID("GenreChips"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4},
|
||||
clip = {horizontal = true, childOffset = clay.GetScrollOffset()},
|
||||
}) {
|
||||
genres := []string{"action", "comedy", "drama", "scifi", "fantasy", "horror", "romance", "mystery", "slice-of-life"}
|
||||
for g in genres {
|
||||
is_sel := app.controller.state.story_genre == g
|
||||
declare_nav_chip(fmt.tprintf("btn_genre_%s", g), g, is_sel)
|
||||
}
|
||||
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...")
|
||||
}
|
||||
}
|
||||
|
||||
clay_label_text("ART STYLE")
|
||||
if clay.UI(clay.ID("ArtStyleChips"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4},
|
||||
clip = {horizontal = true, childOffset = clay.GetScrollOffset()},
|
||||
}) {
|
||||
styles := []string{"manga", "western-comic", "pixel-art", "watercolor", "noir", "chibi", "sketch", "cyberpunk"}
|
||||
for s in styles {
|
||||
is_sel := app.controller.state.art_style == s
|
||||
declare_nav_chip(fmt.tprintf("btn_style_%s", s), s, is_sel)
|
||||
}
|
||||
}
|
||||
|
||||
clay_label_text("AUDIENCE")
|
||||
if clay.UI(clay.ID("AudienceChips"))({
|
||||
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4},
|
||||
clip = {horizontal = true, childOffset = clay.GetScrollOffset()},
|
||||
}) {
|
||||
auds := []string{"general", "children", "teen", "mature"}
|
||||
for a in auds {
|
||||
is_sel := app.controller.state.target_audience == a
|
||||
declare_nav_chip(fmt.tprintf("btn_aud_%s", a), a, is_sel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("PathCard"))(clay_card_style()) {
|
||||
clay_title_text("Project Paths")
|
||||
declare_section_header("PathHeader", "Project Paths", "Configure output locations")
|
||||
|
||||
// Export Path with Browse button
|
||||
clay_muted_text("Export Path")
|
||||
clay_label_text("EXPORT PATH")
|
||||
if clay.UI(clay.ID("ExportPathRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}}}) {
|
||||
if clay.UI(clay.ID("field_export"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_INPUT_BORDER if app.selected_field != 3 else CLAY_BORDER_FOCUS, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
display := app.export_path
|
||||
if len(display) == 0 { display = "(not set)" }
|
||||
clay_body_text(display)
|
||||
clay_body_text(display, color = CLAY_TEXT_SECONDARY if len(app.export_path) > 0 else CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
declare_button_small("btn_browse_export", "Browse")
|
||||
}
|
||||
|
||||
// Project Path with Browse button
|
||||
clay_muted_text("Project Path")
|
||||
clay_label_text("PROJECT PATH")
|
||||
if clay.UI(clay.ID("ProjectPathRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 4, childAlignment = {y = .Center}}}) {
|
||||
if clay.UI(clay.ID("field_project"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_INPUT_BORDER if app.selected_field != 5 else CLAY_BORDER_FOCUS, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
display := app.project_path
|
||||
if len(display) == 0 { display = "(not set)" }
|
||||
clay_body_text(display)
|
||||
clay_body_text(display, color = CLAY_TEXT_SECONDARY if len(app.project_path) > 0 else CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
declare_button_small("btn_browse_project", "Browse")
|
||||
}
|
||||
|
||||
declare_divider("PathDivider")
|
||||
|
||||
if clay.UI(clay.ID("StoryConfigRow"))({layout = clay_row_layout()}) {
|
||||
clay_muted_text("Pages")
|
||||
clay_label_text("PAGES")
|
||||
if clay.UI(clay.ID("field_pages"))({
|
||||
layout = {sizing = {width = clay.SizingFixed(60), height = clay.SizingFixed(34)}},
|
||||
layout = {sizing = {width = clay.SizingFixed(60), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM))}},
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_INPUT_BORDER if app.selected_field != 4 else CLAY_BORDER_FOCUS, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(app.local_script_pages)
|
||||
clay_body_text(app.local_script_pages, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
clay_muted_text("Format")
|
||||
clay_label_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)
|
||||
@@ -193,31 +344,87 @@ declare_story_workspace :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Characters Workspace ─────────────────────────────────────────
|
||||
|
||||
// ─── Characters Workspace ─────────────────────────────────────────
|
||||
declare_characters_workspace :: proc(app: ^GUI_App_State) {
|
||||
char_count := len(app.controller.state.characters)
|
||||
script_char_count := len(app.controller.state.script.characters)
|
||||
|
||||
if clay.UI(clay.ID("CharsCard"))(clay_card_style()) {
|
||||
clay_title_text("Characters")
|
||||
char_count := len(app.controller.state.characters)
|
||||
script_char_count := len(app.controller.state.script.characters)
|
||||
// Show both state.characters and script.characters for debugging
|
||||
declare_section_header("CharsHeader", "Characters", fmt.tprintf("%d characters defined", char_count))
|
||||
|
||||
if char_count == 0 {
|
||||
clay_body_text(fmt.tprintf("State chars: %d, Script chars: %d", char_count, script_char_count), color = CLAY_TEXT_PRIMARY)
|
||||
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)
|
||||
clay_body_text("No characters yet.", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text("Characters are extracted when you generate a script.", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
} else {
|
||||
clay_body_text(fmt.tprintf("%d characters found", char_count), color = CLAY_ACCENT)
|
||||
with_refs := 0
|
||||
with_templates := 0
|
||||
for c in app.controller.state.characters {
|
||||
if len(c.reference_image_url) > 0 { with_refs += 1 }
|
||||
if len(c.prompt_template.age) > 0 || len(c.prompt_template.gender) > 0 { with_templates += 1 }
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("CharStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12}}) {
|
||||
declare_stat_chip("stat_chars", "Total", char_count)
|
||||
declare_stat_chip("stat_refs", "Refs", with_refs)
|
||||
declare_stat_chip("stat_tmpl", "Parsed", with_templates)
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("CharActionsRow"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8}}) {
|
||||
declare_button_small("btn_char_parse_all", "Parse Descriptions")
|
||||
declare_button_primary("btn_char_gen_all", "Generate All Refs")
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID("CharsScroll"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 6},
|
||||
clip = {vertical = true, childOffset = clay.GetScrollOffset()},
|
||||
}) {
|
||||
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},
|
||||
is_editing := app.editing_char_id == char.id
|
||||
is_hov := clay.Hovered()
|
||||
|
||||
card_bg := CLAY_BG_CARD_ALT
|
||||
if is_editing { card_bg = CLAY_BG_SELECTED }
|
||||
else if is_hov { card_bg = CLAY_BG_HOVER }
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("char_card_%s", char.id)))({
|
||||
layout = {layoutDirection = .TopToBottom, padding = {top = 10, right = 12, bottom = 10, left = 12}, childGap = 6},
|
||||
backgroundColor = card_bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = CLAY_ACCENT if is_editing else CLAY_BORDER_SUBTLE, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
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)
|
||||
if clay.UI(clay.ID(fmt.tprintf("char_head_%s", char.id)))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8}}) {
|
||||
clay_body_text(char.name, color = CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
declare_nav_chip(fmt.tprintf("char_role_%s", char.id), role_label, false)
|
||||
clay.UI(clay.ID(fmt.tprintf("char_head_spacer_%s", char.id)))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
||||
if is_editing {
|
||||
declare_button_small(fmt.tprintf("btn_char_save_%s", char.id), "Save")
|
||||
} else {
|
||||
declare_button_small(fmt.tprintf("btn_char_edit_%s", char.id), "Edit")
|
||||
}
|
||||
}
|
||||
|
||||
if is_editing {
|
||||
if clay.UI(clay.ID("field_char_desc"))({
|
||||
layout = clay_input_layout(),
|
||||
backgroundColor = CLAY_BG_INPUT,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_SM),
|
||||
border = {color = CLAY_BORDER_FOCUS, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(app.char_edit_text if len(app.char_edit_text) > 0 else "Enter description...", color = CLAY_TEXT_PRIMARY if len(app.char_edit_text) > 0 else CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
} else {
|
||||
if len(char.description) > 0 {
|
||||
clay_body_text(char.description, color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
} else {
|
||||
clay_body_text("No description", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
}
|
||||
}
|
||||
|
||||
if clay.UI(clay.ID(fmt.tprintf("char_actions_%s", char.id)))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 8}}) {
|
||||
declare_button_small(fmt.tprintf("btn_char_ref_%s", char.id), "Generate Ref")
|
||||
declare_button_small(fmt.tprintf("btn_char_sheet_%s", char.id), "Generate Sheet")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,8 +432,6 @@ declare_characters_workspace :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Export Workspace ─────────────────────────────────────────────
|
||||
character_role_label :: proc(role: core.Character_Role) -> string {
|
||||
switch role {
|
||||
case .Protagonist: return "Protagon."
|
||||
@@ -237,10 +442,22 @@ character_role_label :: proc(role: core.Character_Role) -> string {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// ─── Export Workspace ─────────────────────────────────────────────
|
||||
shot_type_label :: proc(st: core.Shot_Type) -> string {
|
||||
switch st {
|
||||
case .Establishing: return "Establishing"
|
||||
case .Wide: return "Wide"
|
||||
case .Medium: return "Medium"
|
||||
case .Close_Up: return "Close Up"
|
||||
case .Extreme_Close_Up: return "Extreme CU"
|
||||
case .Over_Shoulder: return "O/S"
|
||||
case .Aerial: return "Aerial"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
declare_export_workspace :: proc(app: ^GUI_App_State) {
|
||||
if clay.UI(clay.ID("ExportCard"))(clay_card_style()) {
|
||||
clay_title_text("Export")
|
||||
declare_section_header("ExportHeader", "Export", "Package your comic for distribution")
|
||||
|
||||
ready, total := ready_stage_count(app.controller)
|
||||
all_ready := ready == total
|
||||
@@ -248,35 +465,24 @@ declare_export_workspace :: proc(app: ^GUI_App_State) {
|
||||
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}}) {
|
||||
if clay.UI(clay.ID("ExportStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 12}}) {
|
||||
declare_stat_chip("exp_pages", "Pages", page_count)
|
||||
declare_stat_chip("exp_panels", "Panels", panel_count)
|
||||
declare_stat_chip("exp_ready", "Ready", ready)
|
||||
}
|
||||
|
||||
clay_muted_text(fmt.tprintf("Format: %v • Target: %s", app.controller.state.export_format, app.export_path))
|
||||
clay_body_text(fmt.tprintf("Format: %v", app.controller.state.export_format), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
|
||||
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)
|
||||
}
|
||||
declare_info_callout("ExportBlocked", fmt.tprintf("Blocked: %s", reason), false)
|
||||
clay_body_text("Complete all pipeline stages before exporting.", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
||||
} 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)
|
||||
}
|
||||
declare_info_callout("ExportReady", "All pipeline stages complete. Ready to export!", true)
|
||||
}
|
||||
|
||||
// Export action buttons
|
||||
declare_divider("ExportDivider")
|
||||
|
||||
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)
|
||||
@@ -286,10 +492,11 @@ declare_export_workspace :: proc(app: ^GUI_App_State) {
|
||||
|
||||
if page_count > 0 {
|
||||
if clay.UI(clay.ID("ExportPagesList"))({
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 1},
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
||||
}) {
|
||||
clay_label_text("PAGES")
|
||||
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)))
|
||||
clay_body_text(fmt.tprintf("Page %d - %s - %d panels", l.page_number, l.pattern_id, len(l.panels)), color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,25 +504,25 @@ declare_export_workspace :: proc(app: ^GUI_App_State) {
|
||||
}
|
||||
|
||||
declare_button_state_export :: proc(id: string, label: string, is_current_format: bool) {
|
||||
is_hov := clay.Hovered()
|
||||
bg := CLAY_BTN_DEFAULT
|
||||
if is_current_format { bg = CLAY_BTN_SOFT }
|
||||
if is_hov {
|
||||
if is_current_format { bg = CLAY_BTN_SOFT_HOVER }
|
||||
else { bg = CLAY_BTN_DEFAULT_HOVER }
|
||||
}
|
||||
if clay.UI(clay.ID(id))({
|
||||
layout = clay_button_layout(),
|
||||
backgroundColor = bg,
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_FRAC_BTN),
|
||||
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
||||
border = {color = CLAY_ACCENT if is_current_format else clay.Color{0, 0, 0, 0}, width = clay.BorderOutside(1)},
|
||||
}) {
|
||||
clay_body_text(label)
|
||||
clay_body_text(label, color = CLAY_TEXT_BRIGHT if is_current_format else CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
}
|
||||
declare_empty_state("CommunityCard", "Community",
|
||||
"Community features coming soon",
|
||||
"Share and discover comics made with comic-odin")
|
||||
}
|
||||
|
||||
// ─── Bottom Bar ───────────────────────────────────────────────────
|
||||
Reference in new issue
Block a user