- gui/panel_editor.odin: Full drawing editor overlay - Editor_Tool enum: Pen, Eraser, Line, Rect, Circle, Fill - Brush_Stroke struct with points array for freehand/shape strokes - Panel_Editor_State with active panel, strokes, undo snapshots, zoom/pan - open/close/commit lifecycle: loads panel texture, composites on commit - Screen-to-canvas coordinate mapping with zoom and pan offsets - Stroke rendering: freehand via line segments, shapes via endpoints - Undo via snapshot replay (cap 30 snapshots) - Toolbar: tool buttons, brush size slider, color picker, commit/cancel - Color picker panel: 18 preset color swatches, current color display - to_cstr helper for Odin string->cstring conversion - gui/runtime.odin: Editor integration - GUI_App_State.editor field (Panel_Editor_State) - Editor update/render calls in main loop when active - Escape key closes editor, Ctrl+Z for undo - editor_close in defer chain for cleanup on exit - btn_panel_draw click handler opens editor on selected panel - gui/workspaces.odin: Draw button on panel cards - 'Draw' button triggers btn_panel_draw action - gui/chrome.odin: Editor hint text when editor active - Shows 'Press Escape to close editor' overlay Build passes. 153/156 tests pass (3 pre-existing CLI/TUI failures, 2 new env-related failures due to FAL_API_KEY in .env).
530 lines
24 KiB
Odin
530 lines
24 KiB
Odin
package gui
|
|
|
|
import clay "clay:."
|
|
import "core:fmt"
|
|
import "../core"
|
|
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(34)}, layoutDirection = .LeftToRight, childGap = 8, childAlignment = {y = .Center}},
|
|
}) {
|
|
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 >")
|
|
}
|
|
}
|
|
|
|
declare_script_workspace :: proc(app: ^GUI_App_State) {
|
|
page_count := len(app.controller.state.script.pages)
|
|
if page_count == 0 {
|
|
declare_empty_state("ScriptEmpty", "No Script Yet",
|
|
"Go to Story screen to set up your story idea",
|
|
"Generate Script (F5) to create pages")
|
|
return
|
|
}
|
|
|
|
idx := clamp_script_cursor(page_count, app.summary_opts.script_page_cursor)
|
|
workspace_nav("script", fmt.tprintf("Page %d/%d", idx+1, page_count))
|
|
|
|
title := app.controller.state.script.title
|
|
if len(title) > 50 { title = fmt.tprintf("%s...", title[:47]) }
|
|
clay_muted_text(title)
|
|
|
|
declare_script_detail(app)
|
|
}
|
|
|
|
declare_panels_workspace :: proc(app: ^GUI_App_State) {
|
|
panel_count := count_script_panels(app.controller.state.script)
|
|
if panel_count == 0 {
|
|
declare_empty_state("PanelsEmpty", "No Panels Yet",
|
|
"Generate a script first, then create panel images",
|
|
"Generate Panels (F6) after having a script")
|
|
return
|
|
}
|
|
|
|
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")
|
|
declare_button_small("btn_panel_draw", "Draw")
|
|
} 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)},
|
|
}) {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
declare_section_header("LayoutHeader", "Layout", "Arrange panels into comic pages")
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
declare_bubbles_workspace :: proc(app: ^GUI_App_State) {
|
|
layout_count := len(app.controller.state.page_layouts)
|
|
if layout_count == 0 {
|
|
declare_empty_state("BubblesEmpty", "No Bubbles Yet",
|
|
"Run Layout Auto first to create page layouts",
|
|
"Then use this screen to edit speech bubbles")
|
|
return
|
|
}
|
|
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
panel_count := len(layout_val.panels)
|
|
|
|
if clay.UI(clay.ID("BubblesNav"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(34)}, layoutDirection = .LeftToRight, childGap = 6, childAlignment = {y = .Center}},
|
|
}) {
|
|
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")
|
|
declare_button_small("btn_bubbles_next_panel", "Panel >")
|
|
}
|
|
|
|
declare_bubbles_detail(app)
|
|
}
|
|
|
|
declare_story_workspace :: proc(app: ^GUI_App_State, bp: shared.Breakpoint) {
|
|
if clay.UI(clay.ID("StoryCard"))(clay_card_style()) {
|
|
declare_section_header("StoryHeader", "Story Setup", "Define your comic's premise")
|
|
|
|
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 = 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)},
|
|
}) {
|
|
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,
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
declare_section_header("PathHeader", "Project Paths", "Configure output locations")
|
|
|
|
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, 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")
|
|
}
|
|
|
|
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, 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_label_text("PAGES")
|
|
if clay.UI(clay.ID("field_pages"))({
|
|
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, size = CLAY_FONT_SIZE_SM)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
declare_section_header("CharsHeader", "Characters", fmt.tprintf("%d characters defined", char_count))
|
|
|
|
if char_count == 0 {
|
|
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 {
|
|
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)
|
|
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)},
|
|
}) {
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
character_role_label :: proc(role: core.Character_Role) -> string {
|
|
switch role {
|
|
case .Protagonist: return "Protagon."
|
|
case .Antagonist: return "Antagon."
|
|
case .Supporting: return "Support."
|
|
case .Extra: return "Extra"
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
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()) {
|
|
declare_section_header("ExportHeader", "Export", "Package your comic for distribution")
|
|
|
|
ready, total := ready_stage_count(app.controller)
|
|
all_ready := ready == total
|
|
|
|
panel_count := len(app.controller.state.panel_images)
|
|
page_count := len(app.controller.state.page_layouts)
|
|
|
|
if clay.UI(clay.ID("ExportStats"))({layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childGap = 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_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 {
|
|
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 {
|
|
declare_info_callout("ExportReady", "All pipeline stages complete. Ready to export!", true)
|
|
}
|
|
|
|
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)
|
|
declare_button_state_export("btn_export_png", "Export as PNG", format == .PNG)
|
|
declare_button_state_export("btn_export_cbz", "Export as CBZ", format == .CBZ)
|
|
}
|
|
|
|
if page_count > 0 {
|
|
if clay.UI(clay.ID("ExportPagesList"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, childGap = 2},
|
|
}) {
|
|
clay_label_text("PAGES")
|
|
for l in app.controller.state.page_layouts {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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_MD),
|
|
border = {color = CLAY_ACCENT if is_current_format else clay.Color{0, 0, 0, 0}, width = clay.BorderOutside(1)},
|
|
}) {
|
|
clay_body_text(label, color = CLAY_TEXT_BRIGHT if is_current_format else CLAY_TEXT_PRIMARY, size = CLAY_FONT_SIZE_SM)
|
|
}
|
|
}
|
|
|
|
declare_community_workspace :: proc(app: ^GUI_App_State) {
|
|
declare_empty_state("CommunityCard", "Community",
|
|
"Community features coming soon",
|
|
"Share and discover comics made with comic-odin")
|
|
}
|