- Tool shortcuts (editor active): B=Pen, E=Eraser, L=Line, R=Rect, C=Circle, I=Color Pick - Brush size: [/] keys decrease/increase by 2 - Zoom: 0=reset, +=zoom in, -=zoom out - Ctrl+Z=undo, Ctrl+S=commit (save edit) - A=toggle adjustments panel - Skip global hotkeys (Ctrl+B sidebar, number nav) when editor active - All 156 tests pass.
1278 lines
57 KiB
Odin
1278 lines
57 KiB
Odin
package gui
|
|
|
|
import "core:c"
|
|
import clay "clay:."
|
|
import "core:fmt"
|
|
import "core:math"
|
|
import "core:os"
|
|
import filepath "core:path/filepath"
|
|
import "core:strings"
|
|
import rl "vendor:raylib"
|
|
import "../core"
|
|
import "../shared"
|
|
import "../ui"
|
|
|
|
// GUI_App_State holds all mutable GUI-local state for the main loop.
|
|
FIELD_BUF_SIZE :: 1024
|
|
|
|
ANIM_DURATION_SIDEBAR :: 0.25
|
|
ANIM_DURATION_OVERLAY :: 0.2
|
|
ANIM_DURATION_SLIDE :: 0.3
|
|
|
|
GUI_App_State :: struct {
|
|
controller: ui.App_Controller,
|
|
selected_field: int,
|
|
export_path: string,
|
|
project_path: string,
|
|
local_script_pages: string,
|
|
autosave_interval_text: string,
|
|
export_format: core.Export_Format,
|
|
use_fal_panels: bool,
|
|
sidebar_collapsed: bool,
|
|
status_msg: string,
|
|
is_dirty: bool,
|
|
autosave_enabled: bool,
|
|
autosave_interval_s: f64,
|
|
last_autosave_at: f64,
|
|
last_save_at: f64,
|
|
last_export_at: f64,
|
|
action_log: Action_Log,
|
|
log_show_lines: i32,
|
|
log_oldest_first: bool,
|
|
summary_opts: Summary_View_Options,
|
|
show_help_overlay: bool,
|
|
show_confirm_overlay: bool,
|
|
pending_confirm: Pending_Confirm_Action,
|
|
panel_textures: map[string]rl.Texture2D,
|
|
wireframe_cell_rects: [16]rl.Rectangle,
|
|
wireframe_cell_count: int,
|
|
layout_selected_panel: int,
|
|
bubble_edit_text: string,
|
|
editing_char_id: string,
|
|
char_edit_text: string,
|
|
editing_panel_id: string,
|
|
panel_edit_text: string,
|
|
field_buf: [FIELD_BUF_SIZE]u8,
|
|
// ─── Panel Editor state ──────────────────────────────────────
|
|
editor: Panel_Editor_State,
|
|
// ─── Pen tablet state ────────────────────────────────────────
|
|
pen: Pen_Tablet_State,
|
|
// ─── Animation state ──────────────────────────────────────────
|
|
sidebar_anim: f32, // current animated sidebar width (px)
|
|
overlay_alpha: f32, // 0→1 fade progress for active overlay
|
|
prev_screen: ui.App_Screen,
|
|
slide_offset: f32, // horizontal offset for screen transitions
|
|
slide_progress: f32, // 0→1 progress of slide animation
|
|
}
|
|
|
|
clicked :: proc(id: clay.ElementId) -> bool {
|
|
return clay.PointerOver(id) && rl.IsMouseButtonPressed(.LEFT)
|
|
}
|
|
|
|
// ─── Persistent String Pool (survives temp-allocator resets) ────
|
|
@(private)
|
|
persistent_pool: [256 * 1024]u8
|
|
@(private)
|
|
persistent_offset: int
|
|
|
|
@(private)
|
|
pool_clone :: proc(s: string) -> string {
|
|
if len(s) == 0 { return "" }
|
|
total := len(s) + 1
|
|
if persistent_offset + total > len(persistent_pool) {
|
|
// Pool full — fall back to strings.clone (heap) instead of corrupting old data
|
|
return strings.clone(s)
|
|
}
|
|
start := persistent_offset
|
|
for c, i in s {
|
|
persistent_pool[start + i] = u8(c)
|
|
}
|
|
persistent_pool[start + len(s)] = 0
|
|
persistent_offset += total
|
|
return string(persistent_pool[start:start+len(s)])
|
|
}
|
|
|
|
// ─── Panel Image Loading ─────────────────────────────────────────
|
|
@(private)
|
|
download_path_cache: map[string]string
|
|
@(private)
|
|
download_failed_cache: map[string]bool
|
|
|
|
@(private)
|
|
resolve_image_path :: proc(url: string) -> string {
|
|
if download_path_cache == nil {
|
|
download_path_cache = make(map[string]string, context.allocator)
|
|
}
|
|
if download_failed_cache == nil {
|
|
download_failed_cache = make(map[string]bool, context.allocator)
|
|
}
|
|
|
|
if strings.has_prefix(url, "file://") {
|
|
return url[7:]
|
|
}
|
|
if !strings.has_prefix(url, "http://") && !strings.has_prefix(url, "https://") {
|
|
// Reject non-URL garbage (could be corrupted temp-allocator data)
|
|
if len(url) == 0 || url[0] < 32 || url[0] > 126 { return "" }
|
|
return url
|
|
}
|
|
|
|
if cached_path, ok := download_path_cache[url]; ok {
|
|
return pool_clone(cached_path)
|
|
}
|
|
|
|
if _, failed := download_failed_cache[url]; failed {
|
|
return ""
|
|
}
|
|
|
|
base := url
|
|
if q := strings.index(base, "?"); q >= 0 {
|
|
base = base[:q]
|
|
}
|
|
filename := filepath.base(base)
|
|
if len(filename) == 0 {
|
|
filename = "cached_image.png"
|
|
}
|
|
|
|
cache_dir := "./assets"
|
|
os.mkdir_all(cache_dir)
|
|
local_path := fmt.aprintf("%s/%s", cache_dir, filename)
|
|
|
|
if os.exists(local_path) {
|
|
download_path_cache[url] = pool_clone(local_path)
|
|
return pool_clone(local_path)
|
|
}
|
|
|
|
cmd := [6]string{"curl", "-L", "-sS", "-o", local_path, url}
|
|
desc := os.Process_Desc{command = cmd[:]}
|
|
state, _, _, exec_err := os.process_exec(desc, context.temp_allocator)
|
|
if exec_err != nil || !state.exited || state.exit_code != 0 {
|
|
download_failed_cache[url] = true
|
|
return ""
|
|
}
|
|
|
|
download_path_cache[url] = pool_clone(local_path)
|
|
return pool_clone(local_path)
|
|
}
|
|
|
|
@(private)
|
|
load_panel_texture :: proc(cache: ^map[string]rl.Texture2D, panel_id: string, url: string) -> (rl.Texture2D, bool) {
|
|
if tex, ok := cache[panel_id]; ok {
|
|
return tex, true
|
|
}
|
|
filepath := resolve_image_path(url)
|
|
if len(filepath) == 0 { return {}, false }
|
|
fpath_c := strings.clone_to_cstring(filepath)
|
|
defer delete(fpath_c)
|
|
tex := rl.LoadTexture(fpath_c)
|
|
if tex.id == 0 {
|
|
// Fallback: try LoadImage + LoadTextureFromImage for broader format support
|
|
img := rl.LoadImage(fpath_c)
|
|
if img.data != nil {
|
|
defer rl.UnloadImage(img)
|
|
tex = rl.LoadTextureFromImage(img)
|
|
}
|
|
}
|
|
if tex.id == 0 { return {}, false }
|
|
rl.SetTextureFilter(tex, .BILINEAR)
|
|
cache[panel_id] = tex
|
|
return tex, true
|
|
|
|
}
|
|
@(private)
|
|
unload_panel_textures :: proc(cache: ^map[string]rl.Texture2D) {
|
|
for _, tex in cache {
|
|
rl.UnloadTexture(tex)
|
|
}
|
|
delete(cache^)
|
|
}
|
|
|
|
run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
|
controller := ui.new_controller(state^)
|
|
defer ui.dispose_job_manager(&controller.jobs)
|
|
|
|
rl.SetConfigFlags({.WINDOW_RESIZABLE, .WINDOW_UNDECORATED})
|
|
rl.InitWindow(1240, 820, "comic-odin gui")
|
|
defer rl.CloseWindow()
|
|
monitor := rl.GetCurrentMonitor()
|
|
monitor_pos := rl.GetMonitorPosition(monitor)
|
|
rl.SetWindowPosition(i32(monitor_pos.x), i32(monitor_pos.y))
|
|
rl.SetWindowSize(rl.GetMonitorWidth(monitor), rl.GetMonitorHeight(monitor))
|
|
rl.SetWindowState({.BORDERLESS_WINDOWED_MODE})
|
|
rl.SetTargetFPS(60)
|
|
|
|
clay_init(1240, 820)
|
|
defer clay_shutdown()
|
|
|
|
app: GUI_App_State
|
|
app.controller = controller
|
|
app.panel_textures = make(map[string]rl.Texture2D, 64)
|
|
reserve(&app.panel_textures, 256)
|
|
app.layout_selected_panel = -1
|
|
app.selected_field = 0
|
|
app.export_path = "./gui_export.pdf"
|
|
app.project_path = "./gui_project.comic.json"
|
|
app.local_script_pages = "2"
|
|
app.autosave_interval_text = "20"
|
|
app.export_format = .PDF
|
|
app.use_fal_panels = false
|
|
app.status_msg = fmt.aprintf("GUI ready")
|
|
app.autosave_enabled = true
|
|
app.autosave_interval_s = 20
|
|
app.last_autosave_at = rl.GetTime()
|
|
app.last_save_at = -1
|
|
app.last_export_at = -1
|
|
app.log_oldest_first = false
|
|
app.summary_opts = Summary_View_Options{script_show_all = true, script_desc = true}
|
|
app.sidebar_anim = f32(shared.LAYOUT.sidebar_width)
|
|
app.prev_screen = app.controller.active_screen
|
|
push_status(&app.status_msg, &app.action_log, app.status_msg)
|
|
defer action_log_dispose(&app.action_log)
|
|
defer delete(app.status_msg)
|
|
defer unload_panel_textures(&app.panel_textures)
|
|
defer editor_close(&app)
|
|
pen_tablet_init(&app.pen)
|
|
defer pen_tablet_cleanup(&app.pen)
|
|
|
|
for !rl.WindowShouldClose() {
|
|
screen_w := rl.GetScreenWidth()
|
|
screen_h := rl.GetScreenHeight()
|
|
bp := shared.breakpoint(screen_w, screen_h)
|
|
compact_mode := shared.is_compact(screen_h)
|
|
dt := rl.GetFrameTime()
|
|
|
|
update_sidebar_anim(&app, bp, dt)
|
|
update_overlay_anim(&app, dt)
|
|
update_slide_anim(&app, dt)
|
|
|
|
sidebar_w := sidebar_width(bp, app.sidebar_collapsed, app.sidebar_anim)
|
|
main_w := shared.compute_main_width(screen_w, sidebar_w)
|
|
cfg := shared.load_config()
|
|
has_deepseek_key := len(cfg.deepseek_api_key) > 0
|
|
has_fal_key := len(cfg.fal_api_key) > 0
|
|
|
|
clay_update_dimensions(screen_w, screen_h)
|
|
clay_update_input()
|
|
|
|
// ─── Keyboard Input ──────────────────────────────────────
|
|
interaction_locked := app.show_help_overlay || app.show_confirm_overlay
|
|
|
|
if rl.IsKeyPressed(.SLASH) { toggle_help_overlay(&app.show_help_overlay) }
|
|
if rl.IsKeyPressed(.ESCAPE) {
|
|
if app.editor.active {
|
|
editor_close(&app)
|
|
push_status(&app.status_msg, &app.action_log, "Edit cancelled")
|
|
} else {
|
|
close_help_overlay_if_open(&app.show_help_overlay)
|
|
}
|
|
}
|
|
|
|
if app.editor.active && !interaction_locked {
|
|
if rl.IsKeyPressed(.B) && !rl.IsKeyDown(.LEFT_CONTROL) && !rl.IsKeyDown(.RIGHT_CONTROL) {
|
|
app.editor.current_tool = .Pen
|
|
}
|
|
if rl.IsKeyPressed(.E) {
|
|
app.editor.current_tool = .Eraser
|
|
}
|
|
if rl.IsKeyPressed(.L) {
|
|
app.editor.current_tool = .Line
|
|
}
|
|
if rl.IsKeyPressed(.R) {
|
|
app.editor.current_tool = .Rectangle
|
|
}
|
|
if rl.IsKeyPressed(.C) && !rl.IsKeyDown(.LEFT_CONTROL) && !rl.IsKeyDown(.RIGHT_CONTROL) {
|
|
app.editor.current_tool = .Circle
|
|
}
|
|
if rl.IsKeyPressed(.I) {
|
|
app.editor.current_tool = .Color_Pick
|
|
}
|
|
if rl.IsKeyPressed(.LEFT_BRACKET) {
|
|
app.editor.brush_size = max(app.editor.brush_size - 2, 1)
|
|
}
|
|
if rl.IsKeyPressed(.RIGHT_BRACKET) {
|
|
app.editor.brush_size = min(app.editor.brush_size + 2, 80)
|
|
}
|
|
if rl.IsKeyPressed(.ZERO) {
|
|
app.editor.zoom = 1.0
|
|
app.editor.pan_offset = rl.Vector2{}
|
|
app.editor.needs_redraw = true
|
|
}
|
|
if rl.IsKeyPressed(.EQUAL) || rl.IsKeyPressed(.KP_ADD) {
|
|
app.editor.zoom = min(app.editor.zoom + 0.25, 8.0)
|
|
app.editor.needs_redraw = true
|
|
}
|
|
if rl.IsKeyPressed(.MINUS) || rl.IsKeyPressed(.KP_SUBTRACT) {
|
|
app.editor.zoom = max(app.editor.zoom - 0.25, 0.25)
|
|
app.editor.needs_redraw = true
|
|
}
|
|
if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) {
|
|
if rl.IsKeyPressed(.Z) {
|
|
editor_undo(&app.editor)
|
|
}
|
|
if rl.IsKeyPressed(.S) {
|
|
push_status(&app.status_msg, &app.action_log, editor_commit(&app))
|
|
}
|
|
}
|
|
if rl.IsKeyPressed(.A) && !rl.IsKeyDown(.LEFT_CONTROL) && !rl.IsKeyDown(.RIGHT_CONTROL) {
|
|
app.editor.show_adjust_panel = !app.editor.show_adjust_panel
|
|
}
|
|
}
|
|
|
|
// Compact mode forces the sidebar collapsed
|
|
if bp == .Compact { app.sidebar_collapsed = true }
|
|
|
|
if !interaction_locked && !app.editor.active {
|
|
if rl.IsKeyPressed(.B) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) {
|
|
app.sidebar_collapsed = !app.sidebar_collapsed
|
|
status := "Sidebar expanded"
|
|
if app.sidebar_collapsed { status = "Sidebar collapsed" }
|
|
push_status(&app.status_msg, &app.action_log, status)
|
|
}
|
|
if rl.IsKeyPressed(.ONE) { _ = ui.navigate_to_screen(&app.controller, .Story) }
|
|
if rl.IsKeyPressed(.TWO) { _ = ui.navigate_to_screen(&app.controller, .Script) }
|
|
if rl.IsKeyPressed(.THREE) { _ = ui.navigate_to_screen(&app.controller, .Characters) }
|
|
if rl.IsKeyPressed(.FOUR) { _ = ui.navigate_to_screen(&app.controller, .Panels) }
|
|
if rl.IsKeyPressed(.FIVE) { _ = ui.navigate_to_screen(&app.controller, .Layout) }
|
|
if rl.IsKeyPressed(.SIX) { _ = ui.navigate_to_screen(&app.controller, .Bubbles) }
|
|
if rl.IsKeyPressed(.SEVEN) { _ = ui.navigate_to_screen(&app.controller, .Export) }
|
|
if rl.IsKeyPressed(.EIGHT) { _ = ui.navigate_to_screen(&app.controller, .Community) }
|
|
if rl.IsKeyPressed(.TAB) { app.selected_field = (app.selected_field + 1) % 7 }
|
|
if rl.IsKeyPressed(.F1) { app.selected_field = 0 }
|
|
if rl.IsKeyPressed(.F2) { app.selected_field = 1 }
|
|
if rl.IsKeyPressed(.F3) { app.selected_field = 2 }
|
|
if rl.IsKeyPressed(.F4) { app.selected_field = 3 }
|
|
if rl.IsKeyPressed(.F11) { app.selected_field = 4 }
|
|
if rl.IsKeyPressed(.F12) { app.selected_field = 5 }
|
|
}
|
|
|
|
pages_count := parse_pages_or_default(app.local_script_pages, 2)
|
|
autosave_secs := parse_autosave_interval(app.autosave_interval_text, 20)
|
|
app.autosave_interval_s = f64(autosave_secs)
|
|
if app.selected_field != 6 && len(strings.trim_space(app.autosave_interval_text)) == 0 {
|
|
app.autosave_interval_text = fmt.aprintf("%d", autosave_secs)
|
|
}
|
|
shift_down := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
|
|
can_generate_panels := len(app.controller.state.script.pages) > 0
|
|
can_layout := len(app.controller.state.panel_images) > 0
|
|
can_export := len(app.controller.state.page_layouts) > 0 && len(app.controller.state.panel_images) > 0
|
|
project_path_ok := project_path_is_normalized(app.project_path)
|
|
export_path_ok := export_path_matches_format(app.export_path, app.export_format)
|
|
|
|
// ─── Confirm Overlay Logic ────────────────────────────────
|
|
if app.show_confirm_overlay {
|
|
confirm_yes := rl.IsKeyPressed(.ENTER) || rl.IsKeyPressed(.Y)
|
|
confirm_no := rl.IsKeyPressed(.ESCAPE) || rl.IsKeyPressed(.N)
|
|
if clicked(clay.ID("confirm_yes")) { confirm_yes = true }
|
|
if clicked(clay.ID("confirm_no")) { confirm_no = true }
|
|
if confirm_no {
|
|
app.show_confirm_overlay = false
|
|
app.pending_confirm = .None
|
|
push_status(&app.status_msg, &app.action_log, "Cancelled destructive action")
|
|
} else if confirm_yes {
|
|
action := app.pending_confirm
|
|
app.show_confirm_overlay = false
|
|
app.pending_confirm = .None
|
|
push_status(&app.status_msg, &app.action_log, resolve_confirm_action_with_message(action, &app.controller, &app.project_path, &app.export_path, app.export_format, &app.is_dirty, &app.last_autosave_at))
|
|
}
|
|
}
|
|
|
|
// ─── Text Input (raygui GuiTextBox overlay) ────────────────
|
|
// raygui GuiTextBox is called after Clay render below
|
|
|
|
// ─── Keyboard Shortcuts ─────────────────────────────────────
|
|
ctrl_down := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
|
|
if app.editor.active && ctrl_down && rl.IsKeyPressed(.Z) {
|
|
editor_undo(&app.editor)
|
|
}
|
|
if !interaction_locked {
|
|
if ctrl_down && rl.IsKeyPressed(.S) {
|
|
push_status(&app.status_msg, &app.action_log, save_project_session_with_message(&app.project_path, app.controller.state, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, "Saved project"))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.N) {
|
|
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?"))
|
|
} else {
|
|
push_status(&app.status_msg, &app.action_log, reset_project_session(&app.controller, &app.is_dirty, &app.last_autosave_at, true))
|
|
}
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.O) {
|
|
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, .Open_Project, "Confirm open?"))
|
|
} else {
|
|
push_status(&app.status_msg, &app.action_log, open_project_session(&app.controller, &app.project_path, &app.export_path, app.export_format, &app.is_dirty, &app.last_autosave_at))
|
|
}
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.E) {
|
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.H) {
|
|
push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_show_if_supported(app.controller.active_screen, &app.summary_opts))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.J) {
|
|
push_status_if_nonempty(&app.status_msg, &app.action_log, toggle_summary_sort_if_supported(app.controller.active_screen, &app.summary_opts))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.MINUS) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, autosave_secs-5))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.EQUAL) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, autosave_secs+5))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.SEVEN) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 15))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.EIGHT) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 30))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.NINE) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_autosave_interval_text(&app.autosave_interval_text, 60))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.ZERO) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, reset_helper_fields_with_message(&app.export_path, &app.local_script_pages, &app.autosave_interval_text, app.export_format))
|
|
}
|
|
if ctrl_down && !shift_down && rl.IsKeyPressed(.L) {
|
|
set_status(&app.status_msg, clear_action_log_with_message(&app.action_log))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.V) {
|
|
push_status_if_nonempty(&app.status_msg, &app.action_log, paste_clipboard_into_selected_field_with_message(app.selected_field, &app.controller.state, &app.export_path, &app.local_script_pages, &app.project_path, &app.autosave_interval_text, &app.is_dirty))
|
|
}
|
|
if ctrl_down && rl.IsKeyPressed(.BACKSPACE) {
|
|
push_status(&app.status_msg, &app.action_log, clear_selected_field_with_message(app.selected_field, &app.controller.state, &app.export_path, &app.local_script_pages, &app.project_path, &app.autosave_interval_text, &app.is_dirty))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.C) {
|
|
push_status(&app.status_msg, &app.action_log, copy_text_with_status(app.status_msg, "Copied status to clipboard"))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.A) {
|
|
push_status(&app.status_msg, &app.action_log, toggle_autosave_with_message(&app.autosave_enabled))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.X) {
|
|
push_status(&app.status_msg, &app.action_log, copy_text_with_status(app.export_path, "Copied export path to clipboard"))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.P) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_export_preset_with_message(&app.export_path, app.export_format))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.D) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_export_path_from_project_with_message(&app.export_path, app.project_path, app.export_format))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.G) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, set_project_path_from_export_with_message(&app.project_path, app.export_path))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.J) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_project_path_with_message(&app.project_path))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.K) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_project_path_with_message(&app.project_path))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.M) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_export_path_with_message(&app.export_path, app.export_format))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.U) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, fix_all_paths_with_message(&app.project_path, &app.export_path, app.export_format))
|
|
}
|
|
if ctrl_down && shift_down && rl.IsKeyPressed(.F) {
|
|
push_dirty_status(&app.is_dirty, &app.status_msg, &app.action_log, normalize_export_path_with_message(&app.export_path, app.export_format))
|
|
}
|
|
if rl.IsKeyPressed(.F5) {
|
|
push_status(&app.status_msg, &app.action_log, action_generate_deepseek_script(&app.controller, pages_count))
|
|
}
|
|
if rl.IsKeyPressed(.F6) {
|
|
push_status(&app.status_msg, &app.action_log, "Use FAL panel button to generate panels")
|
|
}
|
|
if rl.IsKeyPressed(.F7) {
|
|
push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty))
|
|
}
|
|
if rl.IsKeyPressed(.F8) {
|
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
if rl.IsKeyPressed(.F9) {
|
|
push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
if rl.IsKeyPressed(.F10) {
|
|
push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
}
|
|
|
|
// ─── Autosave ──────────────────────────────────────────────
|
|
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 ──────────────────────────────
|
|
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)
|
|
clay.BeginLayout()
|
|
|
|
// Root: horizontal layout (sidebar + main)
|
|
if clay.UI(clay.ID("Root"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .LeftToRight},
|
|
backgroundColor = CLAY_BG_BASE,
|
|
}) {
|
|
// ─── Sidebar (responsive width) ───────────────────────
|
|
if clay.UI(clay.ID("Sidebar"))({
|
|
layout = {sizing = {width = clay.SizingFixed(f32(sidebar_w)), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = 10, right = 10, bottom = 10, left = 10}, childGap = 4},
|
|
backgroundColor = CLAY_BG_SIDEBAR,
|
|
}) {
|
|
declare_sidebar(&app, bp)
|
|
}
|
|
|
|
// ─── Main Content ────────────────────────────────
|
|
if clay.UI(clay.ID("MainArea"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom},
|
|
}) {
|
|
declare_pipeline_bar(&app, bp)
|
|
declare_workspace(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, pages_count, bp, main_w)
|
|
next_hint := gui_next_hint(app.controller)
|
|
declare_bottom_bar(&app, can_generate_panels, can_layout, can_export, next_hint, has_fal_key, bp)
|
|
}
|
|
}
|
|
|
|
// ─── Floating Overlays (Clay, animated alpha) ─────────────
|
|
if app.show_help_overlay || app.overlay_alpha > 0.01 {
|
|
declare_help_overlay(bp, app.overlay_alpha)
|
|
}
|
|
if app.show_confirm_overlay || app.overlay_alpha > 0.01 {
|
|
declare_confirm_overlay(app.pending_confirm, bp, app.overlay_alpha)
|
|
}
|
|
declare_toast(&app.action_log)
|
|
|
|
render_commands := clay.EndLayout(rl.GetFrameTime())
|
|
|
|
// ─── Click Detection (after layout, before render) ────────
|
|
process_clicks(&app, can_generate_panels, can_layout, can_export, project_path_ok, export_path_ok, has_deepseek_key, has_fal_key, pages_count, shift_down, autosave_secs, compact_mode, bp)
|
|
|
|
// ─── Render ────────────────────────────────────────────────────
|
|
rl.BeginDrawing()
|
|
rl.EndScissorMode()
|
|
rl.ClearBackground(RL_BG_BASE)
|
|
clay_raylib_render(&render_commands)
|
|
|
|
// ─── Layout wireframe overlay (raylib, after Clay) ──────────────
|
|
if !app.editor.active {
|
|
draw_layout_wireframe(&app)
|
|
}
|
|
|
|
// ─── Panel Editor overlay (raylib, after Clay) ────────────────
|
|
if app.editor.active {
|
|
pen_tablet_poll(&app.pen)
|
|
editor_update(&app)
|
|
editor_render(&app)
|
|
}
|
|
|
|
// ─── Raygui field overlay (draw after Clay, before end) ─────────
|
|
if !interaction_locked {
|
|
render_raygui_fields(&app)
|
|
rl.EndScissorMode()
|
|
}
|
|
|
|
rl.EndDrawing()
|
|
}
|
|
|
|
core.dispose_state(state)
|
|
state^ = app.controller.state
|
|
app.controller.state = core.Comic_State{}
|
|
return shared.ok()
|
|
}
|
|
|
|
// ─── Click Handlers (called by process_clicks) ───────────────────
|
|
|
|
handle_nav_clicks :: proc(app: ^GUI_App_State) {
|
|
nav_screen := []ui.App_Screen{.Story, .Script, .Characters, .Panels, .Layout, .Bubbles, .Export, .Community}
|
|
for i in 0 ..< len(nav_screen) {
|
|
if clicked(clay.ID("Nav", u32(i))) {
|
|
push_status(&app.status_msg, &app.action_log, navigate_screen_with_status(&app.controller, nav_screen[i]))
|
|
}
|
|
}
|
|
pipeline_screens := []ui.App_Screen{.Script, .Panels, .Layout, .Export}
|
|
for i in 0 ..< len(pipeline_screens) {
|
|
if clicked(clay.ID("PStep", u32(i))) {
|
|
push_status(&app.status_msg, &app.action_log, navigate_screen_with_status(&app.controller, pipeline_screens[i]))
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_field_clicks :: proc(app: ^GUI_App_State) {
|
|
input_ids := []string{"field_idea", "field_genre", "field_audience", "field_export", "field_pages", "field_project"}
|
|
for i in 0 ..< len(input_ids) {
|
|
if clicked(clay.ID(input_ids[i])) { app.selected_field = i }
|
|
}
|
|
}
|
|
|
|
handle_format_clicks :: proc(app: ^GUI_App_State, has_deepseek: bool) {
|
|
if clicked(clay.ID("btn_pdf")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PDF, &app.is_dirty)) }
|
|
if clicked(clay.ID("btn_png")) { push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PNG, &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)) }
|
|
}
|
|
|
|
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 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?")) }
|
|
else { push_status(&app.status_msg, &app.action_log, reset_project_session(&app.controller, &app.is_dirty, &app.last_autosave_at, false)) }
|
|
}
|
|
if clicked(clay.ID("btn_script")) { push_status(&app.status_msg, &app.action_log, action_generate_deepseek_script(&app.controller, pages_count)) }
|
|
if clicked(clay.ID("btn_panels")) { push_status(&app.status_msg, &app.action_log, "Click [FAL] then [Panels] to generate via FAL") }
|
|
if clicked(clay.ID("btn_layout")) { push_status(&app.status_msg, &app.action_log, run_layout_action(&app.controller, can_layout, &app.is_dirty)) }
|
|
if clicked(clay.ID("btn_export")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
|
if clicked(clay.ID("btn_export_now")) { push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at)) }
|
|
if clicked(clay.ID("btn_export_png")) {
|
|
push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .PNG, &app.is_dirty))
|
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
if clicked(clay.ID("btn_export_cbz")) {
|
|
push_status(&app.status_msg, &app.action_log, set_export_format_with_message(&app.export_format, &app.export_path, .CBZ, &app.is_dirty))
|
|
push_status(&app.status_msg, &app.action_log, run_export_action(&app.controller, &app.export_path, app.export_format, can_export, &app.is_dirty, &app.last_export_at))
|
|
}
|
|
if clicked(clay.ID("btn_next")) { push_status(&app.status_msg, &app.action_log, run_next_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at)) }
|
|
if clicked(clay.ID("btn_auto")) { push_status(&app.status_msg, &app.action_log, run_auto_all_action(&app.controller, &app.export_path, app.export_format, pages_count, &app.is_dirty, &app.last_export_at)) }
|
|
if clicked(clay.ID("btn_save")) { push_status(&app.status_msg, &app.action_log, save_project_session_with_message(&app.project_path, app.controller.state, &app.is_dirty, &app.last_autosave_at, &app.last_save_at, "Saved project")) }
|
|
if clicked(clay.ID("btn_open")) {
|
|
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, .Open_Project, "Confirm open?")) }
|
|
else { push_status(&app.status_msg, &app.action_log, open_project_session(&app.controller, &app.project_path, &app.export_path, app.export_format, &app.is_dirty, &app.last_autosave_at)) }
|
|
}
|
|
if clicked(clay.ID("btn_autosave_toggle")) { push_status(&app.status_msg, &app.action_log, toggle_autosave_with_message(&app.autosave_enabled)) }
|
|
if clicked(clay.ID("btn_mode_casual")) { app.controller.state.user_mode = .Casual; push_status(&app.status_msg, &app.action_log, "Mode: Casual") }
|
|
if clicked(clay.ID("btn_mode_pro")) { app.controller.state.user_mode = .Professional; push_status(&app.status_msg, &app.action_log, "Mode: Professional") }
|
|
// Genre chips
|
|
genres := []string{"action", "comedy", "drama", "scifi", "fantasy", "horror", "romance", "mystery", "slice-of-life"}
|
|
for g in genres {
|
|
if clicked(clay.ID(fmt.tprintf("btn_genre_%s", g))) {
|
|
delete(app.controller.state.story_genre)
|
|
app.controller.state.story_genre = strings.clone(g)
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Genre: %s", g))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
// Art style chips
|
|
styles := []string{"manga", "western-comic", "pixel-art", "watercolor", "noir", "chibi", "sketch", "cyberpunk"}
|
|
for s in styles {
|
|
if clicked(clay.ID(fmt.tprintf("btn_style_%s", s))) {
|
|
delete(app.controller.state.art_style)
|
|
app.controller.state.art_style = strings.clone(s)
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Style: %s", s))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
// Generate All Refs button (Characters screen)
|
|
if clicked(clay.ID("btn_char_gen_all")) {
|
|
push_status(&app.status_msg, &app.action_log, action_generate_all_character_refs(&app.controller))
|
|
app.is_dirty = true
|
|
}
|
|
// Generate All Panels button
|
|
if clicked(clay.ID("btn_panels_gen_all")) {
|
|
cfg := shared.load_config()
|
|
if len(cfg.fal_api_key) == 0 {
|
|
push_status(&app.status_msg, &app.action_log, "FAL key missing")
|
|
} else {
|
|
push_status(&app.status_msg, &app.action_log, run_panels_action(&app.controller, nil, &app.is_dirty))
|
|
}
|
|
}
|
|
// Page size chips (Layout screen)
|
|
page_sizes := []core.Page_Size_Name{.A4, .Letter, .Manga, .Webtoon, .Square}
|
|
for sz, pi in page_sizes {
|
|
if clicked(clay.ID(fmt.tprintf("btn_pgsize_%d", pi))) {
|
|
app.controller.state.page_size = sz
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Page size: %v", sz))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
// Layout pattern cards
|
|
patterns := []string{"grid-2x2", "manga-3-tier", "action-dynamic", "splash-page", "webtoon-scroll", "western-3x3", "dialogue-heavy", "cinematic-widescreen"}
|
|
for pat in patterns {
|
|
if clicked(clay.ID(fmt.tprintf("btn_pattern_%s", pat))) {
|
|
if len(app.controller.state.panel_images) > 0 {
|
|
panels := collect_script_panels(app.controller.state.script)
|
|
core.dispose_page_layouts(&app.controller.state.page_layouts)
|
|
app.controller.state.page_layouts = core.auto_layout_pages(panels, app.controller.state.page_size, app.controller.state.story_genre, pat)
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Layout: %s", pat))
|
|
app.is_dirty = true
|
|
} else {
|
|
push_status(&app.status_msg, &app.action_log, "Generate panels first")
|
|
}
|
|
}
|
|
}
|
|
// Audience chips
|
|
auds := []string{"general", "children", "teen", "mature"}
|
|
for a in auds {
|
|
if clicked(clay.ID(fmt.tprintf("btn_aud_%s", a))) {
|
|
delete(app.controller.state.target_audience)
|
|
app.controller.state.target_audience = strings.clone(a)
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Audience: %s", a))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
if clicked(clay.ID("btn_help")) { toggle_help_overlay(&app.show_help_overlay) }
|
|
if clicked(clay.ID("btn_toggle_sidebar")) {
|
|
app.sidebar_collapsed = !app.sidebar_collapsed
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Sidebar: %s", "collapsed" if app.sidebar_collapsed else "expanded"))
|
|
}
|
|
if clicked(clay.ID("btn_fal_panels")) {
|
|
if !has_fal_key { push_status(&app.status_msg, &app.action_log, "FAL key missing (set FAL_API_KEY)") }
|
|
else { app.use_fal_panels = !app.use_fal_panels; push_status(&app.status_msg, &app.action_log, fmt.tprintf("FAL panels: %s", "ON" if app.use_fal_panels else "OFF")) }
|
|
}
|
|
|
|
diag_ctx := make_diagnostics_action_context(&app.controller, &app.action_log, app.is_dirty, app.autosave_enabled, proj_ok, export_ok, autosave_secs, app.project_path, app.export_path, app.log_show_lines, app.log_oldest_first)
|
|
if clicked(clay.ID("btn_log_reset")) { push_status(&app.status_msg, &app.action_log, reset_log_view_with_message(&app.log_show_lines, &app.log_oldest_first)) }
|
|
if clicked(clay.ID("btn_log_clear")) { set_status(&app.status_msg, clear_action_log_with_message(&app.action_log)) }
|
|
if clicked(clay.ID("btn_log_report")) { push_status(&app.status_msg, &app.action_log, write_session_report_with_message(diag_ctx)) }
|
|
if clicked(clay.ID("btn_log_copy")) { push_status(&app.status_msg, &app.action_log, copy_action_log_snapshot_with_message(diag_ctx)) }
|
|
if clicked(clay.ID("btn_log_diag")) { push_status(&app.status_msg, &app.action_log, write_diagnostics_with_message(diag_ctx)) }
|
|
if clicked(clay.ID("btn_log_status_copy")) { push_status(&app.status_msg, &app.action_log, copy_text_with_status(app.status_msg, "Copied status to clipboard")) }
|
|
if clicked(clay.ID("btn_log_diag_copy")) { push_status(&app.status_msg, &app.action_log, copy_diagnostics_with_message(diag_ctx)) }
|
|
}
|
|
|
|
handle_workspace_nav :: proc(app: ^GUI_App_State) {
|
|
screen := app.controller.active_screen
|
|
if screen == .Script {
|
|
page_count := len(app.controller.state.script.pages)
|
|
// Collapsible toggles
|
|
if clicked(clay.ID("btn_toggle_chars")) {
|
|
app.summary_opts.script_show_all = !app.summary_opts.script_show_all
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Characters: %s", "shown" if app.summary_opts.script_show_all else "hidden"))
|
|
}
|
|
if clicked(clay.ID("btn_toggle_panels")) {
|
|
app.summary_opts.script_desc = !app.summary_opts.script_desc
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Panels: %s", "shown" if app.summary_opts.script_desc else "hidden"))
|
|
}
|
|
if clicked(clay.ID("btn_script_prev")) && page_count > 0 {
|
|
app.summary_opts.script_page_cursor -= 1
|
|
if app.summary_opts.script_page_cursor < 0 { app.summary_opts.script_page_cursor = page_count - 1 }
|
|
}
|
|
if clicked(clay.ID("btn_script_next")) && page_count > 0 {
|
|
app.summary_opts.script_page_cursor += 1
|
|
if app.summary_opts.script_page_cursor >= page_count { app.summary_opts.script_page_cursor = 0 }
|
|
}
|
|
}
|
|
if screen == .Panels {
|
|
panel_count := count_script_panels(app.controller.state.script)
|
|
if clicked(clay.ID("btn_panels_prev")) && panel_count > 0 {
|
|
app.summary_opts.panel_cursor -= 1
|
|
if app.summary_opts.panel_cursor < 0 { app.summary_opts.panel_cursor = panel_count - 1 }
|
|
}
|
|
if clicked(clay.ID("btn_panels_next")) && panel_count > 0 {
|
|
app.summary_opts.panel_cursor += 1
|
|
if app.summary_opts.panel_cursor >= panel_count { app.summary_opts.panel_cursor = 0 }
|
|
}
|
|
}
|
|
if screen == .Layout {
|
|
layout_count := len(app.controller.state.page_layouts)
|
|
if clicked(clay.ID("btn_layout_prev")) && layout_count > 0 {
|
|
app.summary_opts.layout_page_cursor -= 1
|
|
if app.summary_opts.layout_page_cursor < 0 { app.summary_opts.layout_page_cursor = layout_count - 1 }
|
|
app.layout_selected_panel = -1
|
|
}
|
|
if clicked(clay.ID("btn_layout_next")) && layout_count > 0 {
|
|
app.summary_opts.layout_page_cursor += 1
|
|
if app.summary_opts.layout_page_cursor >= layout_count { app.summary_opts.layout_page_cursor = 0 }
|
|
app.layout_selected_panel = -1
|
|
}
|
|
}
|
|
if screen == .Bubbles {
|
|
layout_count := len(app.controller.state.page_layouts)
|
|
if clicked(clay.ID("btn_bubbles_prev_page")) && layout_count > 0 {
|
|
app.summary_opts.bubble_page_cursor -= 1
|
|
if app.summary_opts.bubble_page_cursor < 0 { app.summary_opts.bubble_page_cursor = layout_count - 1 }
|
|
app.summary_opts.bubble_panel_cursor = 0
|
|
app.summary_opts.bubble_edit_cursor = 0
|
|
}
|
|
if clicked(clay.ID("btn_bubbles_next_page")) && layout_count > 0 {
|
|
app.summary_opts.bubble_page_cursor += 1
|
|
if app.summary_opts.bubble_page_cursor >= layout_count { app.summary_opts.bubble_page_cursor = 0 }
|
|
app.summary_opts.bubble_panel_cursor = 0
|
|
app.summary_opts.bubble_edit_cursor = 0
|
|
}
|
|
if layout_count > 0 {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
panel_count := len(layout_val.panels)
|
|
if clicked(clay.ID("btn_bubbles_prev_panel")) && panel_count > 0 {
|
|
app.summary_opts.bubble_panel_cursor -= 1
|
|
if app.summary_opts.bubble_panel_cursor < 0 { app.summary_opts.bubble_panel_cursor = panel_count - 1 }
|
|
app.summary_opts.bubble_edit_cursor = 0
|
|
}
|
|
if clicked(clay.ID("btn_bubbles_next_panel")) && panel_count > 0 {
|
|
app.summary_opts.bubble_panel_cursor += 1
|
|
if app.summary_opts.bubble_panel_cursor >= panel_count { app.summary_opts.bubble_panel_cursor = 0 }
|
|
app.summary_opts.bubble_edit_cursor = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_detail_clicks :: proc(app: ^GUI_App_State) {
|
|
screen := app.controller.active_screen
|
|
|
|
if screen == .Characters {
|
|
char_count := len(app.controller.state.characters)
|
|
|
|
// Parse Descriptions button
|
|
if clicked(clay.ID("btn_char_parse_all")) {
|
|
push_status(&app.status_msg, &app.action_log, action_parse_character_descriptions(&app.controller))
|
|
app.is_dirty = true
|
|
}
|
|
|
|
for i in 0..<char_count {
|
|
char := app.controller.state.characters[i]
|
|
is_editing := app.editing_char_id == char.id
|
|
|
|
if is_editing {
|
|
if clicked(clay.ID(fmt.tprintf("btn_char_save_%s", char.id))) {
|
|
push_status(&app.status_msg, &app.action_log, action_update_character_desc(&app.controller, char.id, strings.clone(app.char_edit_text)))
|
|
app.editing_char_id = ""
|
|
app.selected_field = 0
|
|
app.is_dirty = true
|
|
}
|
|
} else {
|
|
if clicked(clay.ID(fmt.tprintf("btn_char_edit_%s", char.id))) {
|
|
app.editing_char_id = char.id
|
|
app.char_edit_text = char.description
|
|
app.selected_field = 8
|
|
}
|
|
}
|
|
|
|
if clicked(clay.ID(fmt.tprintf("btn_char_ref_%s", char.id))) {
|
|
push_status(&app.status_msg, &app.action_log, action_generate_character_reference(&app.controller, char.id))
|
|
}
|
|
if clicked(clay.ID(fmt.tprintf("btn_char_sheet_%s", char.id))) {
|
|
push_status(&app.status_msg, &app.action_log, action_generate_character_sheet(&app.controller, char.id))
|
|
}
|
|
if clicked(clay.ID("field_char_desc")) {
|
|
if is_editing {
|
|
app.selected_field = 8
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if screen == .Panels {
|
|
panel_count := count_script_panels(app.controller.state.script)
|
|
if panel_count > 0 {
|
|
idx := clamp_panel_cursor(panel_count, app.summary_opts.panel_cursor)
|
|
panel, _, ok := panel_by_flat_index(app.controller.state.script, idx)
|
|
if ok {
|
|
is_editing := app.editing_panel_id == panel.panel_id
|
|
|
|
if is_editing {
|
|
if clicked(clay.ID("btn_panel_save")) {
|
|
push_status(&app.status_msg, &app.action_log, action_update_panel_prompt(&app.controller, panel.panel_id, strings.clone(app.panel_edit_text)))
|
|
app.editing_panel_id = ""
|
|
app.selected_field = 0
|
|
app.is_dirty = true
|
|
}
|
|
} else {
|
|
if clicked(clay.ID("btn_panel_edit")) {
|
|
app.editing_panel_id = panel.panel_id
|
|
app.panel_edit_text = panel.description
|
|
app.selected_field = 9
|
|
}
|
|
}
|
|
|
|
if clicked(clay.ID("btn_panel_regenerate")) {
|
|
push_status(&app.status_msg, &app.action_log, action_regenerate_panel(&app.controller, panel.panel_id))
|
|
}
|
|
|
|
if clicked(clay.ID("btn_panel_draw")) {
|
|
if editor_open(app, panel.panel_id) {
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Editing panel %s", panel.panel_id))
|
|
} else {
|
|
push_status(&app.status_msg, &app.action_log, "Cannot open editor: image not available")
|
|
}
|
|
}
|
|
|
|
if clicked(clay.ID("field_panel_desc")) || clicked(clay.ID("field_panel_desc_missing")) || clicked(clay.ID("field_panel_desc_no_img")) {
|
|
if is_editing {
|
|
app.selected_field = 9
|
|
}
|
|
}
|
|
}
|
|
for i in 0..<panel_count {
|
|
if clicked(clay.ID(fmt.tprintf("panel_row_%d", i))) {
|
|
app.summary_opts.panel_cursor = i
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Viewing panel %d/%d", i+1, panel_count))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if screen == .Layout {
|
|
if clicked(clay.ID("btn_layout_regen")) {
|
|
push_status(&app.status_msg, &app.action_log, action_regenerate_page_layout(&app.controller, app.summary_opts.layout_page_cursor))
|
|
app.is_dirty = true
|
|
}
|
|
layout_count := len(app.controller.state.page_layouts)
|
|
for i in 0..<layout_count {
|
|
if clicked(clay.ID(fmt.tprintf("layout_row_%d", i))) {
|
|
app.summary_opts.layout_page_cursor = i
|
|
app.layout_selected_panel = -1
|
|
push_status(&app.status_msg, &app.action_log, fmt.tprintf("Viewing layout page %d/%d", i+1, layout_count))
|
|
}
|
|
}
|
|
if app.wireframe_cell_count > 0 && rl.IsMouseButtonPressed(.LEFT) {
|
|
mouse := rl.GetMousePosition()
|
|
for i in 0..<app.wireframe_cell_count {
|
|
r := app.wireframe_cell_rects[i]
|
|
if mouse.x >= r.x && mouse.x <= r.x + r.width && mouse.y >= r.y && mouse.y <= r.y + r.height {
|
|
if app.layout_selected_panel == i {
|
|
app.layout_selected_panel = -1
|
|
} else {
|
|
app.layout_selected_panel = i
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if screen == .Bubbles {
|
|
layout_count := len(app.controller.state.page_layouts)
|
|
if layout_count > 0 {
|
|
if clicked(clay.ID("btn_bubble_auto_place")) {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
if len(layout_val.panels) > 0 {
|
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
|
panel_id := layout_val.panels[panel_idx].panel_id
|
|
push_status(&app.status_msg, &app.action_log, action_auto_place_bubbles_for_panel(&app.controller, panel_id, layout_val))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
if clicked(clay.ID("btn_bubble_add")) {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
if len(layout_val.panels) > 0 {
|
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
|
panel_id := layout_val.panels[panel_idx].panel_id
|
|
push_status(&app.status_msg, &app.action_log, action_add_bubble(&app.controller, panel_id))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
if clicked(clay.ID("field_bubble_text")) {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
if len(layout_val.panels) > 0 {
|
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
|
panel_id := layout_val.panels[panel_idx].panel_id
|
|
bubble_count := count_bubbles_for_panel(app.controller.state.speech_bubbles, panel_id)
|
|
if bubble_count > 0 && app.summary_opts.bubble_edit_cursor < bubble_count {
|
|
bubbles := app.controller.state.speech_bubbles[panel_id]
|
|
app.bubble_edit_text = bubbles[app.summary_opts.bubble_edit_cursor].text
|
|
app.selected_field = 7
|
|
}
|
|
}
|
|
}
|
|
if clicked(clay.ID("btn_bubble_save_text")) {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
if len(layout_val.panels) > 0 && len(app.bubble_edit_text) > 0 {
|
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
|
panel_id := layout_val.panels[panel_idx].panel_id
|
|
if bubbles, ok := app.controller.state.speech_bubbles[panel_id]; ok {
|
|
if app.summary_opts.bubble_edit_cursor < len(bubbles) {
|
|
current_type := bubbles[app.summary_opts.bubble_edit_cursor].type
|
|
push_status(&app.status_msg, &app.action_log, action_update_bubble(&app.controller, panel_id, app.summary_opts.bubble_edit_cursor, current_type, app.bubble_edit_text))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
}
|
|
app.selected_field = 0
|
|
}
|
|
}
|
|
if layout_count > 0 {
|
|
page_idx := clamp_layout_cursor(layout_count, app.summary_opts.bubble_page_cursor)
|
|
layout_val := app.controller.state.page_layouts[page_idx]
|
|
if len(layout_val.panels) > 0 {
|
|
panel_idx := clamp_bubble_cursor(len(layout_val.panels), app.summary_opts.bubble_panel_cursor)
|
|
panel_id := layout_val.panels[panel_idx].panel_id
|
|
panel_panel_count := count_bubbles_for_panel(app.controller.state.speech_bubbles, panel_id)
|
|
for i in 0..<panel_panel_count {
|
|
if clicked(clay.ID(fmt.tprintf("bubble_row_%d", i))) {
|
|
app.summary_opts.bubble_edit_cursor = i
|
|
}
|
|
if clicked(clay.ID(fmt.tprintf("btn_bubble_delete_%d", i))) {
|
|
push_status(&app.status_msg, &app.action_log, action_delete_bubble(&app.controller, panel_id, i))
|
|
app.is_dirty = true
|
|
if app.summary_opts.bubble_edit_cursor > 0 { app.summary_opts.bubble_edit_cursor -= 1 }
|
|
}
|
|
}
|
|
bubble_count := count_bubbles_for_panel(app.controller.state.speech_bubbles, panel_id)
|
|
if bubble_count > 0 && app.summary_opts.bubble_edit_cursor < bubble_count {
|
|
types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect}
|
|
for t in types {
|
|
btn_id := fmt.tprintf("btn_btype_%d", int(t))
|
|
if clicked(clay.ID(btn_id)) {
|
|
push_status(&app.status_msg, &app.action_log, action_update_bubble(&app.controller, panel_id, app.summary_opts.bubble_edit_cursor, t, ""))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
|
|
b := app.controller.state.speech_bubbles[panel_id][app.summary_opts.bubble_edit_cursor]
|
|
if clicked(clay.ID("btn_bubble_left")) {
|
|
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x - 0.05, b.position.y))
|
|
app.is_dirty = true
|
|
}
|
|
if clicked(clay.ID("btn_bubble_right")) {
|
|
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x + 0.05, b.position.y))
|
|
app.is_dirty = true
|
|
}
|
|
if clicked(clay.ID("btn_bubble_up")) {
|
|
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x, b.position.y - 0.05))
|
|
app.is_dirty = true
|
|
}
|
|
if clicked(clay.ID("btn_bubble_down")) {
|
|
push_status(&app.status_msg, &app.action_log, action_reposition_bubble(&app.controller, b.id, b.position.x, b.position.y + 0.05))
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
process_clicks :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_export, proj_ok, export_ok, has_deepseek, has_fal_key: bool, pages_count: int, shift_down: bool, autosave_secs: int, compact_mode: bool, bp: shared.Breakpoint) {
|
|
handle_nav_clicks(app)
|
|
handle_field_clicks(app)
|
|
handle_format_clicks(app, has_deepseek)
|
|
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_detail_clicks(app)
|
|
|
|
// Overlay clicks
|
|
if app.show_confirm_overlay {
|
|
confirm_yes := rl.IsKeyPressed(.ENTER) || rl.IsKeyPressed(.Y) || clicked(clay.ID("confirm_yes"))
|
|
confirm_no := rl.IsKeyPressed(.ESCAPE) || rl.IsKeyPressed(.N) || clicked(clay.ID("confirm_no"))
|
|
if confirm_no {
|
|
app.show_confirm_overlay = false
|
|
app.pending_confirm = .None
|
|
push_status(&app.status_msg, &app.action_log, "Cancelled destructive action")
|
|
} else if confirm_yes {
|
|
app.show_confirm_overlay = false
|
|
msg := resolve_confirm_action_with_message(app.pending_confirm, &app.controller, &app.project_path, &app.export_path, app.export_format, &app.is_dirty, &app.last_autosave_at)
|
|
push_status(&app.status_msg, &app.action_log, msg)
|
|
app.pending_confirm = .None
|
|
}
|
|
}
|
|
if app.show_help_overlay {
|
|
if rl.IsKeyPressed(.ESCAPE) || rl.IsKeyPressed(.SLASH) {
|
|
app.show_help_overlay = false
|
|
push_status(&app.status_msg, &app.action_log, "Closed help overlay")
|
|
}
|
|
}
|
|
}
|
|
// ─── Help Overlay (Clay floating, animated alpha) ──────────────────
|
|
declare_help_overlay :: proc(bp: shared.Breakpoint, alpha: f32 = 1) {
|
|
if clay.UI(clay.ID("HelpBackdrop"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
|
backgroundColor = {0, 0, 0, f32(200 * alpha)},
|
|
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 5},
|
|
}) {}
|
|
|
|
card_w_pct: f32 = 0.7
|
|
card_h_pct: f32 = 0.8
|
|
if bp == .Compact { card_w_pct = 0.92; card_h_pct = 0.92 }
|
|
if clay.UI(clay.ID("HelpCard"))({
|
|
layout = {sizing = {width = clay.SizingPercent(card_w_pct), height = clay.SizingPercent(card_h_pct)}, layoutDirection = .TopToBottom, padding = {top = 24, right = 28, bottom = 24, left = 28}, childGap = CLAY_SPACE_12},
|
|
backgroundColor = CLAY_BG_CARD,
|
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XL),
|
|
border = {color = CLAY_BORDER_CARD, width = clay.BorderOutside(1)},
|
|
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .CenterCenter, parent = .CenterCenter}, zIndex = 10},
|
|
}) {
|
|
if clay.UI(clay.ID("HelpHeader"))({
|
|
layout = {layoutDirection = .LeftToRight, sizing = {width = clay.SizingGrow({}), height = clay.SizingFit({})}, childAlignment = {y = .Center}, childGap = 8},
|
|
}) {
|
|
clay_title_text("Keyboard Shortcuts", color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_XL)
|
|
clay.UI(clay.ID("HelpHeaderSpacer"))({layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}}})
|
|
clay_body_text("Esc or /", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
|
}
|
|
|
|
declare_divider("HelpDiv1")
|
|
|
|
clay_label_text("NAVIGATION")
|
|
clay_body_text("1..8 screens | TAB fields | click to focus | F11 pages | F12 project", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
clay_label_text("CORE ACTIONS")
|
|
clay_body_text("F5 script F6 panels F7 layout F8 export F9 next F10 auto-all", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
clay_body_text("Ctrl+S save | Ctrl+O open | Ctrl+N new | Ctrl+E export | Ctrl+H/J summary", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
clay_label_text("CLIPBOARD + LOGS")
|
|
clay_body_text("Ctrl+L clear log | Ctrl+V paste | Ctrl+0 reset helpers | Ctrl+Backspace clear", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
clay_label_text("PATHS")
|
|
clay_body_text("Ctrl+Shift+X copy export | P preset | D exp-from-project | G proj-from-export", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
clay_label_text("AUTOSAVE")
|
|
clay_body_text("Ctrl+Shift+A toggle | Ctrl+-/= adjust | Ctrl+7/8/9 set 15/30/60", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
clay_label_text("SAFETY")
|
|
clay_body_text("Dirty guard: Shift-click New/Open | Ctrl+Shift+N / Ctrl+Shift+O", color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
}
|
|
}
|
|
|
|
// ─── Confirm Overlay (Clay floating, animated alpha) ────────────────
|
|
declare_confirm_overlay :: proc(action: Pending_Confirm_Action, bp: shared.Breakpoint, alpha: f32 = 1) {
|
|
if clay.UI(clay.ID("ConfirmBackdrop"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}},
|
|
backgroundColor = {0, 0, 0, f32(200 * alpha)},
|
|
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 15},
|
|
}) {}
|
|
|
|
if clay.UI(clay.ID("ConfirmCard"))({
|
|
layout = {sizing = {width = clay.SizingPercent(0.6), height = clay.SizingFit({min = 160, max = 380})}, layoutDirection = .TopToBottom, padding = {top = 24, right = 28, bottom = 24, left = 28}, childGap = CLAY_SPACE_12},
|
|
backgroundColor = CLAY_BG_CARD,
|
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_XL),
|
|
border = {color = CLAY_ERROR_DIM, width = clay.BorderOutside(2)},
|
|
floating = {offset = {0, 0}, attachTo = .Root, attachment = {element = .CenterCenter, parent = .CenterCenter}, zIndex = 20},
|
|
}) {
|
|
if clay.UI(clay.ID("ConfirmAccentBar"))({
|
|
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(3)}},
|
|
backgroundColor = CLAY_ERROR,
|
|
cornerRadius = clay.CornerRadiusAll(2),
|
|
}) {}
|
|
|
|
clay_title_text("Confirm Action", color = CLAY_ERROR, size = CLAY_FONT_SIZE_XL)
|
|
action_label := "reset"
|
|
if action == .Open_Project { action_label = "open a different project" }
|
|
clay_body_text(fmt.tprintf("You have unsaved changes. Do you want to %s?", action_label), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
|
|
|
if clay.UI(clay.ID("ConfirmBtnRow"))({layout = clay_row_layout()}) {
|
|
declare_button_danger("confirm_yes", "Confirm")
|
|
declare_button_soft("confirm_no", "Cancel")
|
|
}
|
|
clay_body_text("Enter/Y confirm | Esc/N cancel", color = CLAY_TEXT_TERTIARY, size = CLAY_FONT_SIZE_XS)
|
|
}
|
|
}
|
|
|
|
// ─── Toast (Clay floating) ──────────────────────────────────────────
|
|
declare_toast :: proc(log: ^Action_Log) {
|
|
if log.count == 0 { return }
|
|
age := rl.GetTime() - log.last_push_at
|
|
if age > 2.8 { return }
|
|
|
|
idx := (log.count - 1) % len(log.entries)
|
|
if idx < 0 { idx += len(log.entries) }
|
|
msg := log.entries[idx]
|
|
bg := CLAY_ERROR
|
|
if is_warning_message(msg) { bg = CLAY_WARNING }
|
|
else if !is_error_message(msg) { bg = CLAY_SUCCESS }
|
|
|
|
if clay.UI(clay.ID("Toast"))({
|
|
layout = {sizing = {width = clay.SizingFit({min = 280, max = 0}), height = clay.SizingFixed(f32(CLAY_HEIGHT_SM))}, padding = {top = 4, right = 12, bottom = 4, left = 12}, childAlignment = {y = .Center}},
|
|
backgroundColor = bg,
|
|
cornerRadius = clay.CornerRadiusAll(CLAY_RADIUS_MD),
|
|
floating = {offset = {f32(228), 70}, attachTo = .Root, attachment = {element = .LeftTop, parent = .LeftTop}, zIndex = 30},
|
|
}) {
|
|
clay_body_text(msg, color = CLAY_TEXT_BRIGHT, size = CLAY_FONT_SIZE_XS)
|
|
}
|
|
}
|
|
|
|
// ─── Raygui Field Overlay ──────────────────────────────────────────
|
|
field_id_for_index :: proc(idx: int) -> string {
|
|
switch idx {
|
|
case 0: return "field_idea"
|
|
case 1: return "field_genre"
|
|
case 2: return "field_audience"
|
|
case 3: return "field_export"
|
|
case 4: return "field_pages"
|
|
case 5: return "field_project"
|
|
case 6: return "field_autosave"
|
|
case 7: return "field_bubble_text"
|
|
case 8: return "field_char_desc"
|
|
case 9: return "field_panel_prompt"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
field_ptr_for_index :: proc(app: ^GUI_App_State, idx: int) -> ^string {
|
|
switch idx {
|
|
case 0: return &app.controller.state.story_idea
|
|
case 1: return &app.controller.state.story_genre
|
|
case 2: return &app.controller.state.target_audience
|
|
case 3: return &app.export_path
|
|
case 4: return &app.local_script_pages
|
|
case 5: return &app.project_path
|
|
case 6: return &app.autosave_interval_text
|
|
case 7: return &app.bubble_edit_text
|
|
case 8: return &app.char_edit_text
|
|
case 9: return &app.panel_edit_text
|
|
}
|
|
return nil
|
|
}
|
|
|
|
render_raygui_fields :: proc(app: ^GUI_App_State) {
|
|
sf := app.selected_field
|
|
fid := field_id_for_index(sf)
|
|
if len(fid) == 0 { return }
|
|
|
|
eid := clay.GetElementId(clay.MakeString(fid))
|
|
data := clay.GetElementData(eid)
|
|
if !data.found { return }
|
|
|
|
bounds := rl.Rectangle {
|
|
x = f32(data.boundingBox.x),
|
|
y = f32(data.boundingBox.y),
|
|
width = f32(data.boundingBox.width),
|
|
height = f32(data.boundingBox.height),
|
|
}
|
|
|
|
fp := field_ptr_for_index(app, sf)
|
|
if fp == nil { return }
|
|
|
|
// Copy field string → buffer for GuiTextBox
|
|
src := fp^
|
|
if len(src) >= FIELD_BUF_SIZE { src = src[:FIELD_BUF_SIZE-1] }
|
|
for i in 0 ..< len(src) {
|
|
app.field_buf[i] = src[i]
|
|
}
|
|
app.field_buf[len(src)] = 0
|
|
|
|
// Call raygui GuiTextBox — handles rendering + input, modifies buffer in-place
|
|
rl.GuiTextBox(bounds, cstring(&app.field_buf[0]), c.int(FIELD_BUF_SIZE), true)
|
|
|
|
// Sync buffer → field string (pick up GuiTextBox edits)
|
|
new_len := 0
|
|
for app.field_buf[new_len] != 0 && new_len < FIELD_BUF_SIZE-1 {
|
|
new_len += 1
|
|
}
|
|
if new_len != len(src) {
|
|
fp^ = string(app.field_buf[:new_len])
|
|
app.is_dirty = true
|
|
}
|
|
}
|
|
|
|
// ─── Animation Updates ─────────────────────────────────────────────
|
|
lerp_f32 :: proc(a, b, t: f32) -> f32 {
|
|
return a + (b - a) * t
|
|
}
|
|
|
|
smooth_towards :: proc(current, target, dt, duration: f32) -> f32 {
|
|
if abs(current - target) < 0.5 { return target }
|
|
k := 1 - math.pow_f32(0.01, dt / duration)
|
|
return lerp_f32(current, target, k)
|
|
}
|
|
|
|
update_sidebar_anim :: proc(app: ^GUI_App_State, bp: shared.Breakpoint, dt: f32) {
|
|
target_w := f32(sidebar_width(bp, app.sidebar_collapsed, 0))
|
|
app.sidebar_anim = smooth_towards(app.sidebar_anim, target_w, dt, ANIM_DURATION_SIDEBAR)
|
|
}
|
|
|
|
update_overlay_anim :: proc(app: ^GUI_App_State, dt: f32) {
|
|
target_alpha: f32 = 0
|
|
if app.show_help_overlay || app.show_confirm_overlay {
|
|
target_alpha = 1
|
|
}
|
|
app.overlay_alpha = smooth_towards(app.overlay_alpha, target_alpha, dt, ANIM_DURATION_OVERLAY)
|
|
}
|
|
|
|
update_slide_anim :: proc(app: ^GUI_App_State, dt: f32) {
|
|
current := app.controller.active_screen
|
|
if current != app.prev_screen && app.slide_progress >= 1 {
|
|
app.slide_progress = 0
|
|
}
|
|
if app.slide_progress < 1 {
|
|
app.slide_progress += dt / ANIM_DURATION_SLIDE
|
|
if app.slide_progress > 1 {
|
|
app.slide_progress = 1
|
|
app.slide_offset = 0
|
|
app.prev_screen = current
|
|
} else {
|
|
// Ease-out cubic: offset goes from 60 → 0
|
|
t := app.slide_progress
|
|
eased := 1 - (1 - t) * (1 - t) * (1 - t)
|
|
app.slide_offset = 60 * (1 - eased)
|
|
}
|
|
}
|
|
}
|
|
|