package gui import "core:fmt" import "core:strconv" import "core:strings" import rl "vendor:raylib" import "../adapters" import "../core" import "../shared" import "../ui" action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: int) -> string { cfg := shared.load_config() if len(cfg.deepseek_api_key) == 0 { return "DeepSeek key missing (set DEEPSEEK_API_KEY)" } opts := adapters.Generate_Script_Options{ story_idea = controller.state.story_idea, genre = controller.state.story_genre, art_style = controller.state.art_style, num_pages = pages, audience = controller.state.target_audience, } script, gerr := adapters.generate_comic_script_stub(cfg, opts) if !shared.is_ok(gerr) { return fmt.aprintf("DeepSeek script failed: %s", gerr.message) } core.dispose_script(&controller.state.script) controller.state.script = script // 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 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 { 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 into owned heap storage so it survives across frames. img.url = strings.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.. string { panels := collect_script_panels(controller.state.script) defer delete(panels) if len(panels) == 0 { return "No script panels for layout" } core.dispose_page_layouts(&controller.state.page_layouts) controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "") controller.active_screen = .Layout controller.state.workflow.current_step = .Layout return "Auto layout generated" } action_regenerate_page_layout :: proc(controller: ^ui.App_Controller, page_num: int) -> string { if page_num < 0 || page_num >= len(controller.state.page_layouts) { return "Invalid layout page" } layout := &controller.state.page_layouts[page_num] panel_count := len(layout.panels) if panel_count == 0 { return "Page has no panels" } // For a simple visible regenerate effect, pick a random pattern that fits pattern_ids := []string{ "single-splash", "grid-2x2", "grid-3x3", "dynamic-action", "manga-spread", "dialogue-heavy", "cinematic-widescreen", } // Just pick the next valid pattern in the list (or wrap around) start_idx := 0 for id, i in pattern_ids { if id == layout.pattern_id { start_idx = i break } } new_pattern_id := layout.pattern_id for i in 1..=len(pattern_ids) { idx := (start_idx + i) % len(pattern_ids) candidate := pattern_ids[idx] if core.pattern_max_panels(candidate) >= panel_count { new_pattern_id = candidate break } } pattern := core.get_layout_pattern_by_id(new_pattern_id) layout.pattern_id = new_pattern_id for i in 0..= len(pattern.cells) { cell_index = len(pattern.cells) - 1 } layout.panels[i].layout_cell = pattern.cells[cell_index] } return "Layout page regenerated" } export_format_name :: proc(f: core.Export_Format) -> string { switch f { case .PDF: return "PDF" case .PNG: return "PNG" case .CBZ: return "CBZ" } return "PDF" } parse_pages_or_default :: proc(s: string, def: int) -> int { v, ok := strconv.parse_int(strings.trim_space(s)) if !ok || v <= 0 { return def } return v } parse_autosave_interval :: proc(s: string, def: int) -> int { v, ok := strconv.parse_int(strings.trim_space(s)) if !ok { return def } if v < 5 { return 5 } if v > 300 { return 300 } return v } set_autosave_interval_text :: proc(dst: ^string, seconds: int) -> string { v := seconds if v < 5 { v = 5 } if v > 300 { v = 300 } dst^ = fmt.aprintf("%d", v) return fmt.aprintf("Autosave interval: %ds", v) } yn :: proc(v: bool) -> string { if v { return "yes" } return "no" } toggle_summary_show :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string { #partial switch active_screen { case .Script: opts.script_show_all = !opts.script_show_all return fmt.aprintf("Script summary show-all: %s", yn(opts.script_show_all)) case .Layout: opts.layout_show_all = !opts.layout_show_all return fmt.aprintf("Layout summary show-all: %s", yn(opts.layout_show_all)) } return "Summary show toggle unavailable on this screen" } toggle_summary_sort :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string { #partial switch active_screen { case .Script: opts.script_desc = !opts.script_desc sort_name := "asc" if opts.script_desc { sort_name = "desc" } return fmt.aprintf("Script summary sort: %s", sort_name) case .Layout: opts.layout_desc = !opts.layout_desc sort_name := "asc" if opts.layout_desc { sort_name = "desc" } return fmt.aprintf("Layout summary sort: %s", sort_name) } return "Summary sort toggle unavailable on this screen" } toggle_summary_show_if_supported :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string { if active_screen == .Script || active_screen == .Layout { return toggle_summary_show(active_screen, opts) } return "" } toggle_summary_sort_if_supported :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string { if active_screen == .Script || active_screen == .Layout { return toggle_summary_sort(active_screen, opts) } return "" } reset_project_session :: proc(controller: ^ui.App_Controller, is_dirty: ^bool, last_autosave_at: ^f64, touch_time: bool) -> string { core.dispose_state(&controller.state) controller.state = core.new_initial_state() controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step) is_dirty^ = false if touch_time { last_autosave_at^ = rl.GetTime() } return "Reset project" } open_project_session :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, is_dirty: ^bool, last_autosave_at: ^f64) -> string { normalize_project_path_field(project_path) loaded, lerr := adapters.load_project(project_path^) if !shared.is_ok(lerr) { return lerr.message } core.dispose_state(&controller.state) controller.state = loaded controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step) sync_export_path_to_project_dir(project_path^, export_path, export_format) is_dirty^ = false last_autosave_at^ = rl.GetTime() return fmt.aprintf("Opened project: %s (export -> %s)", project_path^, export_path^) } resolve_confirm_action_with_message :: proc(action: Pending_Confirm_Action, controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, is_dirty: ^bool, last_autosave_at: ^f64) -> string { switch action { case .Reset_Project: return reset_project_session(controller, is_dirty, last_autosave_at, true) case .Open_Project: return open_project_session(controller, project_path, export_path, export_format, is_dirty, last_autosave_at) case .None: return "No pending destructive action" } return "No pending destructive action" } save_project_session_with_message :: proc(project_path: ^string, state: core.Comic_State, is_dirty: ^bool, last_autosave_at, last_save_at: ^f64, success_prefix: string) -> string { normalize_project_path_field(project_path) err := adapters.save_project(project_path^, state) if !shared.is_ok(err) { return err.message } is_dirty^ = false last_autosave_at^ = rl.GetTime() last_save_at^ = last_autosave_at^ return fmt.aprintf("%s: %s", success_prefix, project_path^) } 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 return fmt.aprintf("Exported %s", export_format_name(export_format)) } gui_next_hint :: proc(controller: ui.App_Controller) -> string { if len(controller.state.script.pages) == 0 { return "generate script" } if len(controller.state.panel_images) == 0 { return "generate panels" } if len(controller.state.page_layouts) == 0 { return "layout auto" } return "export pdf" } action_run_next :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int) -> string { hint := gui_next_hint(controller^) switch hint { case "generate script": return action_generate_deepseek_script(controller, script_pages) case "generate panels": cfg := shared.load_config() if len(cfg.fal_api_key) == 0 { return "FAL API key missing" } return run_panels_action(controller, nil, nil) case "layout auto": return action_layout_auto(controller) case "export pdf": return action_export(controller, export_path, export_format) } return "No next action" } action_run_auto_all :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int) -> string { for _ in 0..<4 { msg := action_run_next(controller, export_path, export_format, script_pages) if controller.active_screen == .Export { return fmt.aprintf("Auto-all complete: %s", msg) } } return "Auto-all could not complete" } run_panels_action :: proc(controller: ^ui.App_Controller, queue: ^adapters.Fal_Generation_Queue, is_dirty: ^bool) -> string { if len(controller.state.script.pages) == 0 { return "Generate script before panels" } cfg := shared.load_config() 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(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) } is_dirty^ = true for _, img in controller.state.panel_images { delete(img.url) delete(img.prompt) } delete(controller.state.panel_images) // Clone URLs/prompts into owned heap storage before storing cloned: map[string]core.Panel_Image for pid, img in images { cloned[pid] = core.Panel_Image{ url = strings.clone(img.url), width = img.width, height = img.height, seed = img.seed, prompt = strings.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 { if !can_layout { return "Generate panels before layout" } is_dirty^ = true return action_layout_auto(controller) } run_export_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, can_export: bool, is_dirty: ^bool, last_export_at: ^f64) -> string { normalize_export_path_field(export_path, export_format) if !can_export { return "Export blocked: generate panels + layout first" } msg := action_export(controller, export_path^, export_format) is_dirty^ = true if strings.has_prefix(msg, "Exported ") { last_export_at^ = rl.GetTime() } return msg } run_next_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at: ^f64) -> string { normalize_export_path_field(export_path, export_format) msg := action_run_next(controller, export_path^, export_format, pages_count) is_dirty^ = true if controller.active_screen == .Export { last_export_at^ = rl.GetTime() } return msg } run_auto_all_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at: ^f64) -> string { normalize_export_path_field(export_path, export_format) msg := action_run_auto_all(controller, export_path^, export_format, pages_count) is_dirty^ = true if controller.active_screen == .Export { last_export_at^ = rl.GetTime() } return msg } run_auto_all_save_action :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, pages_count: int, is_dirty: ^bool, last_export_at, last_autosave_at, last_save_at: ^f64) -> string { normalize_export_path_field(export_path, export_format) msg := action_run_auto_all(controller, export_path^, export_format, pages_count) if controller.active_screen == .Export { last_export_at^ = rl.GetTime() return save_project_session_with_message(project_path, controller.state, is_dirty, last_autosave_at, last_save_at, "Auto-all + saved") } return msg } set_export_format_with_message :: proc(export_format: ^core.Export_Format, export_path: ^string, next: core.Export_Format, is_dirty: ^bool) -> string { export_format^ = next export_path^ = export_path_for_format(export_path^, export_format^) is_dirty^ = true return fmt.aprintf("Export format: %s (%s)", export_format_name(export_format^), export_path^) } autosave_tick_with_message :: proc(project_path: ^string, state: core.Comic_State, autosave_enabled: bool, is_dirty: ^bool, last_autosave_at: ^f64, last_save_at: ^f64, autosave_interval_s: f64) -> string { if !autosave_enabled || !is_dirty^ { return "" } now := rl.GetTime() if now-last_autosave_at^ < autosave_interval_s { return "" } normalize_project_path_field(project_path) err := adapters.save_project(project_path^, state) last_autosave_at^ = now if shared.is_ok(err) { is_dirty^ = false last_save_at^ = now return fmt.aprintf("Autosaved: %s", project_path^) } return fmt.aprintf("Autosave failed: %s", err.message) } action_add_bubble :: proc(controller: ^ui.App_Controller, panel_id: string) -> string { id := fmt.aprintf("bubble_%s_new", panel_id) bubble := core.Speech_Bubble{ id = id, panel_id = panel_id, type = .Normal, text = "New bubble", position = core.Position{x = 0.1, y = 0.1}, size = core.Size{width = 120, height = 40}, tail_direction = "bottom", tail_target = core.Position{x = 0.5, y = 0.5}, style = core.DEFAULT_BUBBLE_STYLE, speaker_id = "", } if controller.state.speech_bubbles == nil { controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) } existing: [dynamic]core.Speech_Bubble if slice, ok := controller.state.speech_bubbles[panel_id]; ok { for b in slice { append(&existing, b) } } append(&existing, bubble) controller.state.speech_bubbles[panel_id] = existing[:] return "Added bubble" } action_delete_bubble :: proc(controller: ^ui.App_Controller, panel_id: string, bubble_idx: int) -> string { if controller.state.speech_bubbles == nil { return "No bubbles to delete" } slice, ok := controller.state.speech_bubbles[panel_id] if !ok || bubble_idx < 0 || bubble_idx >= len(slice) { return "Bubble not found" } // Remove by shifting elements new_slice: [dynamic]core.Speech_Bubble for i in 0.. string { // Find the script panel to get dialogue script_panel: core.Panel found := false for page in controller.state.script.pages { for p in page.panels { if p.panel_id == panel_id { script_panel = p found = true break } } if found { break } } if !found { return "Script panel not found for auto-place" } // Find the layout cell for this panel to get dimensions cell_w: f32 = 1 cell_h: f32 = 1 for lp in layout.panels { if lp.panel_id == panel_id { cell_w = lp.layout_cell.w cell_h = lp.layout_cell.h break } } panel_w := cell_w * f32(layout.width) panel_h := cell_h * f32(layout.height) bubbles := core.auto_place_panel_bubbles(script_panel, panel_w, panel_h) defer delete(bubbles) if len(bubbles) == 0 { return "No dialogue to auto-place" } if controller.state.speech_bubbles == nil { controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) } controller.state.speech_bubbles[panel_id] = bubbles return fmt.tprintf("Auto-placed %d bubbles", len(bubbles)) } action_update_bubble :: proc(controller: ^ui.App_Controller, panel_id: string, bubble_idx: int, new_type: core.Bubble_Type, new_text: string) -> string { if controller.state.speech_bubbles == nil { return "No bubbles to update" } slice, ok := controller.state.speech_bubbles[panel_id] if !ok || bubble_idx < 0 || bubble_idx >= len(slice) { return "Bubble not found" } // Copy to dynamic slice, update, then reassign new_slice: [dynamic]core.Speech_Bubble for i in 0.. string { cfg := shared.load_config() if len(cfg.fal_api_key) == 0 { return "FAL_API_KEY is missing" } // Find the character target_char: core.Character found := false for c in controller.state.characters { if c.id == character_id { target_char = c found = true break } } if !found { return "Character not found" } 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 } // Update character with reference image new_chars: [dynamic]core.Character for c in controller.state.characters { new_c := c if c.id == character_id { new_c.reference_image_url = url } append(&new_chars, new_c) } delete(controller.state.characters) controller.state.characters = new_chars[:] return fmt.tprintf("Generated reference for %s", target_char.name) } action_generate_character_sheet :: proc(controller: ^ui.App_Controller, character_id: string) -> string { cfg := shared.load_config() if len(cfg.fal_api_key) == 0 { return "FAL_API_KEY is missing" } // Find the character target_char: core.Character found := false for c in controller.state.characters { if c.id == character_id { target_char = c found = true break } } if !found { return "Character not found" } 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 } // Update character with sheet URLs new_chars: [dynamic]core.Character for c in controller.state.characters { new_c := c if c.id == character_id { // Free old sheet URLs for u in c.character_sheet_urls { delete(u) } delete(c.character_sheet_urls) new_c.character_sheet_urls = sheet_urls } append(&new_chars, new_c) } delete(controller.state.characters) controller.state.characters = new_chars[:] 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" } for panel_id, slice in controller.state.speech_bubbles { for i in 0.. string { if controller.state.speech_bubbles == nil { return "No bubbles to reposition" } for panel_id, slice in controller.state.speech_bubbles { for i in 0.. string { if controller.state.speech_bubbles == nil { return "No bubbles to reset" } for panel_id, slice in controller.state.speech_bubbles { for i in 0..