comic/odin/src/gui/actions.odin
Dimitar765 9cba73baca
Some checks failed
odin-ci / build-test (push) Has been cancelled
rev
2026-07-13 12:56:24 +02:00

822 lines
25 KiB
Odin

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..<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 {
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..<panel_count {
cell_index := i
if cell_index >= 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..<len(slice) {
if i != bubble_idx {
append(&new_slice, slice[i])
}
}
if len(new_slice) == 0 {
delete_key(&controller.state.speech_bubbles, panel_id)
} else {
controller.state.speech_bubbles[panel_id] = new_slice[:]
}
return "Deleted bubble"
}
action_auto_place_bubbles_for_panel :: proc(controller: ^ui.App_Controller, panel_id: string, layout: core.Page_Layout) -> 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..<len(slice) {
b := slice[i]
if i == bubble_idx {
if new_text != "" {
b.text = new_text
}
b.type = new_type
}
append(&new_slice, b)
}
controller.state.speech_bubbles[panel_id] = new_slice[:]
return "Bubble updated"
}
action_generate_character_reference :: 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"
}
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..<len(slice) {
if slice[i].id == bubble_id {
new_slice: [dynamic]core.Speech_Bubble
for j in 0..<len(slice) {
b := slice[j]
if j == i {
b.text = new_text
}
append(&new_slice, b)
}
controller.state.speech_bubbles[panel_id] = new_slice[:]
return "Bubble text updated"
}
}
}
return "Bubble not found"
}
action_reposition_bubble :: proc(controller: ^ui.App_Controller, bubble_id: string, x, y: f32) -> 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..<len(slice) {
if slice[i].id == bubble_id {
new_slice: [dynamic]core.Speech_Bubble
for j in 0..<len(slice) {
b := slice[j]
if j == i {
b.position = core.Position{x = x, y = y}
}
append(&new_slice, b)
}
controller.state.speech_bubbles[panel_id] = new_slice[:]
return "Bubble repositioned"
}
}
}
return "Bubble not found"
}
action_reset_bubble_position :: proc(controller: ^ui.App_Controller, bubble_id: string) -> 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..<len(slice) {
if slice[i].id == bubble_id {
new_slice: [dynamic]core.Speech_Bubble
for j in 0..<len(slice) {
b := slice[j]
if j == i {
b.position = core.Position{x = 0, y = 0}
}
append(&new_slice, b)
}
controller.state.speech_bubbles[panel_id] = new_slice[:]
return "Bubble position reset"
}
}
}
return "Bubble not found"
}