Compare commits
No commits in common. "rev" and "main" have entirely different histories.
@ -4,7 +4,6 @@ import json "core:encoding/json"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
|
||||||
import "../core"
|
import "../core"
|
||||||
import "../shared"
|
import "../shared"
|
||||||
|
|
||||||
@ -817,8 +816,7 @@ generate_comic_script :: proc(client: Deepseek_Client, cfg: shared.Config, opts:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attempt < attempts && shared.should_retry(last_err) {
|
if attempt < attempts && shared.should_retry(last_err) {
|
||||||
delay_ms := backoff_ms(client.initial_backoff_ms, attempt)
|
_ = backoff_ms(client.initial_backoff_ms, attempt)
|
||||||
time.sleep(time.Duration(delay_ms) * time.Millisecond)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import json "core:encoding/json"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
|
||||||
import "../core"
|
import "../core"
|
||||||
import "../shared"
|
import "../shared"
|
||||||
|
|
||||||
@ -300,8 +299,7 @@ generate_character_reference :: proc(client: Fal_Client, cfg: shared.Config, c:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attempt < attempts && shared.should_retry(last_err) {
|
if attempt < attempts && shared.should_retry(last_err) {
|
||||||
delay_ms := fal_backoff_ms(client.initial_backoff_ms, attempt)
|
_ = fal_backoff_ms(client.initial_backoff_ms, attempt)
|
||||||
time.sleep(time.Duration(delay_ms) * time.Millisecond)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -389,8 +387,7 @@ generate_panel_image :: proc(client: Fal_Client, cfg: shared.Config, panel: core
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attempt < attempts && shared.should_retry(last_err) {
|
if attempt < attempts && shared.should_retry(last_err) {
|
||||||
delay_ms := fal_backoff_ms(client.initial_backoff_ms, attempt)
|
_ = fal_backoff_ms(client.initial_backoff_ms, attempt)
|
||||||
time.sleep(time.Duration(delay_ms) * time.Millisecond)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@ -78,8 +78,8 @@ action_regenerate_panel :: proc(controller: ^ui.App_Controller, panel_id: string
|
|||||||
if controller.state.panel_images == nil {
|
if controller.state.panel_images == nil {
|
||||||
controller.state.panel_images = make(map[string]core.Panel_Image)
|
controller.state.panel_images = make(map[string]core.Panel_Image)
|
||||||
}
|
}
|
||||||
// Clone URL into owned heap storage so it survives across frames.
|
// Clone URL to persistent pool to survive frame resets
|
||||||
img.url = strings.clone(img.url)
|
img.url = pool_clone(img.url)
|
||||||
controller.state.panel_images[panel_id] = img
|
controller.state.panel_images[panel_id] = img
|
||||||
return fmt.aprintf("Generated panel %s", panel_id)
|
return fmt.aprintf("Generated panel %s", panel_id)
|
||||||
}
|
}
|
||||||
@ -393,15 +393,15 @@ run_panels_action :: proc(controller: ^ui.App_Controller, queue: ^adapters.Fal_G
|
|||||||
delete(img.prompt)
|
delete(img.prompt)
|
||||||
}
|
}
|
||||||
delete(controller.state.panel_images)
|
delete(controller.state.panel_images)
|
||||||
// Clone URLs/prompts into owned heap storage before storing
|
// Clone URLs to persistent pool before storing
|
||||||
cloned: map[string]core.Panel_Image
|
cloned: map[string]core.Panel_Image
|
||||||
for pid, img in images {
|
for pid, img in images {
|
||||||
cloned[pid] = core.Panel_Image{
|
cloned[pid] = core.Panel_Image{
|
||||||
url = strings.clone(img.url),
|
url = pool_clone(img.url),
|
||||||
width = img.width,
|
width = img.width,
|
||||||
height = img.height,
|
height = img.height,
|
||||||
seed = img.seed,
|
seed = img.seed,
|
||||||
prompt = strings.clone(img.prompt),
|
prompt = pool_clone(img.prompt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controller.state.panel_images = cloned
|
controller.state.panel_images = cloned
|
||||||
|
|||||||
@ -564,7 +564,7 @@ draw_layout_wireframe :: proc(app: ^GUI_App_State) {
|
|||||||
|
|
||||||
drawn_image := false
|
drawn_image := false
|
||||||
if panel_img, img_ok := app.controller.state.panel_images[panel_layout.panel_id]; img_ok {
|
if panel_img, img_ok := app.controller.state.panel_images[panel_layout.panel_id]; img_ok {
|
||||||
img_url := frame_pool_clone(panel_img.url)
|
img_url := pool_clone(panel_img.url)
|
||||||
tex, loaded := load_panel_texture(&app.panel_textures, panel_layout.panel_id, img_url)
|
tex, loaded := load_panel_texture(&app.panel_textures, panel_layout.panel_id, img_url)
|
||||||
if loaded && tex.id != 0 && cw > 4 && ch > 4 {
|
if loaded && tex.id != 0 && cw > 4 && ch > 4 {
|
||||||
scale_x := cw / f32(tex.width)
|
scale_x := cw / f32(tex.width)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ editor_open :: proc(app: ^GUI_App_State, panel_id: string) -> bool {
|
|||||||
panel_img, has_img := app.controller.state.panel_images[panel_id]
|
panel_img, has_img := app.controller.state.panel_images[panel_id]
|
||||||
if !has_img { return false }
|
if !has_img { return false }
|
||||||
|
|
||||||
img_url := frame_pool_clone(panel_img.url)
|
img_url := pool_clone(panel_img.url)
|
||||||
local_path := resolve_image_path(img_url)
|
local_path := resolve_image_path(img_url)
|
||||||
if len(local_path) == 0 { return false }
|
if len(local_path) == 0 { return false }
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import filepath "core:path/filepath"
|
|||||||
import "core:strings"
|
import "core:strings"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
import "../core"
|
import "../core"
|
||||||
import "../osdialog"
|
|
||||||
import "../shared"
|
import "../shared"
|
||||||
import "../ui"
|
import "../ui"
|
||||||
|
|
||||||
@ -64,53 +63,33 @@ GUI_App_State :: struct {
|
|||||||
prev_screen: ui.App_Screen,
|
prev_screen: ui.App_Screen,
|
||||||
slide_offset: f32, // horizontal offset for screen transitions
|
slide_offset: f32, // horizontal offset for screen transitions
|
||||||
slide_progress: f32, // 0→1 progress of slide animation
|
slide_progress: f32, // 0→1 progress of slide animation
|
||||||
// ─── Cached API-key presence (refreshed ~1/sec) ───────────────
|
|
||||||
// load_config() returns temp-allocated strings that only live one frame,
|
|
||||||
// so we can't keep the Config around — but we only need the booleans for
|
|
||||||
// the UI. Refreshing on a time interval avoids re-reading .env every frame.
|
|
||||||
cached_has_deepseek: bool,
|
|
||||||
cached_has_fal: bool,
|
|
||||||
config_check_time: f64, // wall-clock time of last refresh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clicked :: proc(id: clay.ElementId) -> bool {
|
clicked :: proc(id: clay.ElementId) -> bool {
|
||||||
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Per-frame String Pool ────────────────────────────────────────
|
// ─── Persistent String Pool (survives temp-allocator resets) ────
|
||||||
// This pool holds strings that must survive across calls *within* a single
|
|
||||||
// frame (e.g. a downloaded panel path handed from resolve_image_path to
|
|
||||||
// LoadTexture). It is reset every frame, so it never grows unbounded. Strings
|
|
||||||
// that must outlive the frame are stored via strings.clone into owned storage.
|
|
||||||
@(private)
|
@(private)
|
||||||
frame_pool: [256 * 1024]u8
|
persistent_pool: [256 * 1024]u8
|
||||||
@(private)
|
@(private)
|
||||||
frame_pool_offset: int
|
persistent_offset: int
|
||||||
|
|
||||||
// frame_pool_clone copies a string into the per-frame pool. The result is only
|
|
||||||
// valid until the next reset_frame_pool() call (once per frame).
|
|
||||||
@(private)
|
@(private)
|
||||||
frame_pool_clone :: proc(s: string) -> string {
|
pool_clone :: proc(s: string) -> string {
|
||||||
if len(s) == 0 { return "" }
|
if len(s) == 0 { return "" }
|
||||||
total := len(s) + 1
|
total := len(s) + 1
|
||||||
if frame_pool_offset + total > len(frame_pool) {
|
if persistent_offset + total > len(persistent_pool) {
|
||||||
// Pool full — fall back to strings.clone (heap) instead of corrupting old data
|
// Pool full — fall back to strings.clone (heap) instead of corrupting old data
|
||||||
return strings.clone(s)
|
return strings.clone(s)
|
||||||
}
|
}
|
||||||
start := frame_pool_offset
|
start := persistent_offset
|
||||||
for c, i in s {
|
for c, i in s {
|
||||||
frame_pool[start + i] = u8(c)
|
persistent_pool[start + i] = u8(c)
|
||||||
}
|
}
|
||||||
frame_pool[start + len(s)] = 0
|
persistent_pool[start + len(s)] = 0
|
||||||
frame_pool_offset += total
|
persistent_offset += total
|
||||||
return string(frame_pool[start:start+len(s)])
|
return string(persistent_pool[start:start+len(s)])
|
||||||
}
|
|
||||||
|
|
||||||
// reset_frame_pool reclaims the entire per-frame pool. Called once per frame
|
|
||||||
// at the top of the loop, before any frame_pool_clone use.
|
|
||||||
@(private)
|
|
||||||
reset_frame_pool :: proc() {
|
|
||||||
frame_pool_offset = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Panel Image Loading ─────────────────────────────────────────
|
// ─── Panel Image Loading ─────────────────────────────────────────
|
||||||
@ -138,8 +117,7 @@ resolve_image_path :: proc(url: string) -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cached_path, ok := download_path_cache[url]; ok {
|
if cached_path, ok := download_path_cache[url]; ok {
|
||||||
// Cache stores heap-owned strings; return a per-frame clone for the caller.
|
return pool_clone(cached_path)
|
||||||
return frame_pool_clone(cached_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, failed := download_failed_cache[url]; failed {
|
if _, failed := download_failed_cache[url]; failed {
|
||||||
@ -160,9 +138,8 @@ resolve_image_path :: proc(url: string) -> string {
|
|||||||
local_path := fmt.aprintf("%s/%s", cache_dir, filename)
|
local_path := fmt.aprintf("%s/%s", cache_dir, filename)
|
||||||
|
|
||||||
if os.exists(local_path) {
|
if os.exists(local_path) {
|
||||||
// Store a heap-owned copy in the persistent cache map.
|
download_path_cache[url] = pool_clone(local_path)
|
||||||
download_path_cache[url] = strings.clone(local_path)
|
return pool_clone(local_path)
|
||||||
return frame_pool_clone(local_path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := [6]string{"curl", "-L", "-sS", "-o", local_path, url}
|
cmd := [6]string{"curl", "-L", "-sS", "-o", local_path, url}
|
||||||
@ -173,8 +150,8 @@ resolve_image_path :: proc(url: string) -> string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
download_path_cache[url] = strings.clone(local_path)
|
download_path_cache[url] = pool_clone(local_path)
|
||||||
return frame_pool_clone(local_path)
|
return pool_clone(local_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
@ -263,27 +240,15 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
compact_mode := shared.is_compact(screen_h)
|
compact_mode := shared.is_compact(screen_h)
|
||||||
dt := rl.GetFrameTime()
|
dt := rl.GetFrameTime()
|
||||||
|
|
||||||
// Reclaim the per-frame string pool before any frame_pool_clone use.
|
|
||||||
reset_frame_pool()
|
|
||||||
|
|
||||||
update_sidebar_anim(&app, bp, dt)
|
update_sidebar_anim(&app, bp, dt)
|
||||||
update_overlay_anim(&app, dt)
|
update_overlay_anim(&app, dt)
|
||||||
update_slide_anim(&app, dt)
|
update_slide_anim(&app, dt)
|
||||||
|
|
||||||
sidebar_w := sidebar_width(bp, app.sidebar_collapsed, app.sidebar_anim)
|
sidebar_w := sidebar_width(bp, app.sidebar_collapsed, app.sidebar_anim)
|
||||||
main_w := shared.compute_main_width(screen_w, sidebar_w)
|
main_w := shared.compute_main_width(screen_w, sidebar_w)
|
||||||
|
|
||||||
// load_config() returns temp-allocated strings (one frame of life), so
|
|
||||||
// we only cache the booleans we actually need and refresh ~1/sec instead
|
|
||||||
// of re-reading .env + environment every frame.
|
|
||||||
if rl.GetTime() - app.config_check_time >= 1.0 {
|
|
||||||
cfg := shared.load_config()
|
cfg := shared.load_config()
|
||||||
app.cached_has_deepseek = len(cfg.deepseek_api_key) > 0
|
has_deepseek_key := len(cfg.deepseek_api_key) > 0
|
||||||
app.cached_has_fal = len(cfg.fal_api_key) > 0
|
has_fal_key := len(cfg.fal_api_key) > 0
|
||||||
app.config_check_time = rl.GetTime()
|
|
||||||
}
|
|
||||||
has_deepseek_key := app.cached_has_deepseek
|
|
||||||
has_fal_key := app.cached_has_fal
|
|
||||||
|
|
||||||
clay_update_dimensions(screen_w, screen_h)
|
clay_update_dimensions(screen_w, screen_h)
|
||||||
clay_update_input()
|
clay_update_input()
|
||||||
@ -619,10 +584,8 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
push_status_if_nonempty(&app.status_msg, &app.action_log, autosave_tick_with_message(&app.project_path, app.controller.state, app.autosave_enabled, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, app.autosave_interval_s))
|
push_status_if_nonempty(&app.status_msg, &app.action_log, autosave_tick_with_message(&app.project_path, app.controller.state, app.autosave_enabled, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, app.autosave_interval_s))
|
||||||
|
|
||||||
// ─── Clay Layout Declaration ──────────────────────────────
|
// ─── Clay Layout Declaration ──────────────────────────────
|
||||||
if app.editor.show_debug_overlay {
|
|
||||||
fmt.eprintf("LAYOUT: screen=%dx%d sidebar_w=%.0f sidebar_anim=%.0f collapsed=%v bp=%v\n",
|
fmt.eprintf("LAYOUT: screen=%dx%d sidebar_w=%.0f sidebar_anim=%.0f collapsed=%v bp=%v\n",
|
||||||
screen_w, screen_h, f32(sidebar_width(bp, app.sidebar_collapsed, 0)), app.sidebar_anim, app.sidebar_collapsed, bp)
|
screen_w, screen_h, f32(sidebar_width(bp, app.sidebar_collapsed, 0)), app.sidebar_anim, app.sidebar_collapsed, bp)
|
||||||
}
|
|
||||||
clay.BeginLayout()
|
clay.BeginLayout()
|
||||||
|
|
||||||
// Root: horizontal layout (sidebar + main)
|
// Root: horizontal layout (sidebar + main)
|
||||||
@ -690,13 +653,9 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|||||||
rl.EndDrawing()
|
rl.EndDrawing()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The controller took the state by value in new_controller, so it owns its
|
core.dispose_state(state)
|
||||||
// own copy of every heap-backed field it allocated or reassigned during the
|
state^ = app.controller.state
|
||||||
// session. Dispose that copy here. The caller's `state` (owned by main())
|
app.controller.state = core.Comic_State{}
|
||||||
// is left untouched for its own defer to clean up. We must NOT move the
|
|
||||||
// controller's state back into `state` — the two copies may share backing
|
|
||||||
// arrays (e.g. workflow.completed_steps), which would double-free.
|
|
||||||
core.dispose_state(&app.controller.state)
|
|
||||||
return shared.ok()
|
return shared.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -730,41 +689,6 @@ handle_format_clicks :: proc(app: ^GUI_App_State, has_deepseek: bool) {
|
|||||||
if clicked(clay.ID("btn_cbz")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty)) }
|
if clicked(clay.ID("btn_cbz")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Native file dialogs (osdialog) ──────────────────────────────────
|
|
||||||
// The dialog returns a temp-allocator string; we clone it into the persistent
|
|
||||||
// path field. Guard with WINDOW_TOPMOST so the native dialog stays above the
|
|
||||||
// borderless raylib window on platforms that lose focus otherwise.
|
|
||||||
handle_browse_clicks :: proc(app: ^GUI_App_State) {
|
|
||||||
// Open Project — native .comic.json picker
|
|
||||||
if clicked(clay.ID("btn_browse_project")) {
|
|
||||||
picked := osdialog.open_file_dialog("", "Project Files:comic.json")
|
|
||||||
if len(picked) > 0 {
|
|
||||||
delete(app.project_path)
|
|
||||||
app.project_path = strings.clone(picked)
|
|
||||||
app.is_dirty = true
|
|
||||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Project: %s", app.project_path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Export path — native save dialog, filter by current format
|
|
||||||
if clicked(clay.ID("btn_browse_export")) {
|
|
||||||
ext: string = "pdf"
|
|
||||||
switch app.export_format {
|
|
||||||
case .PDF: ext = "pdf"
|
|
||||||
case .PNG: ext = "png"
|
|
||||||
case .CBZ: ext = "cbz"
|
|
||||||
}
|
|
||||||
filters_str := fmt.tprintf("Export:%s", ext)
|
|
||||||
picked := osdialog.save_file_dialog("", "comic", filters_str)
|
|
||||||
if len(picked) > 0 {
|
|
||||||
delete(app.export_path)
|
|
||||||
app.export_path = strings.clone(picked)
|
|
||||||
app.is_dirty = true
|
|
||||||
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Export: %s", app.export_path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, pages_count: int, shift_down: bool, proj_ok, export_ok: bool, autosave_secs: int, has_fal_key: bool) {
|
handle_action_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export: bool, pages_count: int, shift_down: bool, proj_ok, export_ok: bool, autosave_secs: int, has_fal_key: bool) {
|
||||||
if clicked(clay.ID("btn_new")) {
|
if clicked(clay.ID("btn_new")) {
|
||||||
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Reset_Project, "Confirm reset?")) }
|
if app.is_dirty && !shift_down { push_status(&app.status_msg, &app.action_log, request_confirmation(&app.show_confirm_overlay, &app.show_help_overlay, &app.pending_confirm, .Reset_Project, "Confirm reset?")) }
|
||||||
@ -1191,7 +1115,6 @@ process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_expo
|
|||||||
handle_nav_clicks(app)
|
handle_nav_clicks(app)
|
||||||
handle_field_clicks(app)
|
handle_field_clicks(app)
|
||||||
handle_format_clicks(app, has_deepseek)
|
handle_format_clicks(app, has_deepseek)
|
||||||
handle_browse_clicks(app)
|
|
||||||
handle_action_clicks(app, can_gen_panels, can_layout, can_export, pages_count, shift_down, proj_ok, export_ok, autosave_secs, has_fal_key)
|
handle_action_clicks(app, can_gen_panels, can_layout, can_export, pages_count, shift_down, proj_ok, export_ok, autosave_secs, has_fal_key)
|
||||||
handle_workspace_nav(app)
|
handle_workspace_nav(app)
|
||||||
handle_detail_clicks(app)
|
handle_detail_clicks(app)
|
||||||
|
|||||||
@ -149,7 +149,7 @@ declare_panel_card :: proc(app: ^GUI_App_State, panel: core.Panel, page_num, pan
|
|||||||
}
|
}
|
||||||
|
|
||||||
if panel_img, has_img := app.controller.state.panel_images[panel.panel_id]; has_img {
|
if panel_img, has_img := app.controller.state.panel_images[panel.panel_id]; has_img {
|
||||||
img_url := frame_pool_clone(panel_img.url)
|
img_url := pool_clone(panel_img.url)
|
||||||
_, loaded := load_panel_texture(&app.panel_textures, panel.panel_id, img_url)
|
_, loaded := load_panel_texture(&app.panel_textures, panel.panel_id, img_url)
|
||||||
if loaded {
|
if loaded {
|
||||||
tex_ptr := &app.panel_textures[panel.panel_id]
|
tex_ptr := &app.panel_textures[panel.panel_id]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user