check point

This commit is contained in:
dimitar committed 2026-05-22 03:51:50 +02:00
1 parent 33c70e776a
commit b0f9acdb47
37 files changed
+4469 -177

No files matched your search

+285
View File
@@ -476,3 +476,288 @@ autosave_tick_with_message :: proc(project_path: ^string, state: core.Comic_Stat
}
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)
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)
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_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"
}
+255
View File
@@ -0,0 +1,255 @@
package gui
import "core:fmt"
import rl "vendor:raylib"
import "../core"
import "../ui"
bubble_type_name :: proc(t: core.Bubble_Type) -> string {
switch t {
case .Normal: return "Normal"
case .Thought: return "Thought"
case .Shout: return "Shout"
case .Whisper: return "Whisper"
case .Narration: return "Narration"
case .Sound_Effect: return "SFX"
}
return "Normal"
}
bubble_type_from_name :: proc(name: string) -> core.Bubble_Type {
switch name {
case "Normal": return .Normal
case "Thought": return .Thought
case "Shout": return .Shout
case "Whisper": return .Whisper
case "Narration": return .Narration
case "SFX": return .Sound_Effect
}
return .Normal
}
clamp_bubble_cursor :: proc(count, cursor: int) -> int {
if count <= 0 {
return 0
}
if cursor < 0 {
return 0
}
if cursor >= count {
return count - 1
}
return cursor
}
collect_layout_panels_for_page :: proc(layouts: []core.Page_Layout, page_cursor: int) -> []core.Page_Layout_Panel {
if len(layouts) == 0 || page_cursor < 0 || page_cursor >= len(layouts) {
return nil
}
return layouts[page_cursor].panels
}
count_bubbles_for_panel :: proc(bubbles: map[string][]core.Speech_Bubble, panel_id: string) -> int {
if bubbles == nil {
return 0
}
if slice, ok := bubbles[panel_id]; ok {
return len(slice)
}
return 0
}
draw_bubbles_detail_panel :: proc(
controller: ui.App_Controller,
x, y, w, h: i32,
page_cursor: int,
panel_cursor: int,
bubble_cursor: int,
) -> (
add_clicked: bool,
delete_clicked: bool,
auto_place_clicked: bool,
new_page_cursor: int,
new_panel_cursor: int,
new_bubble_cursor: int,
edited_text: string,
edited_type: core.Bubble_Type,
type_changed: bool,
text_changed: bool,
) {
add_clicked = false
delete_clicked = false
auto_place_clicked = false
new_page_cursor = page_cursor
new_panel_cursor = panel_cursor
new_bubble_cursor = bubble_cursor
type_changed = false
text_changed = false
edited_text = ""
edited_type = .Normal
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Bubble Editor")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
layout_count := len(controller.state.page_layouts)
if layout_count == 0 {
draw_summary_line(x+18, y+46, "No layouts yet. Run Layout Auto first.", SUMMARY_HINT)
return
}
page_idx := clamp_layout_cursor(layout_count, page_cursor)
if page_idx != page_cursor {
new_page_cursor = page_idx
}
layout := controller.state.page_layouts[page_idx]
panel_list := layout.panels
panel_count := len(panel_list)
if panel_count == 0 {
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d has no panels", layout.page_number), SUMMARY_HINT)
return
}
// Page navigation
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • %d panels", page_idx+1, layout_count, panel_count), SUMMARY_ACCENT)
panel_idx := clamp_bubble_cursor(panel_count, panel_cursor)
if panel_idx != panel_cursor {
new_panel_cursor = panel_idx
}
panel := panel_list[panel_idx]
// Panel info + auto-place button
draw_summary_subline(x+18, y+66, fmt.tprintf("Panel %d (id: %s)", panel.panel_number, panel.panel_id), SUMMARY_SUBLINE)
auto_btn_rec := rl.Rectangle{x = f32(x + w - 110), y = f32(y+62), width = 90, height = 22}
draw_small_button_state(auto_btn_rec, "Auto Place", true)
if button_clicked(auto_btn_rec) {
auto_place_clicked = true
}
// Bubble list
bubble_map := controller.state.speech_bubbles
bubbles_for_panel: []core.Speech_Bubble = nil
if bubble_map != nil {
if slice, ok := bubble_map[panel.panel_id]; ok {
bubbles_for_panel = slice
}
}
bubble_count := len(bubbles_for_panel)
bubble_idx := clamp_bubble_cursor(bubble_count, bubble_cursor)
if bubble_count > 0 && bubble_idx != bubble_cursor {
new_bubble_cursor = bubble_idx
}
// Bubble list header
list_y := y + 90
draw_summary_line(x+18, list_y, fmt.tprintf("Bubbles: %d", bubble_count), SUMMARY_ACCENT)
// Add button
add_btn_rec := rl.Rectangle{x = f32(x + w - 70), y = f32(list_y-4), width = 50, height = 20}
draw_small_button_state(add_btn_rec, "Add", true)
if button_clicked(add_btn_rec) {
add_clicked = true
}
// Bubble rows
row_start_y := list_y + 22
row_h: i32 = 20
max_rows: int = int(h - 120) / int(row_h)
if max_rows < 1 {
max_rows = 1
}
// Scroll window for bubble list
scroll_start := 0
if bubble_idx >= max_rows {
scroll_start = bubble_idx - max_rows + 1
}
scroll_end := scroll_start + max_rows
if scroll_end > bubble_count {
scroll_end = bubble_count
scroll_start = scroll_end - max_rows
if scroll_start < 0 {
scroll_start = 0
}
}
row: i32 = 0
for i in scroll_start..<scroll_end {
b := bubbles_for_panel[i]
mark := " "
row_color := TEXT_TERTIARY
if i == bubble_idx {
mark = ">"
row_color = TEXT_PRIMARY
}
row_rec := rl.Rectangle{x = f32(x+18), y = f32(row_start_y+row*row_h), width = f32(w-90), height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
new_bubble_cursor = i
}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
row_color = TEXT_SECONDARY
}
// Delete button on hover/selected
del_rec := rl.Rectangle{x = f32(x+w-28), y = f32(row_start_y+row*row_h), width = 18, height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), del_rec) && rl.IsMouseButtonPressed(.LEFT) && i == bubble_idx {
delete_clicked = true
}
if i == bubble_idx {
draw_text_fitted("x", x+w-26, row_start_y+row*row_h+3, 12, 14, 7, ERROR)
}
preview := b.text
if len(preview) > 30 {
preview = preview[:30]
preview = fmt.tprintf("%s...", preview)
}
if len(preview) == 0 {
preview = "(empty)"
}
draw_summary_subline(x+18, row_start_y+row*row_h+2,
fit_text_for_width(fmt.tprintf("%s [%s] %s", mark, bubble_type_name(b.type), preview), int(w-100), 7), row_color)
row += 1
}
// Selected bubble editor
if bubble_count > 0 && bubble_idx < bubble_count {
editor_y := row_start_y + row*row_h + 8
if editor_y < y+h-80 {
selected := bubbles_for_panel[bubble_idx]
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(editor_y-6), width = f32(w-24), height = 70})
draw_summary_line(x+18, editor_y, fmt.tprintf("Editing: %s", bubble_type_name(selected.type)), SUMMARY_ACCENT)
// Type selector buttons
type_y := editor_y + 20
type_idx: i32 = 0
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
type_w: i32 = 60
type_spacing: i32 = 4
for t in types {
t_rec := rl.Rectangle{x = f32(x)+18+f32(int(type_idx)*int(type_w+type_spacing)), y = f32(type_y), width = f32(type_w), height = 18}
draw_nav_item(t_rec, bubble_type_name(t), selected.type == t)
if button_clicked(t_rec) && selected.type != t {
edited_type = t
type_changed = true
}
type_idx += 1
}
// Text preview
text_y := type_y + 22
draw_summary_subline(x+18, text_y, fit_text_for_width(fmt.tprintf("text: %s", selected.text), int(w-36), 7), SUMMARY_SUBLINE)
}
}
if bubble_count == 0 {
draw_summary_line(x+18, row_start_y, "No bubbles for this panel. Click Add or Auto Place.", SUMMARY_HINT)
}
return
}
+21 -5
View File
@@ -41,7 +41,7 @@ build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script
chars_present := make([]string, 1)
chars_present[0] = "char_001"
dialogue := make([]core.Dialogue, 1)
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = "neutral"}
dialogue[0] = core.Dialogue{speaker_id = "char_001", text = "Let's do this.", bubble_type = .Normal, emotion = .Neutral}
panel := core.Panel{
panel_id = local_panel_id_by_index(i),
@@ -75,13 +75,29 @@ build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel
name := fmt.aprintf("panel_%03d_%s.png", idx+1, p.panel_id)
out_path := fmt.aprintf("%s/%s", tmp_dir, name)
delete(name)
if werr := os.write_entire_file(out_path, "LOCAL PANEL IMAGE"); werr != nil {
// Create a real PNG image using python3
gen_w := 1024
gen_h := 1024
py_script := fmt.aprintf(
"import struct,zlib,sys;w,h=%d,%d;rows=[]\nfor _ in range(h): rows.append(b'\\x00'+b'\\xff\\xff\\xff'*w)\nraw=b''.join(rows);comp=zlib.compress(raw)\ndef crc(d): return struct.pack('>I',zlib.crc32(d)&0xffffffff)\nf=open(sys.argv[1],'wb')\nf.write(b'\\x89PNG\\r\\n\\x1a\\n')\nihdr_data=struct.pack('>IIBBBBB',w,h,8,2,0,0,0)\nf.write(struct.pack('>I',13)+b'IHDR'+ihdr_data+crc(b'IHDR'+ihdr_data))\nf.write(struct.pack('>I',len(comp))+b'IDAT'+comp+crc(b'IDAT'+comp))\nf.write(struct.pack('>I',0)+b'IEND'+crc(b'IEND'))\nf.close()",
gen_w, gen_h,
)
defer delete(py_script)
py_cmd := [4]string{"python3", "-c", py_script, out_path}
desc := os.Process_Desc{command = py_cmd[:]}
state, _, stderr, cerr := os.process_exec(desc, context.temp_allocator)
if cerr != nil || !state.exited || state.exit_code != 0 {
delete(out_path)
return nil, shared.new_error(.Generation, "failed writing local panel image", true)
msg := fmt.aprintf("failed to create panel image: %s", string(stderr))
defer delete(msg)
return nil, shared.new_error(.Generation, msg, true)
}
url := fmt.aprintf("file://%s", out_path)
prompt := fmt.aprintf("local")
images[p.panel_id] = core.Panel_Image{url = url, width = 1024, height = 1024, seed = i64(idx + 1), prompt = prompt}
prompt := fmt.aprintf("local panel %d", idx+1)
images[p.panel_id] = core.Panel_Image{url = url, width = gen_w, height = gen_h, seed = i64(idx + 1), prompt = prompt}
delete(out_path)
}
return images, shared.ok()
+173 -24
View File
@@ -49,19 +49,13 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
for !rl.WindowShouldClose() {
screen_w_loop := rl.GetScreenWidth()
screen_h_loop := rl.GetScreenHeight()
compact_mode := screen_h_loop < 860
compact_mode := shared.is_compact(screen_h_loop)
cfg_loop := shared.load_config()
has_deepseek_key := len(cfg_loop.deepseek_api_key) > 0
main_w_loop := screen_w_loop - 282 - 20
if main_w_loop < 960 {
main_w_loop = 960
}
main_w_loop := shared.compute_main_width(screen_w_loop)
status_w_loop := (main_w_loop - 2) / 2
log_x_loop := 282 + status_w_loop + 2
lower_y_loop := screen_h_loop - 252
if lower_y_loop < 450 {
lower_y_loop = 450
}
log_x_loop := shared.LAYOUT.sidebar_width + status_w_loop + 2
lower_y_loop := shared.compute_lower_y(screen_h_loop)
idea_rec := rl.Rectangle{x = 420, y = 90, width = f32(main_w_loop-460), height = 36}
genre_rec := rl.Rectangle{x = 420, y = 146, width = f32(main_w_loop-460), height = 36}
@@ -125,8 +119,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
path_fix_export_status_btn := rl.Rectangle{x = 674, y = 556, width = 32, height = 20}
path_fix_all_status_btn := rl.Rectangle{x = 710, y = 556, width = 34, height = 20}
summary_show_btn := rl.Rectangle{x = f32(282 + status_w_loop - 178), y = f32(lower_y_loop + 18), width = 78, height = 24}
summary_sort_btn := rl.Rectangle{x = f32(282 + status_w_loop - 94), y = f32(lower_y_loop + 18), width = 78, height = 24}
summary_show_btn := rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 178), y = f32(lower_y_loop + 18), width = 78, height = 24}
summary_sort_btn := rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 94), y = f32(lower_y_loop + 18), width = 78, height = 24}
summary_prev_btn := rl.Rectangle{x = f32(300), y = f32(lower_y_loop + 18), width = 52, height = 24}
summary_next_btn := rl.Rectangle{x = f32(358), y = f32(lower_y_loop + 18), width = 52, height = 24}
@@ -351,6 +345,36 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if controller.active_screen == .Bubbles && button_clicked(summary_prev_btn) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
panel_count := len(controller.state.page_layouts[page_idx].panels)
if panel_count > 0 {
summary_opts.bubble_panel_cursor -= 1
if summary_opts.bubble_panel_cursor < 0 {
summary_opts.bubble_panel_cursor = panel_count - 1
}
summary_opts.bubble_edit_cursor = 0
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
}
}
}
if controller.active_screen == .Bubbles && button_clicked(summary_next_btn) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
panel_count := len(controller.state.page_layouts[page_idx].panels)
if panel_count > 0 {
summary_opts.bubble_panel_cursor += 1
if summary_opts.bubble_panel_cursor >= panel_count {
summary_opts.bubble_panel_cursor = 0
}
summary_opts.bubble_edit_cursor = 0
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
}
}
}
if button_clicked(new_btn) {
if is_dirty && !shift_down {
@@ -559,6 +583,36 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if controller.active_screen == .Bubbles && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
panel_count := len(controller.state.page_layouts[page_idx].panels)
if panel_count > 0 {
summary_opts.bubble_panel_cursor -= 1
if summary_opts.bubble_panel_cursor < 0 {
summary_opts.bubble_panel_cursor = panel_count - 1
}
summary_opts.bubble_edit_cursor = 0
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
}
}
}
if controller.active_screen == .Bubbles && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
panel_count := len(controller.state.page_layouts[page_idx].panels)
if panel_count > 0 {
summary_opts.bubble_panel_cursor += 1
if summary_opts.bubble_panel_cursor >= panel_count {
summary_opts.bubble_panel_cursor = 0
}
summary_opts.bubble_edit_cursor = 0
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.bubble_panel_cursor+1, panel_count))
}
}
}
if ctrl_down && rl.IsKeyPressed(.MINUS) {
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, autosave_secs-5))
}
@@ -690,10 +744,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
rl.ClearBackground(BG_BASE)
screen_w := rl.GetScreenWidth()
screen_h := rl.GetScreenHeight()
main_w := screen_w - 282 - 20
if main_w < 960 {
main_w = 960
}
main_w := shared.compute_main_width(screen_w)
rl.DrawRectangle(0, 0, 260, screen_h, BG_SIDEBAR)
rl.DrawRectangle(260, 0, screen_w-260, 72, BG_TOPBAR)
@@ -716,7 +767,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
draw_sidebar_shortcuts(screen_h_loop)
// --- Pipeline Stepper in Topbar ---
draw_card(rl.Rectangle{x = 282, y = 12, width = f32(main_w), height = 48})
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 12, width = f32(main_w), height = 48})
rl.DrawText(fmt.ctprintf("%s", ui.screen_name(controller.active_screen)), 300, 26, 20, BRAND_TITLE)
script_ok := len(controller.state.script.pages) > 0
@@ -768,8 +819,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
rl.DrawText("Local script pages", screen_w_loop - 250, 26, 16, TEXT_SECONDARY)
draw_input_field(pages_rec, local_script_pages, selected_field == 4)
draw_card(rl.Rectangle{x = 282, y = 82, width = f32(main_w), height = 356})
draw_card(rl.Rectangle{x = 282, y = 460, width = f32(main_w), height = 110})
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 82, width = f32(main_w), height = 356})
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = 460, width = f32(main_w), height = 110})
draw_section_title(300, 92, "Project Setup")
draw_section_title(300, 470, "Actions")
@@ -867,11 +918,11 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
}
}
draw_card(rl.Rectangle{x = 282, y = f32(lower_y_loop-140), width = f32(status_w_loop), height = 160})
draw_card(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width), y = f32(lower_y_loop-140), width = f32(status_w_loop), height = 160})
now_draw := rl.GetTime()
status_y := lower_y_loop - 126
rl.DrawText("Status", 300, status_y, 19, BRAND_TITLE)
rl.DrawLine(370, status_y+10, i32(282+status_w_loop)-14, status_y+10, BORDER_DIVIDER)
rl.DrawLine(370, status_y+10, i32(shared.LAYOUT.sidebar_width+status_w_loop)-14, status_y+10, BORDER_DIVIDER)
draw_text_fitted(status_msg, 300, status_y+26, 18, int(status_w_loop-36), 8, status_text_color(status_msg))
draw_readiness_row(controller, 300, status_y+50)
ready_count, total_count := ready_stage_count(controller)
@@ -901,7 +952,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
draw_small_button_state(path_fix_all_status_btn, "PE", !project_path_ok || !export_path_ok)
draw_screen_summary(controller, export_path, 300, lower_y_loop+12, status_w_loop-8, summary_opts)
if controller.active_screen == .Script || controller.active_screen == .Layout || controller.active_screen == .Panels {
if controller.active_screen == .Script || controller.active_screen == .Layout || controller.active_screen == .Panels || controller.active_screen == .Bubbles {
if controller.active_screen == .Script || controller.active_screen == .Layout {
show_txt := "Top"
sort_txt := "Asc"
@@ -932,10 +983,13 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
} else if controller.active_screen == .Layout {
draw_small_button(summary_prev_btn, "< Ly")
draw_small_button(summary_next_btn, "Ly >")
} else if controller.active_screen == .Bubbles {
draw_small_button(summary_prev_btn, "< Pn")
draw_small_button(summary_next_btn, "Pn >")
}
if !compact_mode {
hint_label := "Ctrl+[ / Ctrl+]"
draw_hint_pill(rl.Rectangle{x = f32(282 + status_w_loop - 186), y = f32(lower_y_loop + 46), width = 172, height = 20}, hint_label, false)
draw_hint_pill(rl.Rectangle{x = f32(shared.LAYOUT.sidebar_width + status_w_loop - 186), y = f32(lower_y_loop + 46), width = 172, height = 20}, hint_label, false)
}
}
if !compact_mode {
@@ -985,6 +1039,7 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
if regen {
msg := action_regenerate_page_layout(&controller, summary_opts.layout_page_cursor)
push_status(&status_msg, &action_log, msg)
is_dirty = true
}
wheel := rl.GetMouseWheelMove()
@@ -999,7 +1054,101 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
draw_text_fitted("Ctrl+H / Ctrl+J layout nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY)
draw_text_fitted("Ctrl+[ / Ctrl+] layout nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY)
} else if controller.active_screen == .Bubbles {
add_clicked, delete_clicked, auto_place_clicked, new_page_cursor, new_panel_cursor, new_bubble_cursor, edited_text, edited_type, type_changed, text_changed := draw_bubbles_detail_panel(
controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200,
summary_opts.bubble_page_cursor, summary_opts.bubble_panel_cursor, summary_opts.bubble_edit_cursor,
)
if summary_opts.bubble_page_cursor != new_page_cursor {
summary_opts.bubble_page_cursor = new_page_cursor
summary_opts.bubble_panel_cursor = 0
summary_opts.bubble_edit_cursor = 0
}
if summary_opts.bubble_panel_cursor != new_panel_cursor {
summary_opts.bubble_panel_cursor = new_panel_cursor
summary_opts.bubble_edit_cursor = 0
}
if summary_opts.bubble_edit_cursor != new_bubble_cursor {
summary_opts.bubble_edit_cursor = new_bubble_cursor
}
if add_clicked {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
layout := controller.state.page_layouts[page_idx]
if len(layout.panels) > 0 {
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
panel_id := layout.panels[panel_idx].panel_id
msg := action_add_bubble(&controller, panel_id)
push_status(&status_msg, &action_log, msg)
is_dirty = true
}
}
}
if delete_clicked {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
layout := controller.state.page_layouts[page_idx]
if len(layout.panels) > 0 {
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
panel_id := layout.panels[panel_idx].panel_id
msg := action_delete_bubble(&controller, panel_id, summary_opts.bubble_edit_cursor)
push_status(&status_msg, &action_log, msg)
is_dirty = true
if summary_opts.bubble_edit_cursor > 0 {
summary_opts.bubble_edit_cursor -= 1
}
}
}
}
if auto_place_clicked {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
layout := controller.state.page_layouts[page_idx]
if len(layout.panels) > 0 {
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
panel_id := layout.panels[panel_idx].panel_id
msg := action_auto_place_bubbles_for_panel(&controller, panel_id, layout)
push_status(&status_msg, &action_log, msg)
is_dirty = true
}
}
}
if type_changed {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
page_idx := clamp_layout_cursor(layout_page_count, summary_opts.bubble_page_cursor)
layout := controller.state.page_layouts[page_idx]
if len(layout.panels) > 0 {
panel_idx := clamp_bubble_cursor(len(layout.panels), summary_opts.bubble_panel_cursor)
panel_id := layout.panels[panel_idx].panel_id
msg := action_update_bubble(&controller, panel_id, summary_opts.bubble_edit_cursor, edited_type, "")
push_status(&status_msg, &action_log, msg)
is_dirty = true
}
}
}
wheel := rl.GetMouseWheelMove()
if wheel != 0 {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
summary_opts.bubble_panel_cursor -= int(wheel)
if summary_opts.bubble_panel_cursor < 0 {
summary_opts.bubble_panel_cursor = 0
}
panel_count := len(controller.state.page_layouts[summary_opts.bubble_page_cursor].panels)
if summary_opts.bubble_panel_cursor >= panel_count {
summary_opts.bubble_panel_cursor = panel_count - 1
}
summary_opts.bubble_edit_cursor = 0
}
}
draw_text_fitted("Ctrl+[ / Ctrl+] panel nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY)
} else {
draw_card(rl.Rectangle{x = f32(log_x_loop), y = f32(lower_y_loop), width = f32(main_w_loop-status_w_loop-2), height = 200})
draw_section_title(log_x_loop+18, lower_y_loop+6, "Action Log")
+85 -6
View File
@@ -176,8 +176,22 @@ draw_screen_summary :: proc(controller: ui.App_Controller, export_path: string,
}
}
case .Bubbles:
draw_summary_line(x, y+30, fmt.tprintf("Bubble maps: %d", len(controller.state.speech_bubbles)), rl.DARKGRAY)
rl.DrawText("Bubble editor is scaffolded", x, y+54, 18, rl.DARKGRAY)
bubble_count := 0
if controller.state.speech_bubbles != nil {
for _, bubbles in controller.state.speech_bubbles {
bubble_count += len(bubbles)
}
}
draw_summary_line(x, y+30, fmt.tprintf("Total bubbles: %d", bubble_count), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Layout pages: %d", len(controller.state.page_layouts)), rl.DARKGRAY)
if len(controller.state.page_layouts) == 0 {
rl.DrawText("No layouts yet. Run Layout Auto first.", x, y+86, 18, SUMMARY_HINT)
} else if bubble_count == 0 {
rl.DrawText("No bubbles yet. Use Auto Place or Add on Bubbles screen.", x, y+86, 18, SUMMARY_HINT)
} else {
draw_summary_line(x, y+78, fmt.tprintf("Panels with bubbles: %d", len(controller.state.speech_bubbles)), SUMMARY_ACCENT)
rl.DrawText("Select a panel in Bubbles screen to edit.", x, y+102, 18, SUMMARY_HINT)
}
case .Export:
draw_summary_line(x, y+30, fmt.tprintf("Format: %v", controller.state.export_format), rl.DARKGRAY)
draw_summary_line(x, y+54, fmt.tprintf("Layouts: %d | Panels: %d", len(controller.state.page_layouts), len(controller.state.panel_images)), rl.DARKGRAY)
@@ -455,6 +469,62 @@ clamp_layout_cursor :: proc(layout_count, cursor: int) -> int {
return cursor
}
Layout_Validation_Result :: struct {
coverage_pct: f32,
missing_bindings: int,
bounds_violations: int,
total_panels: int,
}
validate_layout_page :: proc(layout: core.Page_Layout, panel_images: map[string]core.Panel_Image) -> Layout_Validation_Result {
result: Layout_Validation_Result
result.total_panels = len(layout.panels)
// Coverage: sum of panel cell areas vs page area
page_area := f32(layout.width) * f32(layout.height)
covered_area: f32 = 0
for p in layout.panels {
cell_w := p.layout_cell.w * f32(layout.width)
cell_h := p.layout_cell.h * f32(layout.height)
covered_area += cell_w * cell_h
}
if page_area > 0 {
result.coverage_pct = covered_area / page_area * 100
}
// Missing bindings: panels without corresponding images
for p in layout.panels {
if _, has := panel_images[p.panel_id]; !has {
result.missing_bindings += 1
}
}
// Bounds violations: cells that extend outside [0,1] range
for p in layout.panels {
c := p.layout_cell
if c.x < 0 || c.y < 0 || c.x+c.w > 1.001 || c.y+c.h > 1.001 {
result.bounds_violations += 1
}
}
return result
}
draw_validation_badge :: proc(x, y, w: i32, label: string, ok: bool) {
bg := UNREADY_BG
border := UNREADY_BORDER
fg := WARNING
if ok {
bg = READY_BG
border = READY_BORDER
fg = SUCCESS
}
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 22}
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 6, bg)
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 6, 1.0, border)
draw_text_fitted(label, x+8, y+5, 11, int(w-16), 6, fg)
}
draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (regen_clicked: bool, new_cursor: int) {
new_cursor = cursor
regen_clicked = false
@@ -473,6 +543,15 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
// Header line with status
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • pattern: %s • %d panels", idx+1, layout_count, layout.pattern_id, len(layout.panels)), SUMMARY_ACCENT)
// Validation badges
val := validate_layout_page(layout, controller.state.panel_images)
coverage_ok := val.coverage_pct >= 80 && val.coverage_pct <= 105
bindings_ok := val.missing_bindings == 0
bounds_ok := val.bounds_violations == 0
draw_validation_badge(x+18, y+68, 100, fmt.tprintf("Cov: %.0f%%", val.coverage_pct), coverage_ok)
draw_validation_badge(x+124, y+68, 110, fmt.tprintf("Bind: %d miss", val.missing_bindings), bindings_ok)
draw_validation_badge(x+240, y+68, 100, fmt.tprintf("Bounds: %d", val.bounds_violations), bounds_ok)
// Regenerate button
btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24}
draw_small_button_state(btn_rec, "Regen", true)
@@ -481,11 +560,11 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
}
// Layout dimensions
draw_summary_subline(x+18, y+68, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
draw_summary_subline(x+18, y+94, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
// ── Mini wireframe preview ─────────────────────────────────
preview_x := x + 18
preview_y := y + 88
preview_y := y + 114
preview_max_w: f32 = f32(w) * 0.4
preview_max_h: f32 = f32(h - 100)
if preview_max_h < 40 {
@@ -533,10 +612,10 @@ draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
// ── Page list (right side) ─────────────────────────────────
list_x := x + i32(preview_max_w) + 36
list_y := y + 88
list_y := y + 114
list_w := w - i32(preview_max_w) - 54
row_h: i32 = 18
rows: i32 = (h - 100) / row_h
rows: i32 = (h - 126) / row_h
if rows < 1 {
rows = 1
}
+3
View File
@@ -8,6 +8,9 @@ Summary_View_Options :: struct {
layout_show_all: bool,
layout_desc: bool,
layout_page_cursor: int,
bubble_page_cursor: int,
bubble_panel_cursor: int,
bubble_edit_cursor: int,
}
Pending_Confirm_Action :: enum {