check point
This commit is contained in:
1 parent
ae1cae967b
commit
1e85df5193
72 files changed
+11452
No files matched your search
@@ -0,0 +1,429 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
import "../adapters"
|
||||
import "../core"
|
||||
import "../shared"
|
||||
import "../ui"
|
||||
|
||||
action_generate_local_script :: proc(controller: ^ui.App_Controller, pages: int) -> string {
|
||||
story := controller.state.story_idea
|
||||
if len(story) == 0 {
|
||||
story = "A local GUI adventure"
|
||||
}
|
||||
script := build_local_script(story, pages)
|
||||
core.dispose_script(&controller.state.script)
|
||||
controller.state.script = script
|
||||
controller.state.characters = controller.state.script.characters
|
||||
controller.active_screen = .Script
|
||||
controller.state.workflow.current_step = .Script_Review
|
||||
return "Generated local script"
|
||||
}
|
||||
|
||||
action_generate_deepseek_script :: proc(controller: ^ui.App_Controller, pages: int) -> string {
|
||||
cfg := shared.load_config()
|
||||
if len(cfg.deepseek_api_key) == 0 {
|
||||
return "DeepSeek key missing (set DEEPSEEK_API_KEY)"
|
||||
}
|
||||
opts := adapters.Generate_Script_Options{
|
||||
story_idea = controller.state.story_idea,
|
||||
genre = controller.state.story_genre,
|
||||
art_style = controller.state.art_style,
|
||||
num_pages = pages,
|
||||
audience = controller.state.target_audience,
|
||||
}
|
||||
script, gerr := adapters.generate_comic_script_stub(cfg, opts)
|
||||
if !shared.is_ok(gerr) {
|
||||
return fmt.aprintf("DeepSeek script failed: %s", gerr.message)
|
||||
}
|
||||
core.dispose_script(&controller.state.script)
|
||||
controller.state.script = script
|
||||
controller.state.characters = controller.state.script.characters
|
||||
controller.active_screen = .Script
|
||||
controller.state.workflow.current_step = .Script_Review
|
||||
return "Generated DeepSeek script"
|
||||
}
|
||||
|
||||
action_generate_local_panels :: proc(controller: ^ui.App_Controller) -> string {
|
||||
panels := collect_script_panels(controller.state.script)
|
||||
defer delete(panels)
|
||||
if len(panels) == 0 {
|
||||
return "No script panels available"
|
||||
}
|
||||
images, ierr := build_local_panel_images(panels)
|
||||
if !shared.is_ok(ierr) {
|
||||
return ierr.message
|
||||
}
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = images
|
||||
controller.active_screen = .Panels
|
||||
return "Generated local panels"
|
||||
}
|
||||
|
||||
action_regenerate_panel :: proc(controller: ^ui.App_Controller, panel_id: string) -> string {
|
||||
panels := collect_script_panels(controller.state.script)
|
||||
defer delete(panels)
|
||||
|
||||
target_panel: core.Panel
|
||||
found := false
|
||||
for p in panels {
|
||||
if p.panel_id == panel_id {
|
||||
target_panel = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "Panel not found in script"
|
||||
}
|
||||
|
||||
single := make([]core.Panel, 1)
|
||||
single[0] = target_panel
|
||||
defer delete(single)
|
||||
|
||||
images, ierr := build_local_panel_images(single)
|
||||
if !shared.is_ok(ierr) {
|
||||
if controller.state.panel_errors == nil {
|
||||
controller.state.panel_errors = make(map[string]string)
|
||||
}
|
||||
controller.state.panel_errors[panel_id] = strings.clone(ierr.message)
|
||||
return "Panel generation failed"
|
||||
}
|
||||
|
||||
if img, has := images[panel_id]; has {
|
||||
if old, exists := controller.state.panel_images[panel_id]; exists {
|
||||
delete(old.url)
|
||||
delete(old.prompt)
|
||||
}
|
||||
if controller.state.panel_images == nil {
|
||||
controller.state.panel_images = make(map[string]core.Panel_Image)
|
||||
}
|
||||
controller.state.panel_images[panel_id] = img
|
||||
|
||||
if err_msg, err_exists := controller.state.panel_errors[panel_id]; err_exists {
|
||||
delete(err_msg)
|
||||
delete_key(&controller.state.panel_errors, panel_id)
|
||||
}
|
||||
}
|
||||
delete(images) // free the map shell returned by build_local_panel_images
|
||||
|
||||
return "Regenerated panel"
|
||||
}
|
||||
|
||||
action_layout_auto :: proc(controller: ^ui.App_Controller) -> string {
|
||||
panels := collect_script_panels(controller.state.script)
|
||||
defer delete(panels)
|
||||
if len(panels) == 0 {
|
||||
return "No script panels for layout"
|
||||
}
|
||||
core.dispose_page_layouts(&controller.state.page_layouts)
|
||||
controller.state.page_layouts = core.auto_layout_pages(panels, controller.state.page_size, controller.state.story_genre, "")
|
||||
controller.active_screen = .Layout
|
||||
controller.state.workflow.current_step = .Layout
|
||||
return "Auto layout generated"
|
||||
}
|
||||
|
||||
export_format_name :: proc(f: core.Export_Format) -> string {
|
||||
switch f {
|
||||
case .PDF: return "PDF"
|
||||
case .PNG: return "PNG"
|
||||
case .CBZ: return "CBZ"
|
||||
}
|
||||
return "PDF"
|
||||
}
|
||||
|
||||
parse_pages_or_default :: proc(s: string, def: int) -> int {
|
||||
v, ok := strconv.parse_int(strings.trim_space(s))
|
||||
if !ok || v <= 0 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
parse_autosave_interval :: proc(s: string, def: int) -> int {
|
||||
v, ok := strconv.parse_int(strings.trim_space(s))
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
if v < 5 {
|
||||
return 5
|
||||
}
|
||||
if v > 300 {
|
||||
return 300
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
set_autosave_interval_text :: proc(dst: ^string, seconds: int) -> string {
|
||||
v := seconds
|
||||
if v < 5 {
|
||||
v = 5
|
||||
}
|
||||
if v > 300 {
|
||||
v = 300
|
||||
}
|
||||
dst^ = fmt.aprintf("%d", v)
|
||||
return fmt.aprintf("Autosave interval: %ds", v)
|
||||
}
|
||||
|
||||
yn :: proc(v: bool) -> string {
|
||||
if v {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
|
||||
toggle_summary_show :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string {
|
||||
#partial switch active_screen {
|
||||
case .Script:
|
||||
opts.script_show_all = !opts.script_show_all
|
||||
return fmt.aprintf("Script summary show-all: %s", yn(opts.script_show_all))
|
||||
case .Layout:
|
||||
opts.layout_show_all = !opts.layout_show_all
|
||||
return fmt.aprintf("Layout summary show-all: %s", yn(opts.layout_show_all))
|
||||
}
|
||||
return "Summary show toggle unavailable on this screen"
|
||||
}
|
||||
|
||||
toggle_summary_sort :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string {
|
||||
#partial switch active_screen {
|
||||
case .Script:
|
||||
opts.script_desc = !opts.script_desc
|
||||
sort_name := "asc"
|
||||
if opts.script_desc {
|
||||
sort_name = "desc"
|
||||
}
|
||||
return fmt.aprintf("Script summary sort: %s", sort_name)
|
||||
case .Layout:
|
||||
opts.layout_desc = !opts.layout_desc
|
||||
sort_name := "asc"
|
||||
if opts.layout_desc {
|
||||
sort_name = "desc"
|
||||
}
|
||||
return fmt.aprintf("Layout summary sort: %s", sort_name)
|
||||
}
|
||||
return "Summary sort toggle unavailable on this screen"
|
||||
}
|
||||
|
||||
toggle_summary_show_if_supported :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string {
|
||||
if active_screen == .Script || active_screen == .Layout {
|
||||
return toggle_summary_show(active_screen, opts)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
toggle_summary_sort_if_supported :: proc(active_screen: ui.App_Screen, opts: ^Summary_View_Options) -> string {
|
||||
if active_screen == .Script || active_screen == .Layout {
|
||||
return toggle_summary_sort(active_screen, opts)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
reset_project_session :: proc(controller: ^ui.App_Controller, is_dirty: ^bool, last_autosave_at: ^f64, touch_time: bool) -> string {
|
||||
core.dispose_state(&controller.state)
|
||||
controller.state = core.new_initial_state()
|
||||
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
||||
is_dirty^ = false
|
||||
if touch_time {
|
||||
last_autosave_at^ = rl.GetTime()
|
||||
}
|
||||
return "Reset project"
|
||||
}
|
||||
|
||||
open_project_session :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, is_dirty: ^bool, last_autosave_at: ^f64) -> string {
|
||||
normalize_project_path_field(project_path)
|
||||
loaded, lerr := adapters.load_project(project_path^)
|
||||
if !shared.is_ok(lerr) {
|
||||
return lerr.message
|
||||
}
|
||||
core.dispose_state(&controller.state)
|
||||
controller.state = loaded
|
||||
controller.active_screen = ui.screen_from_workflow(controller.state.workflow.current_step)
|
||||
sync_export_path_to_project_dir(project_path^, export_path, export_format)
|
||||
is_dirty^ = false
|
||||
last_autosave_at^ = rl.GetTime()
|
||||
return fmt.aprintf("Opened project: %s (export -> %s)", project_path^, export_path^)
|
||||
}
|
||||
|
||||
resolve_confirm_action_with_message :: proc(action: Pending_Confirm_Action, controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, is_dirty: ^bool, last_autosave_at: ^f64) -> string {
|
||||
switch action {
|
||||
case .Reset_Project:
|
||||
return reset_project_session(controller, is_dirty, last_autosave_at, true)
|
||||
case .Open_Project:
|
||||
return open_project_session(controller, project_path, export_path, export_format, is_dirty, last_autosave_at)
|
||||
case .None:
|
||||
return "No pending destructive action"
|
||||
}
|
||||
return "No pending destructive action"
|
||||
}
|
||||
|
||||
save_project_session_with_message :: proc(project_path: ^string, state: core.Comic_State, is_dirty: ^bool, last_autosave_at, last_save_at: ^f64, success_prefix: string) -> string {
|
||||
normalize_project_path_field(project_path)
|
||||
err := adapters.save_project(project_path^, state)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
is_dirty^ = false
|
||||
last_autosave_at^ = rl.GetTime()
|
||||
last_save_at^ = last_autosave_at^
|
||||
return fmt.aprintf("%s: %s", success_prefix, project_path^)
|
||||
}
|
||||
|
||||
action_export :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format) -> string {
|
||||
opts := adapters.Export_Options{format = export_format, page_size = controller.state.page_size, dpi = 300, quality = 90}
|
||||
err := adapters.export_comic_stub(export_path, controller.state.page_layouts, controller.state.panel_images, opts)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
controller.active_screen = .Export
|
||||
controller.state.workflow.current_step = .Complete
|
||||
controller.state.export_format = export_format
|
||||
return fmt.aprintf("Exported %s", export_format_name(export_format))
|
||||
}
|
||||
|
||||
gui_next_hint_with_source :: proc(controller: ui.App_Controller, use_deepseek_script: bool) -> string {
|
||||
if len(controller.state.script.pages) == 0 {
|
||||
if use_deepseek_script {
|
||||
return "generate script"
|
||||
}
|
||||
return "generate script local"
|
||||
}
|
||||
if len(controller.state.panel_images) == 0 {
|
||||
return "generate panels local"
|
||||
}
|
||||
if len(controller.state.page_layouts) == 0 {
|
||||
return "layout auto"
|
||||
}
|
||||
return "export pdf"
|
||||
}
|
||||
|
||||
gui_next_hint :: proc(controller: ui.App_Controller) -> string {
|
||||
return gui_next_hint_with_source(controller, false)
|
||||
}
|
||||
|
||||
action_run_next :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int, use_deepseek_script: bool) -> string {
|
||||
hint := gui_next_hint_with_source(controller^, use_deepseek_script)
|
||||
switch hint {
|
||||
case "generate script":
|
||||
return action_generate_deepseek_script(controller, script_pages)
|
||||
case "generate script local":
|
||||
return action_generate_local_script(controller, script_pages)
|
||||
case "generate panels local":
|
||||
return action_generate_local_panels(controller)
|
||||
case "layout auto":
|
||||
return action_layout_auto(controller)
|
||||
case "export pdf":
|
||||
return action_export(controller, export_path, export_format)
|
||||
}
|
||||
return "No next action"
|
||||
}
|
||||
|
||||
action_run_auto_all_local :: proc(controller: ^ui.App_Controller, export_path: string, export_format: core.Export_Format, script_pages: int, use_deepseek_script: bool) -> string {
|
||||
for _ in 0..<4 {
|
||||
msg := action_run_next(controller, export_path, export_format, script_pages, use_deepseek_script)
|
||||
if controller.active_screen == .Export {
|
||||
return fmt.aprintf("Auto-all complete: %s", msg)
|
||||
}
|
||||
}
|
||||
return "Auto-all could not complete"
|
||||
}
|
||||
|
||||
run_script_action :: proc(controller: ^ui.App_Controller, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool) -> string {
|
||||
is_dirty^ = true
|
||||
if use_deepseek_script {
|
||||
return action_generate_deepseek_script(controller, pages_count)
|
||||
}
|
||||
return action_generate_local_script(controller, pages_count)
|
||||
}
|
||||
|
||||
run_panels_action :: proc(controller: ^ui.App_Controller, can_generate_panels: bool, is_dirty: ^bool) -> string {
|
||||
if !can_generate_panels {
|
||||
return "Generate script before panels"
|
||||
}
|
||||
is_dirty^ = true
|
||||
return action_generate_local_panels(controller)
|
||||
}
|
||||
|
||||
run_layout_action :: proc(controller: ^ui.App_Controller, can_layout: bool, is_dirty: ^bool) -> string {
|
||||
if !can_layout {
|
||||
return "Generate panels before layout"
|
||||
}
|
||||
is_dirty^ = true
|
||||
return action_layout_auto(controller)
|
||||
}
|
||||
|
||||
run_export_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, can_export: bool, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
||||
normalize_export_path_field(export_path, export_format)
|
||||
if !can_export {
|
||||
return "Export blocked: generate panels + layout first"
|
||||
}
|
||||
msg := action_export(controller, export_path^, export_format)
|
||||
is_dirty^ = true
|
||||
if strings.has_prefix(msg, "Exported ") {
|
||||
last_export_at^ = rl.GetTime()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
run_next_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
||||
normalize_export_path_field(export_path, export_format)
|
||||
msg := action_run_next(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
||||
is_dirty^ = true
|
||||
if controller.active_screen == .Export {
|
||||
last_export_at^ = rl.GetTime()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
run_auto_all_action :: proc(controller: ^ui.App_Controller, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at: ^f64) -> string {
|
||||
normalize_export_path_field(export_path, export_format)
|
||||
msg := action_run_auto_all_local(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
||||
is_dirty^ = true
|
||||
if controller.active_screen == .Export {
|
||||
last_export_at^ = rl.GetTime()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
run_auto_all_save_action :: proc(controller: ^ui.App_Controller, project_path, export_path: ^string, export_format: core.Export_Format, pages_count: int, use_deepseek_script: bool, is_dirty: ^bool, last_export_at, last_autosave_at, last_save_at: ^f64) -> string {
|
||||
normalize_export_path_field(export_path, export_format)
|
||||
msg := action_run_auto_all_local(controller, export_path^, export_format, pages_count, use_deepseek_script)
|
||||
if controller.active_screen == .Export {
|
||||
last_export_at^ = rl.GetTime()
|
||||
return save_project_session_with_message(project_path, controller.state, is_dirty, last_autosave_at, last_save_at, "Auto-all + saved")
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
set_export_format_with_message :: proc(export_format: ^core.Export_Format, export_path: ^string, next: core.Export_Format, is_dirty: ^bool) -> string {
|
||||
export_format^ = next
|
||||
export_path^ = export_path_for_format(export_path^, export_format^)
|
||||
is_dirty^ = true
|
||||
return fmt.aprintf("Export format: %s (%s)", export_format_name(export_format^), export_path^)
|
||||
}
|
||||
|
||||
autosave_tick_with_message :: proc(project_path: ^string, state: core.Comic_State, autosave_enabled: bool, is_dirty: ^bool, last_autosave_at: ^f64, last_save_at: ^f64, autosave_interval_s: f64) -> string {
|
||||
if !autosave_enabled || !is_dirty^ {
|
||||
return ""
|
||||
}
|
||||
now := rl.GetTime()
|
||||
if now-last_autosave_at^ < autosave_interval_s {
|
||||
return ""
|
||||
}
|
||||
normalize_project_path_field(project_path)
|
||||
err := adapters.save_project(project_path^, state)
|
||||
last_autosave_at^ = now
|
||||
if shared.is_ok(err) {
|
||||
is_dirty^ = false
|
||||
last_save_at^ = now
|
||||
return fmt.aprintf("Autosaved: %s", project_path^)
|
||||
}
|
||||
return fmt.aprintf("Autosave failed: %s", err.message)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
button_clicked :: proc(rec: rl.Rectangle) -> bool {
|
||||
if !rl.IsMouseButtonPressed(.LEFT) {
|
||||
return false
|
||||
}
|
||||
return rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
}
|
||||
|
||||
draw_button :: proc(rec: rl.Rectangle, label: string) {
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := BTN_BG
|
||||
border := BTN_BORDER
|
||||
text := BTN_TEXT
|
||||
if hover {
|
||||
bg = BTN_BG_HOVER
|
||||
border = BTN_BORDER_HOVER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
|
||||
}
|
||||
|
||||
draw_button_primary :: proc(rec: rl.Rectangle, label: string) {
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := ACCENT
|
||||
border := ACCENT_MUTED
|
||||
if hover {
|
||||
bg = ACCENT_HOVER
|
||||
border = ACCENT
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, TEXT_BRIGHT)
|
||||
}
|
||||
|
||||
draw_button_danger :: proc(rec: rl.Rectangle, label: string) {
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := DANGER_BG
|
||||
border := DANGER_BORDER
|
||||
if hover {
|
||||
bg = DANGER_BG_HOVER
|
||||
border = DANGER_BORDER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, TEXT_BRIGHT)
|
||||
}
|
||||
|
||||
draw_button_warning :: proc(rec: rl.Rectangle, label: string) {
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := WARN_BTN_BG
|
||||
border := WARN_BTN_BORDER
|
||||
text := WARN_BTN_TEXT
|
||||
if hover {
|
||||
bg = WARN_BTN_BG_HOVER
|
||||
border = WARN_BTN_BORDER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
|
||||
}
|
||||
|
||||
draw_button_soft_accent :: proc(rec: rl.Rectangle, label: string) {
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := BTN_SOFT_BG
|
||||
border := BTN_SOFT_BORDER
|
||||
text := BTN_SOFT_TEXT
|
||||
if hover {
|
||||
bg = BTN_SOFT_BG_HOVER
|
||||
border = BTN_SOFT_BORDER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, border)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, text)
|
||||
}
|
||||
|
||||
draw_button_state :: proc(rec: rl.Rectangle, label: string, enabled: bool) {
|
||||
if !enabled {
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BUTTON, 8, BTN_DISABLED_BG)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BUTTON, 8, 1.2, BTN_DISABLED_BORDER)
|
||||
draw_text_fitted(label, i32(rec.x)+10, i32(rec.y)+8, 18, int(rec.width)-20, 8, BTN_DISABLED_TEXT)
|
||||
return
|
||||
}
|
||||
draw_button(rec, label)
|
||||
}
|
||||
|
||||
draw_small_button_state :: proc(rec: rl.Rectangle, label: string, enabled: bool) {
|
||||
if !enabled {
|
||||
rl.DrawRectangleRounded(rec, 0.20, 6, BTN_DISABLED_BG)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 6, 1.0, BTN_DISABLED_BORDER)
|
||||
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-12, 7, BTN_DISABLED_TEXT)
|
||||
return
|
||||
}
|
||||
hover := rl.CheckCollisionPointRec(rl.GetMousePosition(), rec)
|
||||
bg := SBTN_BG
|
||||
border := SBTN_BORDER
|
||||
if hover {
|
||||
bg = SBTN_BG_HOVER
|
||||
border = SBTN_BORDER_HOVER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, 0.20, 6, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 6, 1.0, border)
|
||||
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-12, 7, SBTN_TEXT)
|
||||
}
|
||||
|
||||
draw_small_button :: proc(rec: rl.Rectangle, label: string) {
|
||||
draw_small_button_state(rec, label, true)
|
||||
}
|
||||
|
||||
button_readiness_hint :: proc(mouse: rl.Vector2, panels_btn, layout_btn, export_btn: rl.Rectangle, can_generate_panels, can_layout, can_export: bool) -> string {
|
||||
if rl.CheckCollisionPointRec(mouse, panels_btn) && !can_generate_panels {
|
||||
return "Panels requires a generated script"
|
||||
}
|
||||
if rl.CheckCollisionPointRec(mouse, layout_btn) && !can_layout {
|
||||
return "Layout requires generated panels"
|
||||
}
|
||||
if rl.CheckCollisionPointRec(mouse, export_btn) && !can_export {
|
||||
return "Export requires panels + layout"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
draw_button_recommended :: proc(rec: rl.Rectangle, label: string) {
|
||||
halo := rl.Rectangle{x = rec.x-2, y = rec.y-2, width = rec.width+4, height = rec.height+4}
|
||||
rl.DrawRectangleRounded(halo, RADIUS_BUTTON, 8, RECOMMEND_HALO_FILL)
|
||||
rl.DrawRectangleRoundedLinesEx(halo, RADIUS_BUTTON, 8, 1.4, RECOMMEND_HALO_BORDER)
|
||||
draw_button(rec, label)
|
||||
}
|
||||
|
||||
draw_nav_item :: proc(rec: rl.Rectangle, label: string, active: bool) {
|
||||
bg := NAV_BG
|
||||
border := NAV_BORDER
|
||||
text := NAV_TEXT
|
||||
if active {
|
||||
bg = NAV_ACTIVE_BG
|
||||
border = NAV_ACTIVE_BG
|
||||
text = NAV_ACTIVE_TEXT
|
||||
} else if rl.CheckCollisionPointRec(rl.GetMousePosition(), rec) {
|
||||
bg = NAV_BG_HOVER
|
||||
border = NAV_BORDER_HOVER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_NAV, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_NAV, 8, 1.0, border)
|
||||
if active {
|
||||
rl.DrawRectangleRounded(rl.Rectangle{x = rec.x+2, y = rec.y+4, width = 4, height = rec.height-8}, 0.5, 8, NAV_ACTIVE_BAR)
|
||||
}
|
||||
label_x := i32(rec.x) + 8
|
||||
label_w := int(rec.width) - 16
|
||||
if active {
|
||||
label_x = i32(rec.x) + 14
|
||||
label_w = int(rec.width) - 22
|
||||
}
|
||||
draw_text_fitted(label, label_x, i32(rec.y)+6, 18, label_w, 8, text)
|
||||
}
|
||||
|
||||
draw_input_field :: proc(rec: rl.Rectangle, value: string, selected: bool) {
|
||||
bg := INPUT_BG
|
||||
border := INPUT_BORDER
|
||||
if selected {
|
||||
halo := rl.Rectangle{x = rec.x - 2, y = rec.y - 2, width = rec.width + 4, height = rec.height + 4}
|
||||
rl.DrawRectangleRounded(halo, RADIUS_INPUT, 8, INPUT_FOCUS_RING)
|
||||
rl.DrawRectangleRoundedLinesEx(halo, RADIUS_INPUT, 8, 1.0, INPUT_FOCUS_BORDER)
|
||||
bg = INPUT_FOCUS_BG
|
||||
border = INPUT_FOCUS_BORDER
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_INPUT, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_INPUT, 8, 1.2, border)
|
||||
if !selected {
|
||||
draw_text_fitted(value, i32(rec.x)+8, i32(rec.y)+6, 18, int(rec.width)-16, 8, INPUT_TEXT)
|
||||
return
|
||||
}
|
||||
rl.DrawText(fmt.ctprintf("%s", value), i32(rec.x)+8, i32(rec.y)+6, 18, INPUT_TEXT_FOCUS)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import rl "vendor:raylib"
|
||||
import "../ui"
|
||||
|
||||
build_diagnostics_snapshot :: proc(controller: ui.App_Controller, is_dirty, autosave_enabled, project_ok, export_ok: bool, autosave_secs: int, project_path, export_path: string, log_show_lines: i32, log_oldest_first: bool) -> string {
|
||||
log_order := "newest"
|
||||
if log_oldest_first {
|
||||
log_order = "oldest"
|
||||
}
|
||||
return fmt.aprintf("screen=%s workflow=%v next=%s dirty=%s autosave=%s(%ds) content=pages:%d,panels:%d,layouts:%d,chars:%d paths=P:%s,E:%s project=%s export=%s log=%d,%s uptime=%.1fs", ui.screen_name(controller.active_screen), controller.state.workflow.current_step, gui_next_hint(controller), yn(is_dirty), yn(autosave_enabled), autosave_secs, len(controller.state.script.pages), len(controller.state.panel_images), len(controller.state.page_layouts), len(controller.state.characters), yn(project_ok), yn(export_ok), project_path, export_path, log_show_lines, log_order, rl.GetTime())
|
||||
}
|
||||
|
||||
build_action_log_snapshot :: proc(log: Action_Log) -> string {
|
||||
if log.count == 0 {
|
||||
return fmt.aprintf("(action log empty)")
|
||||
}
|
||||
now := rl.GetTime()
|
||||
max_lines := len(log.entries)
|
||||
if log.count < max_lines {
|
||||
max_lines = log.count
|
||||
}
|
||||
out := ""
|
||||
for line in 0..<max_lines {
|
||||
idx := (log.count - 1 - line) % len(log.entries)
|
||||
if idx < 0 {
|
||||
idx += len(log.entries)
|
||||
}
|
||||
entry := fmt.aprintf("[%2.0fs] %s", now-log.entry_times[idx], log.entries[idx])
|
||||
if line == 0 {
|
||||
out = entry
|
||||
} else {
|
||||
prev := out
|
||||
out = fmt.aprintf("%s\n%s", prev, entry)
|
||||
delete(prev)
|
||||
delete(entry)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
build_session_report :: proc(controller: ui.App_Controller, log: Action_Log, is_dirty, autosave_enabled, project_ok, export_ok: bool, autosave_secs: int, project_path, export_path: string, log_show_lines: i32, log_oldest_first: bool) -> string {
|
||||
diag := build_diagnostics_snapshot(controller, is_dirty, autosave_enabled, project_ok, export_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
log_text := build_action_log_snapshot(log)
|
||||
report := fmt.aprintf("# comic-odin gui session report\n\n[meta]\ngenerated_uptime=%.1fs\n\n[diagnostics]\n%s\n\n[action_log]\n%s\n", rl.GetTime(), diag, log_text)
|
||||
delete(diag)
|
||||
delete(log_text)
|
||||
return report
|
||||
}
|
||||
|
||||
Diagnostics_Action_Context :: struct {
|
||||
controller: ^ui.App_Controller,
|
||||
action_log: ^Action_Log,
|
||||
is_dirty: bool,
|
||||
autosave_enabled: bool,
|
||||
project_ok: bool,
|
||||
export_ok: bool,
|
||||
autosave_secs: int,
|
||||
project_path: string,
|
||||
export_path: string,
|
||||
log_show_lines: i32,
|
||||
log_oldest_first: bool,
|
||||
}
|
||||
|
||||
make_diagnostics_action_context :: proc(controller: ^ui.App_Controller, action_log: ^Action_Log, is_dirty, autosave_enabled, project_ok, export_ok: bool, autosave_secs: int, project_path, export_path: string, log_show_lines: i32, log_oldest_first: bool) -> Diagnostics_Action_Context {
|
||||
return Diagnostics_Action_Context{
|
||||
controller = controller,
|
||||
action_log = action_log,
|
||||
is_dirty = is_dirty,
|
||||
autosave_enabled = autosave_enabled,
|
||||
project_ok = project_ok,
|
||||
export_ok = export_ok,
|
||||
autosave_secs = autosave_secs,
|
||||
project_path = project_path,
|
||||
export_path = export_path,
|
||||
log_show_lines = log_show_lines,
|
||||
log_oldest_first = log_oldest_first,
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics_path_for_project :: proc(project_path: string) -> string {
|
||||
dir, _ := filepath.split(project_path)
|
||||
if len(dir) == 0 {
|
||||
dir = "./"
|
||||
}
|
||||
parts := []string{dir, "gui_diagnostics.txt"}
|
||||
joined, err := filepath.join(parts)
|
||||
if err != nil {
|
||||
return "./gui_diagnostics.txt"
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
session_report_path_for_project :: proc(project_path: string) -> string {
|
||||
dir, _ := filepath.split(project_path)
|
||||
if len(dir) == 0 {
|
||||
dir = "./"
|
||||
}
|
||||
parts := []string{dir, "gui_session_report.txt"}
|
||||
joined, err := filepath.join(parts)
|
||||
if err != nil {
|
||||
return "./gui_session_report.txt"
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
write_diagnostics_file :: proc(project_path, diag: string) -> string {
|
||||
diag_path := diagnostics_path_for_project(project_path)
|
||||
defer delete(diag_path)
|
||||
if err := os.write_entire_file(diag_path, diag); err == nil {
|
||||
return fmt.aprintf("Wrote diagnostics file: %s", diag_path)
|
||||
}
|
||||
return fmt.aprintf("Failed writing diagnostics file")
|
||||
}
|
||||
|
||||
write_session_report_file :: proc(project_path, report: string) -> string {
|
||||
report_path := session_report_path_for_project(project_path)
|
||||
defer delete(report_path)
|
||||
if err := os.write_entire_file(report_path, report); err == nil {
|
||||
return fmt.aprintf("Wrote session report: %s", report_path)
|
||||
}
|
||||
return fmt.aprintf("Failed writing session report")
|
||||
}
|
||||
|
||||
write_session_report_with_message :: proc(ctx: Diagnostics_Action_Context) -> string {
|
||||
report := build_session_report(ctx.controller^, ctx.action_log^, ctx.is_dirty, ctx.autosave_enabled, ctx.project_ok, ctx.export_ok, ctx.autosave_secs, ctx.project_path, ctx.export_path, ctx.log_show_lines, ctx.log_oldest_first)
|
||||
msg := write_session_report_file(ctx.project_path, report)
|
||||
delete(report)
|
||||
return msg
|
||||
}
|
||||
|
||||
write_diagnostics_with_message :: proc(ctx: Diagnostics_Action_Context) -> string {
|
||||
diag := build_diagnostics_snapshot(ctx.controller^, ctx.is_dirty, ctx.autosave_enabled, ctx.project_ok, ctx.export_ok, ctx.autosave_secs, ctx.project_path, ctx.export_path, ctx.log_show_lines, ctx.log_oldest_first)
|
||||
msg := write_diagnostics_file(ctx.project_path, diag)
|
||||
delete(diag)
|
||||
return msg
|
||||
}
|
||||
|
||||
copy_diagnostics_with_message :: proc(ctx: Diagnostics_Action_Context) -> string {
|
||||
diag := build_diagnostics_snapshot(ctx.controller^, ctx.is_dirty, ctx.autosave_enabled, ctx.project_ok, ctx.export_ok, ctx.autosave_secs, ctx.project_path, ctx.export_path, ctx.log_show_lines, ctx.log_oldest_first)
|
||||
msg := copy_text_with_status(diag, "Copied diagnostics snapshot")
|
||||
delete(diag)
|
||||
return msg
|
||||
}
|
||||
|
||||
copy_action_log_snapshot_with_message :: proc(ctx: Diagnostics_Action_Context) -> string {
|
||||
log_text := build_action_log_snapshot(ctx.action_log^)
|
||||
msg := copy_text_with_status(log_text, "Copied action log snapshot")
|
||||
delete(log_text)
|
||||
return msg
|
||||
}
|
||||
|
||||
copy_text_with_status :: proc(text, status: string) -> string {
|
||||
rl.SetClipboardText(fmt.ctprintf("%s", text))
|
||||
return status
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "../core"
|
||||
import "../shared"
|
||||
|
||||
collect_script_panels :: proc(script: core.Comic_Script) -> []core.Panel {
|
||||
out: [dynamic]core.Panel
|
||||
for p in script.pages {
|
||||
for pan in p.panels {
|
||||
append(&out, pan)
|
||||
}
|
||||
}
|
||||
return out[:]
|
||||
}
|
||||
|
||||
count_script_panels :: proc(script: core.Comic_Script) -> int {
|
||||
count := 0
|
||||
for p in script.pages {
|
||||
count += len(p.panels)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
local_panel_id_by_index :: proc(i: int) -> string {
|
||||
switch i {
|
||||
case 0: return "panel_local_001"
|
||||
case 1: return "panel_local_002"
|
||||
case 2: return "panel_local_003"
|
||||
case 3: return "panel_local_004"
|
||||
case 4: return "panel_local_005"
|
||||
case 5: return "panel_local_006"
|
||||
}
|
||||
return "panel_local_overflow"
|
||||
}
|
||||
|
||||
build_local_script :: proc(story_idea: string, pages: int) -> core.Comic_Script {
|
||||
out_pages: [dynamic]core.Page
|
||||
for i in 0..<pages {
|
||||
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"}
|
||||
|
||||
panel := core.Panel{
|
||||
panel_id = local_panel_id_by_index(i),
|
||||
panel_number = 1,
|
||||
shot_type = .Medium,
|
||||
description = story_idea,
|
||||
characters_present = chars_present,
|
||||
dialogue = dialogue,
|
||||
caption = "",
|
||||
sound_effects = nil,
|
||||
transition_from_previous = .None,
|
||||
}
|
||||
panels := make([]core.Panel, 1)
|
||||
panels[0] = panel
|
||||
append(&out_pages, core.Page{page_number = i + 1, layout_type = .Grid, panels = panels})
|
||||
}
|
||||
|
||||
chars := make([]core.Character, 1)
|
||||
chars[0] = core.Character{id = "char_001", name = "Protagonist", role = .Protagonist, description = "Main character"}
|
||||
return core.Comic_Script{title = "Local Script", synopsis = story_idea, characters = chars, pages = out_pages[:]}
|
||||
}
|
||||
|
||||
build_local_panel_images :: proc(panels: []core.Panel) -> (map[string]core.Panel_Image, shared.App_Error) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-gui-local-panels-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
return nil, shared.new_error(.Generation, "failed to create local panel temp dir", true)
|
||||
}
|
||||
|
||||
images := make(map[string]core.Panel_Image)
|
||||
for p, idx in panels {
|
||||
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 {
|
||||
delete(out_path)
|
||||
return nil, shared.new_error(.Generation, "failed writing local panel image", 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}
|
||||
delete(out_path)
|
||||
}
|
||||
return images, shared.ok()
|
||||
}
|
||||
|
||||
append_char :: proc(dst: ^string, ch: rune) {
|
||||
dst^ = fmt.aprintf("%s%c", dst^, ch)
|
||||
}
|
||||
|
||||
pop_char :: proc(dst: ^string) {
|
||||
if len(dst^) == 0 {
|
||||
return
|
||||
}
|
||||
dst^ = dst^[:len(dst^)-1]
|
||||
}
|
||||
|
||||
recommended_label_from_hint :: proc(hint: string) -> string {
|
||||
switch hint {
|
||||
case "generate script":
|
||||
return "Generate Script"
|
||||
case "generate script local":
|
||||
return "Generate Script Local"
|
||||
case "generate panels local":
|
||||
return "Generate Panels Local"
|
||||
case "layout auto":
|
||||
return "Layout"
|
||||
case "export pdf":
|
||||
return "Export"
|
||||
}
|
||||
return "Next"
|
||||
}
|
||||
|
||||
pending_action_name :: proc(a: Pending_Confirm_Action) -> string {
|
||||
switch a {
|
||||
case .Reset_Project: return "reset project"
|
||||
case .Open_Project: return "open project"
|
||||
case .None: return "continue"
|
||||
}
|
||||
return "continue"
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_action_log :: proc(log: Action_Log, x, y, max_visible: i32, oldest_first: bool) {
|
||||
now := rl.GetTime()
|
||||
max_lines := len(log.entries)
|
||||
if log.count < max_lines {
|
||||
max_lines = log.count
|
||||
}
|
||||
if max_visible > 0 && int(max_visible) < max_lines {
|
||||
max_lines = int(max_visible)
|
||||
}
|
||||
for line in 0..<max_lines {
|
||||
idx := 0
|
||||
if oldest_first {
|
||||
start := log.count - max_lines
|
||||
idx = (start + line) % len(log.entries)
|
||||
} else {
|
||||
idx = (log.count - 1 - line) % len(log.entries)
|
||||
}
|
||||
if idx < 0 {
|
||||
idx += len(log.entries)
|
||||
}
|
||||
row_y := y + i32(line*22)
|
||||
if line % 2 == 0 {
|
||||
rl.DrawRectangleRounded(rl.Rectangle{x = f32(x-4), y = f32(row_y-2), width = 442, height = 20}, 0.2, 6, LOG_ROW_ALT)
|
||||
}
|
||||
age := now - log.entry_times[idx]
|
||||
line_text := fmt.tprintf("[%2.0fs] %s", age, log.entries[idx])
|
||||
draw_text_fitted(line_text, x, row_y, 15, 430, 7, LOG_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
is_error_message :: proc(msg: string) -> bool {
|
||||
return strings.contains(msg, "failed") || strings.contains(msg, "blocked") || strings.contains(msg, "No script")
|
||||
}
|
||||
|
||||
is_warning_message :: proc(msg: string) -> bool {
|
||||
return strings.contains(msg, "Unsaved") || strings.contains(msg, "Confirm") || strings.contains(msg, "requires") || strings.contains(msg, "before") || strings.contains(msg, "Cancelled")
|
||||
}
|
||||
|
||||
status_text_color :: proc(msg: string) -> rl.Color {
|
||||
if is_error_message(msg) {
|
||||
return ERROR
|
||||
}
|
||||
if is_warning_message(msg) {
|
||||
return WARNING
|
||||
}
|
||||
return SUCCESS
|
||||
}
|
||||
|
||||
toast_bg_color :: proc(msg: string) -> rl.Color {
|
||||
if is_error_message(msg) {
|
||||
return TOAST_ERROR
|
||||
}
|
||||
if is_warning_message(msg) {
|
||||
return TOAST_WARNING
|
||||
}
|
||||
return TOAST_SUCCESS
|
||||
}
|
||||
|
||||
draw_toast :: proc(log: Action_Log, x, y, w: i32) {
|
||||
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 := toast_bg_color(msg)
|
||||
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 34}
|
||||
shadow := rl.Rectangle{x = f32(x), y = f32(y + 2), width = f32(w), height = 34}
|
||||
rl.DrawRectangleRounded(shadow, RADIUS_TOAST, 8, TOAST_SHADOW)
|
||||
rl.DrawRectangleRounded(rec, RADIUS_TOAST, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_TOAST, 8, 1.0, TOAST_BORDER)
|
||||
draw_text_fitted(msg, x+10, y+8, 16, int(w-20), 8, TEXT_BRIGHT)
|
||||
}
|
||||
|
||||
draw_help_line :: proc(x, y: i32, text: string) {
|
||||
draw_text_fitted(text, x, y, 16, 820, 8, HELP_LINE)
|
||||
}
|
||||
|
||||
draw_help_overlay :: proc() {
|
||||
sw := rl.GetScreenWidth()
|
||||
sh := rl.GetScreenHeight()
|
||||
rec := rl.Rectangle{x = f32((sw-860)/2), y = f32((sh-642)/2), width = 860, height = 642}
|
||||
rl.DrawRectangle(0, 0, sw, sh, BG_OVERLAY)
|
||||
draw_card(rec)
|
||||
x := i32(rec.x) + 30
|
||||
y := i32(rec.y) + 28
|
||||
rl.DrawText("Keyboard Shortcuts", x, y, 28, HELP_TITLE)
|
||||
rl.DrawText("Navigation", x, y+44, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+72, "1..8 screens | TAB fields | click to focus | F11 pages | F12 project")
|
||||
rl.DrawText("Core Actions", x, y+106, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+134, "F5 script F6 panels F7 layout F8 export F9 next F10 auto-all")
|
||||
draw_help_line(x, y+160, "Ctrl+S save | Ctrl+O open | Ctrl+N new | Ctrl+E export | Ctrl+H/J summary")
|
||||
rl.DrawText("Clipboard + Logs", x, y+196, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+224, "Ctrl+L clear log | Ctrl+Shift+L copy log | Ctrl+Shift+T/B log view | Ctrl+Shift+Z reset")
|
||||
draw_help_line(x, y+248, "Ctrl+Shift+C status")
|
||||
draw_help_line(x, y+272, "Ctrl+Shift+Y diag copy | Ctrl+Shift+R diag file | Ctrl+Shift+W report")
|
||||
draw_help_line(x, y+296, "Ctrl+0 reset helpers | Ctrl+V paste | Ctrl+Shift+I copy | Ctrl+Backspace clear")
|
||||
rl.DrawText("Paths", x, y+332, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+360, "Ctrl+Shift+X copy export | P preset | D exp-from-project | G proj-from-export")
|
||||
draw_help_line(x, y+386, "Ctrl+Shift+J fix project | F fix export | K/M quick-fix P/E | U fix all")
|
||||
rl.DrawText("Autosave", x, y+422, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+450, "Ctrl+Shift+A toggle | Ctrl+-/= adjust | Ctrl+7/8/9 set 15/30/60")
|
||||
rl.DrawText("Safety", x, y+486, 20, HELP_SECTION)
|
||||
draw_help_line(x, y+514, "Dirty guard: Shift-click New/Open | Keyboard confirm: Ctrl+Shift+N / Ctrl+Shift+O")
|
||||
rl.DrawText("Close help: Esc or /", x, y+542, 18, HELP_CLOSE)
|
||||
}
|
||||
|
||||
draw_sidebar_shortcut_line :: proc(x, y: i32, text: string, c: rl.Color) {
|
||||
draw_text_fitted(text, x, y, 14, 220, 7, c)
|
||||
}
|
||||
|
||||
draw_sidebar_shortcuts :: proc(screen_h: i32) {
|
||||
base_y := screen_h - 280
|
||||
if base_y < 120 {
|
||||
base_y = 120
|
||||
}
|
||||
draw_card(rl.Rectangle{x = 14, y = f32(base_y), width = 236, height = 210})
|
||||
rl.DrawText("Quick Keys", 26, base_y+12, 18, SIDEBAR_TITLE)
|
||||
draw_sidebar_shortcut_line(26, base_y+36, "F5/F6/F7/F8 generate/layout/export", SIDEBAR_TEXT)
|
||||
draw_sidebar_shortcut_line(26, base_y+54, "Ctrl+S save Ctrl+O open", SIDEBAR_TEXT)
|
||||
draw_sidebar_shortcut_line(26, base_y+72, "Ctrl+N new", SIDEBAR_TEXT)
|
||||
draw_sidebar_shortcut_line(26, base_y+90, "F9 next F10 auto-all", SIDEBAR_TEXT)
|
||||
draw_sidebar_shortcut_line(26, base_y+108, "/ full shortcut help", SIDEBAR_TEXT)
|
||||
draw_sidebar_shortcut_line(26, base_y+140, "Press / for all shortcuts", SIDEBAR_FOOTER)
|
||||
}
|
||||
|
||||
draw_confirm_overlay :: proc(action: Pending_Confirm_Action) {
|
||||
sw := rl.GetScreenWidth()
|
||||
sh := rl.GetScreenHeight()
|
||||
rec := rl.Rectangle{x = f32((sw-520)/2), y = f32((sh-230)/2), width = 520, height = 230}
|
||||
rl.DrawRectangle(0, 0, sw, sh, BG_OVERLAY)
|
||||
draw_card(rec)
|
||||
rl.DrawRectangleRounded(rl.Rectangle{x = rec.x, y = rec.y, width = rec.width, height = 8}, 0.08, 8, CONFIRM_ACCENT)
|
||||
x := i32(rec.x) + 30
|
||||
y := i32(rec.y) + 34
|
||||
rl.DrawText("Confirm destructive action", x, y, 26, CONFIRM_TITLE)
|
||||
draw_text_fitted(fmt.tprintf("You have unsaved changes. Do you want to %s?", pending_action_name(action)), x, y+42, 18, 470, 8, CONFIRM_BODY)
|
||||
rl.DrawText("Enter/Y confirm • Esc/N cancel", x, y+72, 16, CONFIRM_HINT)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
import "../core"
|
||||
|
||||
format_suffix :: proc(f: core.Export_Format) -> string {
|
||||
switch f {
|
||||
case .PDF: return ".pdf"
|
||||
case .PNG: return ".zip"
|
||||
case .CBZ: return ".cbz"
|
||||
}
|
||||
return ".pdf"
|
||||
}
|
||||
|
||||
trim_known_export_suffix :: proc(path: string) -> string {
|
||||
if strings.has_suffix(path, ".pdf") { return path[:len(path)-4] }
|
||||
if strings.has_suffix(path, ".zip") { return path[:len(path)-4] }
|
||||
if strings.has_suffix(path, ".cbz") { return path[:len(path)-4] }
|
||||
if strings.has_suffix(path, ".png") { return path[:len(path)-4] }
|
||||
return path
|
||||
}
|
||||
|
||||
export_path_for_format :: proc(path: string, f: core.Export_Format) -> string {
|
||||
base := trim_known_export_suffix(path)
|
||||
return fmt.aprintf("%s%s", base, format_suffix(f))
|
||||
}
|
||||
|
||||
default_export_filename_for_format :: proc(f: core.Export_Format) -> string {
|
||||
switch f {
|
||||
case .PDF: return "comic.pdf"
|
||||
case .PNG: return "comic_png.zip"
|
||||
case .CBZ: return "comic.cbz"
|
||||
}
|
||||
return "comic.pdf"
|
||||
}
|
||||
|
||||
default_export_path_for_format :: proc(f: core.Export_Format) -> string {
|
||||
return fmt.aprintf("./%s", default_export_filename_for_format(f))
|
||||
}
|
||||
|
||||
export_path_in_project_dir :: proc(project_path: string, f: core.Export_Format) -> string {
|
||||
dir, _ := filepath.split(project_path)
|
||||
if len(dir) == 0 {
|
||||
dir = "./"
|
||||
}
|
||||
parts := []string{dir, default_export_filename_for_format(f)}
|
||||
joined, err := filepath.join(parts)
|
||||
if err != nil {
|
||||
return default_export_path_for_format(f)
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
reset_helper_fields :: proc(export_path, local_script_pages, autosave_interval_text: ^string, f: core.Export_Format) {
|
||||
export_path^ = default_export_path_for_format(f)
|
||||
local_script_pages^ = "2"
|
||||
autosave_interval_text^ = "20"
|
||||
}
|
||||
|
||||
sync_export_path_to_project_dir :: proc(project_path: string, export_path: ^string, f: core.Export_Format) {
|
||||
export_path^ = export_path_in_project_dir(project_path, f)
|
||||
}
|
||||
|
||||
project_path_in_export_dir :: proc(export_path: string) -> string {
|
||||
dir, _ := filepath.split(export_path)
|
||||
if len(dir) == 0 {
|
||||
dir = "./"
|
||||
}
|
||||
parts := []string{dir, "gui_project.comic.json"}
|
||||
joined, err := filepath.join(parts)
|
||||
if err != nil {
|
||||
return "./gui_project.comic.json"
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
normalize_project_path :: proc(path: string) -> string {
|
||||
trimmed := strings.trim_space(path)
|
||||
if len(trimmed) == 0 {
|
||||
return "./gui_project.comic.json"
|
||||
}
|
||||
if strings.has_suffix(trimmed, ".comic.json") {
|
||||
return trimmed
|
||||
}
|
||||
if strings.has_suffix(trimmed, ".json") {
|
||||
return fmt.aprintf("%s.comic.json", trimmed[:len(trimmed)-5])
|
||||
}
|
||||
return fmt.aprintf("%s.comic.json", trimmed)
|
||||
}
|
||||
|
||||
normalize_project_path_field :: proc(path: ^string) {
|
||||
path^ = normalize_project_path(path^)
|
||||
}
|
||||
|
||||
normalize_export_path_field :: proc(path: ^string, f: core.Export_Format) {
|
||||
trimmed := strings.trim_space(path^)
|
||||
if len(trimmed) == 0 {
|
||||
path^ = default_export_path_for_format(f)
|
||||
return
|
||||
}
|
||||
path^ = export_path_for_format(trimmed, f)
|
||||
}
|
||||
|
||||
fix_all_paths :: proc(project_path, export_path: ^string, f: core.Export_Format) {
|
||||
normalize_project_path_field(project_path)
|
||||
normalize_export_path_field(export_path, f)
|
||||
}
|
||||
|
||||
set_export_preset_with_message :: proc(export_path: ^string, f: core.Export_Format) -> string {
|
||||
export_path^ = default_export_path_for_format(f)
|
||||
return fmt.aprintf("Preset export path: %s", export_path^)
|
||||
}
|
||||
|
||||
set_export_path_from_project_with_message :: proc(export_path: ^string, project_path: string, f: core.Export_Format) -> string {
|
||||
export_path^ = export_path_in_project_dir(project_path, f)
|
||||
return fmt.aprintf("Export path from project dir: %s", export_path^)
|
||||
}
|
||||
|
||||
set_project_path_from_export_with_message :: proc(project_path: ^string, export_path: string) -> string {
|
||||
project_path^ = project_path_in_export_dir(export_path)
|
||||
return fmt.aprintf("Project path from export dir: %s", project_path^)
|
||||
}
|
||||
|
||||
normalize_project_path_with_message :: proc(project_path: ^string) -> string {
|
||||
normalize_project_path_field(project_path)
|
||||
return fmt.aprintf("Normalized project path: %s", project_path^)
|
||||
}
|
||||
|
||||
normalize_export_path_with_message :: proc(export_path: ^string, f: core.Export_Format) -> string {
|
||||
normalize_export_path_field(export_path, f)
|
||||
return fmt.aprintf("Normalized export path: %s", export_path^)
|
||||
}
|
||||
|
||||
fix_all_paths_with_message :: proc(project_path, export_path: ^string, f: core.Export_Format) -> string {
|
||||
fix_all_paths(project_path, export_path, f)
|
||||
return fmt.aprintf("Normalized paths: P=%s E=%s", project_path^, export_path^)
|
||||
}
|
||||
|
||||
project_path_is_normalized :: proc(path: string) -> bool {
|
||||
trimmed := strings.trim_space(path)
|
||||
return len(trimmed) > 0 && strings.has_suffix(trimmed, ".comic.json")
|
||||
}
|
||||
|
||||
export_path_matches_format :: proc(path: string, f: core.Export_Format) -> bool {
|
||||
trimmed := strings.trim_space(path)
|
||||
if len(trimmed) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.has_suffix(trimmed, format_suffix(f))
|
||||
}
|
||||
|
||||
path_health_hint :: proc(project_ok, export_ok: bool) -> string {
|
||||
if project_ok && export_ok {
|
||||
return ""
|
||||
}
|
||||
if !project_ok && !export_ok {
|
||||
return "Fix paths: P/E/PE buttons or Ctrl+Shift+U"
|
||||
}
|
||||
if !project_ok {
|
||||
return "Fix project path: P button or Ctrl+Shift+K"
|
||||
}
|
||||
return "Fix export path: E button or Ctrl+Shift+M"
|
||||
}
|
||||
@@ -0,0 +1,973 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
import "../core"
|
||||
import "../shared"
|
||||
import "../ui"
|
||||
|
||||
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)
|
||||
|
||||
selected_field := 0 // 0 idea, 1 genre, 2 audience, 3 export path, 4 local pages, 5 project path, 6 autosave interval
|
||||
export_path := "./gui_export.pdf"
|
||||
project_path := "./gui_project.comic.json"
|
||||
local_script_pages := "2"
|
||||
autosave_interval_text := "20"
|
||||
export_format: core.Export_Format = .PDF
|
||||
use_deepseek_script := false
|
||||
status_msg := fmt.aprintf("GUI ready")
|
||||
is_dirty := false
|
||||
autosave_enabled := true
|
||||
autosave_interval_s: f64 = 20
|
||||
last_autosave_at := rl.GetTime()
|
||||
last_save_at: f64 = -1
|
||||
last_export_at: f64 = -1
|
||||
action_log: Action_Log
|
||||
log_show_lines: i32 = 6
|
||||
log_oldest_first := false
|
||||
summary_opts := Summary_View_Options{}
|
||||
show_help_overlay := false
|
||||
show_confirm_overlay := false
|
||||
pending_confirm_action: Pending_Confirm_Action = .None
|
||||
push_status(&status_msg, &action_log, status_msg)
|
||||
defer action_log_dispose(&action_log)
|
||||
defer delete(status_msg)
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
screen_w_loop := rl.GetScreenWidth()
|
||||
screen_h_loop := rl.GetScreenHeight()
|
||||
compact_mode := screen_h_loop < 860
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
audience_rec := rl.Rectangle{x = 420, y = 202, width = f32(main_w_loop-460), height = 36}
|
||||
export_rec := rl.Rectangle{x = 420, y = 258, width = f32(main_w_loop-460), height = 36}
|
||||
project_rec := rl.Rectangle{x = 420, y = 314, width = f32(main_w_loop-460), height = 36}
|
||||
pages_rec := rl.Rectangle{x = f32(screen_w_loop - 120), y = 22, width = 100, height = 28}
|
||||
|
||||
fmt_pdf_btn := rl.Rectangle{x = 420, y = 400, width = 80, height = 30}
|
||||
fmt_png_btn := rl.Rectangle{x = 510, y = 400, width = 80, height = 30}
|
||||
fmt_cbz_btn := rl.Rectangle{x = 600, y = 400, width = 80, height = 30}
|
||||
script_src_local_btn := rl.Rectangle{x = 800, y = 400, width = 92, height = 30}
|
||||
script_src_deepseek_btn := rl.Rectangle{x = 898, y = 400, width = 120, height = 30}
|
||||
|
||||
// Main Actions Row 1 (y=470)
|
||||
new_btn := rl.Rectangle{x = 290, y = 470, width = 140, height = 38}
|
||||
script_btn := rl.Rectangle{x = 440, y = 470, width = 230, height = 38}
|
||||
panels_btn := rl.Rectangle{x = 680, y = 470, width = 230, height = 38}
|
||||
layout_btn := rl.Rectangle{x = 920, y = 470, width = 140, height = 38}
|
||||
export_btn := rl.Rectangle{x = 1070, y = 470, width = 140, height = 38}
|
||||
|
||||
// Secondary Actions Row 2 (y=518)
|
||||
save_btn := rl.Rectangle{x = 290, y = 518, width = 160, height = 38}
|
||||
open_btn := rl.Rectangle{x = 460, y = 518, width = 160, height = 38}
|
||||
next_btn := rl.Rectangle{x = 630, y = 518, width = 160, height = 38}
|
||||
auto_btn := rl.Rectangle{x = 800, y = 518, width = 160, height = 38}
|
||||
auto_save_btn := rl.Rectangle{x = 970, y = 518, width = 240, height = 38}
|
||||
|
||||
// Utility Strip Row 3 (y=566)
|
||||
autosave_btn := rl.Rectangle{x = 290, y = 566, width = 160, height = 34}
|
||||
autosave_rec := rl.Rectangle{x = 600, y = 568, width = 70, height = 30}
|
||||
autosave_15_btn := rl.Rectangle{x = 680, y = 568, width = 44, height = 30}
|
||||
autosave_30_btn := rl.Rectangle{x = 730, y = 568, width = 44, height = 30}
|
||||
autosave_60_btn := rl.Rectangle{x = 780, y = 568, width = 44, height = 30}
|
||||
help_btn := rl.Rectangle{x = 850, y = 566, width = 110, height = 34}
|
||||
clear_field_btn := rl.Rectangle{x = 970, y = 566, width = 110, height = 34}
|
||||
reset_helpers_btn := rl.Rectangle{x = 1090, y = 566, width = 120, height = 34}
|
||||
|
||||
// Path Fixes (now below project inputs)
|
||||
export_copy_btn := rl.Rectangle{x = 420, y = 360, width = 110, height = 24}
|
||||
export_preset_btn := rl.Rectangle{x = 540, y = 360, width = 110, height = 24}
|
||||
path_fix_btn := rl.Rectangle{x = 660, y = 360, width = 110, height = 24}
|
||||
project_fix_btn := rl.Rectangle{x = 780, y = 360, width = 110, height = 24}
|
||||
project_from_export_btn := rl.Rectangle{x = 900, y = 360, width = 110, height = 24}
|
||||
export_project_btn := rl.Rectangle{x = 1020, y = 360, width = 110, height = 24}
|
||||
|
||||
log_reset_btn := rl.Rectangle{x = f32(log_x_loop + 18), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
report_file_btn := rl.Rectangle{x = f32(log_x_loop + 92), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
log_copy_btn := rl.Rectangle{x = f32(log_x_loop + 166), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
script_copy_page_btn := rl.Rectangle{x = f32(log_x_loop + 18), y = f32(lower_y_loop + 2), width = 96, height = 26}
|
||||
script_copy_all_btn := rl.Rectangle{x = f32(log_x_loop + 120), y = f32(lower_y_loop + 2), width = 86, height = 26}
|
||||
diag_file_btn := rl.Rectangle{x = f32(log_x_loop + 240), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
status_copy_btn := rl.Rectangle{x = f32(log_x_loop + 314), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
log_clear_btn := rl.Rectangle{x = f32(log_x_loop + 388), y = f32(lower_y_loop + 2), width = 68, height = 26}
|
||||
diag_copy_btn := rl.Rectangle{x = f32(log_x_loop + 314), y = f32(lower_y_loop + 32), width = 68, height = 26}
|
||||
confirm_base_x := (screen_w_loop - 520) / 2
|
||||
confirm_base_y := (screen_h_loop - 230) / 2
|
||||
confirm_yes_btn := rl.Rectangle{x = f32(confirm_base_x + 180), y = f32(confirm_base_y + 154), width = 140, height = 34}
|
||||
confirm_no_btn := rl.Rectangle{x = f32(confirm_base_x + 330), y = f32(confirm_base_y + 154), width = 140, height = 34}
|
||||
path_fix_project_status_btn := rl.Rectangle{x = 638, y = 556, width = 32, height = 20}
|
||||
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_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}
|
||||
|
||||
if rl.IsKeyPressed(.SLASH) {
|
||||
toggle_help_overlay(&show_help_overlay)
|
||||
}
|
||||
if rl.IsKeyPressed(.ESCAPE) {
|
||||
close_help_overlay_if_open(&show_help_overlay)
|
||||
}
|
||||
interaction_locked := show_help_overlay || show_confirm_overlay
|
||||
|
||||
nav_story := rl.Rectangle{x = 16, y = 90, width = 228, height = 32}
|
||||
nav_script := rl.Rectangle{x = 16, y = 126, width = 228, height = 32}
|
||||
nav_chars := rl.Rectangle{x = 16, y = 162, width = 228, height = 32}
|
||||
nav_panels := rl.Rectangle{x = 16, y = 198, width = 228, height = 32}
|
||||
nav_layout := rl.Rectangle{x = 16, y = 234, width = 228, height = 32}
|
||||
nav_bubbles := rl.Rectangle{x = 16, y = 270, width = 228, height = 32}
|
||||
nav_export := rl.Rectangle{x = 16, y = 306, width = 228, height = 32}
|
||||
nav_community := rl.Rectangle{x = 16, y = 342, width = 228, height = 32}
|
||||
|
||||
if !interaction_locked {
|
||||
if rl.IsKeyPressed(.ONE) { _ = ui.navigate_to_screen(&controller, .Story) }
|
||||
if rl.IsKeyPressed(.TWO) { _ = ui.navigate_to_screen(&controller, .Script) }
|
||||
if rl.IsKeyPressed(.THREE) { _ = ui.navigate_to_screen(&controller, .Characters) }
|
||||
if rl.IsKeyPressed(.FOUR) { _ = ui.navigate_to_screen(&controller, .Panels) }
|
||||
if rl.IsKeyPressed(.FIVE) { _ = ui.navigate_to_screen(&controller, .Layout) }
|
||||
if rl.IsKeyPressed(.SIX) { _ = ui.navigate_to_screen(&controller, .Bubbles) }
|
||||
if rl.IsKeyPressed(.SEVEN) { _ = ui.navigate_to_screen(&controller, .Export) }
|
||||
if rl.IsKeyPressed(.EIGHT) { _ = ui.navigate_to_screen(&controller, .Community) }
|
||||
|
||||
if button_clicked(nav_story) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Story))
|
||||
}
|
||||
if button_clicked(nav_script) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Script))
|
||||
}
|
||||
if button_clicked(nav_chars) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Characters))
|
||||
}
|
||||
if button_clicked(nav_panels) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Panels))
|
||||
}
|
||||
if button_clicked(nav_layout) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Layout))
|
||||
}
|
||||
if button_clicked(nav_bubbles) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Bubbles))
|
||||
}
|
||||
if button_clicked(nav_export) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Export))
|
||||
}
|
||||
if button_clicked(nav_community) {
|
||||
push_status(&status_msg, &action_log, navigate_screen_with_status(&controller, .Community))
|
||||
}
|
||||
|
||||
if rl.IsKeyPressed(.TAB) { selected_field = (selected_field + 1) % 7 }
|
||||
if rl.IsKeyPressed(.F1) { selected_field = 0 }
|
||||
if rl.IsKeyPressed(.F2) { selected_field = 1 }
|
||||
if rl.IsKeyPressed(.F3) { selected_field = 2 }
|
||||
if rl.IsKeyPressed(.F4) { selected_field = 3 }
|
||||
if rl.IsKeyPressed(.F11) { selected_field = 4 }
|
||||
if rl.IsKeyPressed(.F12) { selected_field = 5 }
|
||||
if button_clicked(idea_rec) { selected_field = 0 }
|
||||
if button_clicked(genre_rec) { selected_field = 1 }
|
||||
if button_clicked(audience_rec) { selected_field = 2 }
|
||||
if button_clicked(export_rec) { selected_field = 3 }
|
||||
if button_clicked(pages_rec) { selected_field = 4 }
|
||||
if button_clicked(project_rec) { selected_field = 5 }
|
||||
if button_clicked(autosave_rec) { selected_field = 6 }
|
||||
if button_clicked(autosave_15_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 15))
|
||||
}
|
||||
if button_clicked(autosave_30_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 30))
|
||||
}
|
||||
if button_clicked(autosave_60_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 60))
|
||||
}
|
||||
}
|
||||
|
||||
pages_count := parse_pages_or_default(local_script_pages, 2)
|
||||
autosave_secs := parse_autosave_interval(autosave_interval_text, 20)
|
||||
autosave_interval_s = f64(autosave_secs)
|
||||
if selected_field != 6 && len(strings.trim_space(autosave_interval_text)) == 0 {
|
||||
autosave_interval_text = fmt.aprintf("%d", autosave_secs)
|
||||
}
|
||||
shift_down := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
|
||||
can_generate_panels := len(controller.state.script.pages) > 0
|
||||
can_layout := len(controller.state.panel_images) > 0
|
||||
can_export := len(controller.state.page_layouts) > 0 && len(controller.state.panel_images) > 0
|
||||
project_path_ok := project_path_is_normalized(project_path)
|
||||
export_path_ok := export_path_matches_format(export_path, export_format)
|
||||
|
||||
if show_confirm_overlay {
|
||||
confirm_yes := button_clicked(confirm_yes_btn) || rl.IsKeyPressed(.ENTER) || rl.IsKeyPressed(.Y)
|
||||
confirm_no := button_clicked(confirm_no_btn) || rl.IsKeyPressed(.ESCAPE) || rl.IsKeyPressed(.N)
|
||||
if confirm_no {
|
||||
show_confirm_overlay = false
|
||||
pending_confirm_action = .None
|
||||
push_status(&status_msg, &action_log, "Cancelled destructive action")
|
||||
} else if confirm_yes {
|
||||
action := pending_confirm_action
|
||||
show_confirm_overlay = false
|
||||
pending_confirm_action = .None
|
||||
push_status(&status_msg, &action_log, resolve_confirm_action_with_message(action, &controller, &project_path, &export_path, export_format, &is_dirty, &last_autosave_at))
|
||||
}
|
||||
}
|
||||
|
||||
if !interaction_locked {
|
||||
if button_clicked(export_copy_btn) {
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(export_path, "Copied export path to clipboard"))
|
||||
}
|
||||
if button_clicked(export_preset_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_export_preset_with_message(&export_path, export_format))
|
||||
}
|
||||
if button_clicked(path_fix_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_export_path_with_message(&export_path, export_format))
|
||||
}
|
||||
if button_clicked(project_fix_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_project_path_with_message(&project_path))
|
||||
}
|
||||
if button_clicked(project_from_export_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_project_path_from_export_with_message(&project_path, export_path))
|
||||
}
|
||||
if button_clicked(export_project_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_export_path_from_project_with_message(&export_path, project_path, export_format))
|
||||
}
|
||||
if button_clicked(path_fix_project_status_btn) && !project_path_ok {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_project_path_with_message(&project_path))
|
||||
}
|
||||
if button_clicked(path_fix_export_status_btn) && !export_path_ok {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_export_path_with_message(&export_path, export_format))
|
||||
}
|
||||
if button_clicked(path_fix_all_status_btn) && (!project_path_ok || !export_path_ok) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, fix_all_paths_with_message(&project_path, &export_path, export_format))
|
||||
}
|
||||
if button_clicked(fmt_pdf_btn) {
|
||||
push_status(&status_msg, &action_log, set_export_format_with_message(&export_format, &export_path, .PDF, &is_dirty))
|
||||
}
|
||||
if button_clicked(fmt_png_btn) {
|
||||
push_status(&status_msg, &action_log, set_export_format_with_message(&export_format, &export_path, .PNG, &is_dirty))
|
||||
}
|
||||
if button_clicked(fmt_cbz_btn) {
|
||||
push_status(&status_msg, &action_log, set_export_format_with_message(&export_format, &export_path, .CBZ, &is_dirty))
|
||||
}
|
||||
if button_clicked(script_src_local_btn) {
|
||||
use_deepseek_script = false
|
||||
push_status(&status_msg, &action_log, "Script source: Local")
|
||||
}
|
||||
if button_clicked(script_src_deepseek_btn) {
|
||||
if !has_deepseek_key {
|
||||
push_status(&status_msg, &action_log, "DeepSeek key missing (set DEEPSEEK_API_KEY)")
|
||||
} else {
|
||||
use_deepseek_script = true
|
||||
push_status(&status_msg, &action_log, "Script source: DeepSeek")
|
||||
}
|
||||
}
|
||||
|
||||
if button_clicked(summary_show_btn) {
|
||||
push_status_if_nonempty(&status_msg, &action_log, toggle_summary_show_if_supported(controller.active_screen, &summary_opts))
|
||||
}
|
||||
if button_clicked(summary_sort_btn) {
|
||||
push_status_if_nonempty(&status_msg, &action_log, toggle_summary_sort_if_supported(controller.active_screen, &summary_opts))
|
||||
}
|
||||
if controller.active_screen == .Script && button_clicked(summary_prev_btn) {
|
||||
page_count := len(controller.state.script.pages)
|
||||
if page_count > 0 {
|
||||
summary_opts.script_page_cursor -= 1
|
||||
if summary_opts.script_page_cursor < 0 {
|
||||
summary_opts.script_page_cursor = page_count - 1
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing script page %d/%d", summary_opts.script_page_cursor+1, page_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Script && button_clicked(summary_next_btn) {
|
||||
page_count := len(controller.state.script.pages)
|
||||
if page_count > 0 {
|
||||
summary_opts.script_page_cursor += 1
|
||||
if summary_opts.script_page_cursor >= page_count {
|
||||
summary_opts.script_page_cursor = 0
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing script page %d/%d", summary_opts.script_page_cursor+1, page_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Panels && button_clicked(summary_prev_btn) {
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if panel_count > 0 {
|
||||
summary_opts.panel_cursor -= 1
|
||||
if summary_opts.panel_cursor < 0 {
|
||||
summary_opts.panel_cursor = panel_count - 1
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Panels && button_clicked(summary_next_btn) {
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if panel_count > 0 {
|
||||
summary_opts.panel_cursor += 1
|
||||
if summary_opts.panel_cursor >= panel_count {
|
||||
summary_opts.panel_cursor = 0
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
|
||||
if button_clicked(new_btn) {
|
||||
if is_dirty && !shift_down {
|
||||
push_status(&status_msg, &action_log, request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_confirm_action, .Reset_Project, "Confirm reset?"))
|
||||
} else {
|
||||
push_status(&status_msg, &action_log, reset_project_session(&controller, &is_dirty, &last_autosave_at, false))
|
||||
}
|
||||
}
|
||||
if button_clicked(save_btn) {
|
||||
push_status(&status_msg, &action_log, save_project_session_with_message(&project_path, controller.state, &is_dirty, &last_autosave_at, &last_save_at, "Saved project"))
|
||||
}
|
||||
if button_clicked(open_btn) {
|
||||
if is_dirty && !shift_down {
|
||||
push_status(&status_msg, &action_log, request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_confirm_action, .Open_Project, "Confirm open?"))
|
||||
} else {
|
||||
push_status(&status_msg, &action_log, open_project_session(&controller, &project_path, &export_path, export_format, &is_dirty, &last_autosave_at))
|
||||
}
|
||||
}
|
||||
if button_clicked(auto_save_btn) {
|
||||
push_status(&status_msg, &action_log, run_auto_all_save_action(&controller, &project_path, &export_path, export_format, pages_count, use_deepseek_script, &is_dirty, &last_export_at, &last_autosave_at, &last_save_at))
|
||||
}
|
||||
if button_clicked(autosave_btn) {
|
||||
push_status(&status_msg, &action_log, toggle_autosave_with_message(&autosave_enabled))
|
||||
}
|
||||
if button_clicked(help_btn) {
|
||||
toggle_help_overlay(&show_help_overlay)
|
||||
}
|
||||
if button_clicked(clear_field_btn) {
|
||||
push_status(&status_msg, &action_log, clear_selected_field_with_message(selected_field, &controller.state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty))
|
||||
}
|
||||
if button_clicked(reset_helpers_btn) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, reset_helper_fields_with_message(&export_path, &local_script_pages, &autosave_interval_text, export_format))
|
||||
}
|
||||
if controller.active_screen != .Script {
|
||||
if button_clicked(log_reset_btn) {
|
||||
push_status(&status_msg, &action_log, reset_log_view_with_message(&log_show_lines, &log_oldest_first))
|
||||
}
|
||||
if button_clicked(report_file_btn) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, write_session_report_with_message(diag_ctx))
|
||||
}
|
||||
if button_clicked(log_copy_btn) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, copy_action_log_snapshot_with_message(diag_ctx))
|
||||
}
|
||||
if button_clicked(diag_file_btn) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, write_diagnostics_with_message(diag_ctx))
|
||||
}
|
||||
if button_clicked(log_clear_btn) {
|
||||
set_status(&status_msg, clear_action_log_with_message(&action_log))
|
||||
}
|
||||
if button_clicked(status_copy_btn) {
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(status_msg, "Copied status to clipboard"))
|
||||
}
|
||||
if button_clicked(diag_copy_btn) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, copy_diagnostics_with_message(diag_ctx))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Script && button_clicked(script_copy_page_btn) {
|
||||
page_text := build_script_page_detail_text(controller.state, summary_opts.script_page_cursor)
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(page_text, "Copied script page detail"))
|
||||
delete(page_text)
|
||||
}
|
||||
if controller.active_screen == .Script && button_clicked(script_copy_all_btn) {
|
||||
full_text := build_full_script_text(controller.state)
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(full_text, "Copied full script"))
|
||||
delete(full_text)
|
||||
}
|
||||
if button_clicked(script_btn) {
|
||||
push_status(&status_msg, &action_log, run_script_action(&controller, pages_count, use_deepseek_script, &is_dirty))
|
||||
}
|
||||
if button_clicked(panels_btn) {
|
||||
push_status(&status_msg, &action_log, run_panels_action(&controller, can_generate_panels, &is_dirty))
|
||||
}
|
||||
if button_clicked(layout_btn) {
|
||||
push_status(&status_msg, &action_log, run_layout_action(&controller, can_layout, &is_dirty))
|
||||
}
|
||||
if button_clicked(export_btn) {
|
||||
push_status(&status_msg, &action_log, run_export_action(&controller, &export_path, export_format, can_export, &is_dirty, &last_export_at))
|
||||
}
|
||||
if button_clicked(next_btn) {
|
||||
push_status(&status_msg, &action_log, run_next_action(&controller, &export_path, export_format, pages_count, use_deepseek_script, &is_dirty, &last_export_at))
|
||||
}
|
||||
if button_clicked(auto_btn) {
|
||||
push_status(&status_msg, &action_log, run_auto_all_action(&controller, &export_path, export_format, pages_count, use_deepseek_script, &is_dirty, &last_export_at))
|
||||
}
|
||||
|
||||
if rl.IsKeyPressed(.F5) {
|
||||
push_status(&status_msg, &action_log, run_script_action(&controller, pages_count, use_deepseek_script, &is_dirty))
|
||||
}
|
||||
if rl.IsKeyPressed(.F6) {
|
||||
push_status(&status_msg, &action_log, run_panels_action(&controller, can_generate_panels, &is_dirty))
|
||||
}
|
||||
if rl.IsKeyPressed(.F7) {
|
||||
push_status(&status_msg, &action_log, run_layout_action(&controller, can_layout, &is_dirty))
|
||||
}
|
||||
if rl.IsKeyPressed(.F8) {
|
||||
push_status(&status_msg, &action_log, run_export_action(&controller, &export_path, export_format, can_export, &is_dirty, &last_export_at))
|
||||
}
|
||||
if rl.IsKeyPressed(.F9) {
|
||||
push_status(&status_msg, &action_log, run_next_action(&controller, &export_path, export_format, pages_count, use_deepseek_script, &is_dirty, &last_export_at))
|
||||
}
|
||||
if rl.IsKeyPressed(.F10) {
|
||||
push_status(&status_msg, &action_log, run_auto_all_action(&controller, &export_path, export_format, pages_count, use_deepseek_script, &is_dirty, &last_export_at))
|
||||
}
|
||||
|
||||
ctrl_down := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
|
||||
if ctrl_down && rl.IsKeyPressed(.S) {
|
||||
push_status(&status_msg, &action_log, save_project_session_with_message(&project_path, controller.state, &is_dirty, &last_autosave_at, &last_save_at, "Saved project"))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.N) {
|
||||
if is_dirty && !shift_down {
|
||||
push_status(&status_msg, &action_log, request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_confirm_action, .Reset_Project, "Confirm reset?"))
|
||||
} else {
|
||||
push_status(&status_msg, &action_log, reset_project_session(&controller, &is_dirty, &last_autosave_at, true))
|
||||
}
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.O) {
|
||||
if is_dirty && !shift_down {
|
||||
push_status(&status_msg, &action_log, request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_confirm_action, .Open_Project, "Confirm open?"))
|
||||
} else {
|
||||
push_status(&status_msg, &action_log, open_project_session(&controller, &project_path, &export_path, export_format, &is_dirty, &last_autosave_at))
|
||||
}
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.E) {
|
||||
push_status(&status_msg, &action_log, run_export_action(&controller, &export_path, export_format, can_export, &is_dirty, &last_export_at))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.G) {
|
||||
if use_deepseek_script {
|
||||
use_deepseek_script = false
|
||||
push_status(&status_msg, &action_log, "Script source: Local")
|
||||
} else {
|
||||
if !has_deepseek_key {
|
||||
push_status(&status_msg, &action_log, "DeepSeek key missing (set DEEPSEEK_API_KEY)")
|
||||
} else {
|
||||
use_deepseek_script = true
|
||||
push_status(&status_msg, &action_log, "Script source: DeepSeek")
|
||||
}
|
||||
}
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.H) {
|
||||
push_status_if_nonempty(&status_msg, &action_log, toggle_summary_show_if_supported(controller.active_screen, &summary_opts))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.J) {
|
||||
push_status_if_nonempty(&status_msg, &action_log, toggle_summary_sort_if_supported(controller.active_screen, &summary_opts))
|
||||
}
|
||||
if controller.active_screen == .Script && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) {
|
||||
page_count := len(controller.state.script.pages)
|
||||
if page_count > 0 {
|
||||
summary_opts.script_page_cursor -= 1
|
||||
if summary_opts.script_page_cursor < 0 {
|
||||
summary_opts.script_page_cursor = page_count - 1
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing script page %d/%d", summary_opts.script_page_cursor+1, page_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Script && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) {
|
||||
page_count := len(controller.state.script.pages)
|
||||
if page_count > 0 {
|
||||
summary_opts.script_page_cursor += 1
|
||||
if summary_opts.script_page_cursor >= page_count {
|
||||
summary_opts.script_page_cursor = 0
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing script page %d/%d", summary_opts.script_page_cursor+1, page_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Panels && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) {
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if panel_count > 0 {
|
||||
summary_opts.panel_cursor -= 1
|
||||
if summary_opts.panel_cursor < 0 {
|
||||
summary_opts.panel_cursor = panel_count - 1
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
|
||||
}
|
||||
}
|
||||
if controller.active_screen == .Panels && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) {
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if panel_count > 0 {
|
||||
summary_opts.panel_cursor += 1
|
||||
if summary_opts.panel_cursor >= panel_count {
|
||||
summary_opts.panel_cursor = 0
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.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))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.EQUAL) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, autosave_secs+5))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.SEVEN) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 15))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.EIGHT) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 30))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.NINE) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, 60))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.ZERO) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, reset_helper_fields_with_message(&export_path, &local_script_pages, &autosave_interval_text, export_format))
|
||||
}
|
||||
if ctrl_down && !shift_down && rl.IsKeyPressed(.L) {
|
||||
set_status(&status_msg, clear_action_log_with_message(&action_log))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.L) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, copy_action_log_snapshot_with_message(diag_ctx))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.Z) {
|
||||
push_status(&status_msg, &action_log, reset_log_view_with_message(&log_show_lines, &log_oldest_first))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.W) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, write_session_report_with_message(diag_ctx))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.V) {
|
||||
push_status_if_nonempty(&status_msg, &action_log, paste_clipboard_into_selected_field_with_message(selected_field, &controller.state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty))
|
||||
}
|
||||
if ctrl_down && rl.IsKeyPressed(.BACKSPACE) {
|
||||
push_status(&status_msg, &action_log, clear_selected_field_with_message(selected_field, &controller.state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.I) {
|
||||
field_text := selected_field_value(selected_field, controller.state, export_path, local_script_pages, project_path, autosave_interval_text)
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(field_text, "Copied selected field to clipboard"))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.C) {
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(status_msg, "Copied status to clipboard"))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.Y) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, copy_diagnostics_with_message(diag_ctx))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.T) {
|
||||
push_status(&status_msg, &action_log, toggle_log_lines_with_message(&log_show_lines))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.B) {
|
||||
push_status(&status_msg, &action_log, toggle_log_order_with_message(&log_oldest_first))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.R) {
|
||||
diag_ctx := make_diagnostics_action_context(&controller, &action_log, is_dirty, autosave_enabled, project_path_ok, export_path_ok, autosave_secs, project_path, export_path, log_show_lines, log_oldest_first)
|
||||
push_status(&status_msg, &action_log, write_diagnostics_with_message(diag_ctx))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.X) {
|
||||
push_status(&status_msg, &action_log, copy_text_with_status(export_path, "Copied export path to clipboard"))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.P) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_export_preset_with_message(&export_path, export_format))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.D) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_export_path_from_project_with_message(&export_path, project_path, export_format))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.G) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, set_project_path_from_export_with_message(&project_path, export_path))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.J) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_project_path_with_message(&project_path))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.K) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_project_path_with_message(&project_path))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.M) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_export_path_with_message(&export_path, export_format))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.U) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, fix_all_paths_with_message(&project_path, &export_path, export_format))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.F) {
|
||||
push_dirty_status(&is_dirty, &status_msg, &action_log, normalize_export_path_with_message(&export_path, export_format))
|
||||
}
|
||||
if ctrl_down && shift_down && rl.IsKeyPressed(.A) {
|
||||
push_status(&status_msg, &action_log, toggle_autosave_with_message(&autosave_enabled))
|
||||
}
|
||||
|
||||
if !interaction_locked {
|
||||
for {
|
||||
ch := rl.GetCharPressed()
|
||||
if ch == 0 { break }
|
||||
if ch < 32 || ch > 126 { continue }
|
||||
if selected_field == 6 && (ch < '0' || ch > '9') {
|
||||
continue
|
||||
}
|
||||
switch selected_field {
|
||||
case 0: append_char(&controller.state.story_idea, ch)
|
||||
case 1: append_char(&controller.state.story_genre, ch)
|
||||
case 2: append_char(&controller.state.target_audience, ch)
|
||||
case 3: append_char(&export_path, ch)
|
||||
case 4: append_char(&local_script_pages, ch)
|
||||
case 5: append_char(&project_path, ch)
|
||||
case 6: append_char(&autosave_interval_text, ch)
|
||||
}
|
||||
is_dirty = true
|
||||
}
|
||||
if rl.IsKeyPressed(.BACKSPACE) {
|
||||
switch selected_field {
|
||||
case 0: pop_char(&controller.state.story_idea)
|
||||
case 1: pop_char(&controller.state.story_genre)
|
||||
case 2: pop_char(&controller.state.target_audience)
|
||||
case 3: pop_char(&export_path)
|
||||
case 4: pop_char(&local_script_pages)
|
||||
case 5: pop_char(&project_path)
|
||||
case 6: pop_char(&autosave_interval_text)
|
||||
}
|
||||
is_dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
push_status_if_nonempty(&status_msg, &action_log, autosave_tick_with_message(&project_path, controller.state, autosave_enabled, &is_dirty, &last_autosave_at, &last_save_at, autosave_interval_s))
|
||||
|
||||
rl.BeginDrawing()
|
||||
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
|
||||
}
|
||||
|
||||
rl.DrawRectangle(0, 0, 260, screen_h, BG_SIDEBAR)
|
||||
rl.DrawRectangle(260, 0, screen_w-260, 72, BG_TOPBAR)
|
||||
rl.DrawLine(260, 72, screen_w, 72, BORDER_DIVIDER)
|
||||
rl.DrawLine(260, 0, 260, screen_h, BORDER_DIVIDER)
|
||||
|
||||
draw_card(rl.Rectangle{x = 14, y = 10, width = 236, height = 64})
|
||||
rl.DrawText("comic-odin", 24, 22, 23, BRAND_TITLE)
|
||||
rl.DrawText("Native GUI", 24, 46, 14, BRAND_SUBTITLE)
|
||||
draw_section_title(24, 68, "Screens")
|
||||
draw_card(rl.Rectangle{x = 14, y = 86, width = 236, height = 296})
|
||||
draw_nav_item(nav_story, "1 Story", controller.active_screen == .Story)
|
||||
draw_nav_item(nav_script, "2 Script", controller.active_screen == .Script)
|
||||
draw_nav_item(nav_chars, "3 Characters", controller.active_screen == .Characters)
|
||||
draw_nav_item(nav_panels, "4 Panels", controller.active_screen == .Panels)
|
||||
draw_nav_item(nav_layout, "5 Layout", controller.active_screen == .Layout)
|
||||
draw_nav_item(nav_bubbles, "6 Bubbles", controller.active_screen == .Bubbles)
|
||||
draw_nav_item(nav_export, "7 Export", controller.active_screen == .Export)
|
||||
draw_nav_item(nav_community, "8 Community", controller.active_screen == .Community)
|
||||
draw_sidebar_shortcuts(screen_h_loop)
|
||||
|
||||
// --- Pipeline Stepper in Topbar ---
|
||||
draw_card(rl.Rectangle{x = 282, 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
|
||||
panels_ok := len(controller.state.panel_images) > 0
|
||||
layout_ok := len(controller.state.page_layouts) > 0
|
||||
export_ok := panels_ok && layout_ok
|
||||
|
||||
step_x := i32(460)
|
||||
step_y := i32(36)
|
||||
step_spacing := i32(140)
|
||||
|
||||
// Draw connecting lines first
|
||||
for i in 0..<3 {
|
||||
x1 := step_x + i32(i)*step_spacing + 24
|
||||
x2 := step_x + i32(i+1)*step_spacing - 24
|
||||
c := STEP_LINE_TODO
|
||||
if (i == 0 && panels_ok) || (i == 1 && layout_ok) || (i == 2 && export_ok) {
|
||||
c = STEP_LINE_DONE
|
||||
}
|
||||
rl.DrawLineEx(rl.Vector2{f32(x1), f32(step_y)}, rl.Vector2{f32(x2), f32(step_y)}, 2.0, c)
|
||||
}
|
||||
|
||||
// Helper to draw a single pipeline step
|
||||
draw_step := proc(x, y: i32, label: string, done: bool) {
|
||||
fill := STEP_TODO_FILL
|
||||
border := STEP_TODO_BORDER
|
||||
text_col := STEP_LABEL_TODO
|
||||
if done {
|
||||
fill = STEP_DONE_FILL
|
||||
border = STEP_DONE_BORDER
|
||||
text_col = STEP_LABEL_DONE
|
||||
}
|
||||
rl.DrawCircle(x, y, 10, fill)
|
||||
rl.DrawCircleLines(x, y, 10, border)
|
||||
// Draw checkmark if done
|
||||
if done {
|
||||
rl.DrawLineEx(rl.Vector2{f32(x-3), f32(y+1)}, rl.Vector2{f32(x-1), f32(y+4)}, 2.0, BG_BASE)
|
||||
rl.DrawLineEx(rl.Vector2{f32(x-1), f32(y+4)}, rl.Vector2{f32(x+4), f32(y-3)}, 2.0, BG_BASE)
|
||||
}
|
||||
draw_text_fitted(label, x - 24, y + 16, 14, 48, 7, text_col)
|
||||
}
|
||||
|
||||
draw_step(step_x, step_y, "Script", script_ok)
|
||||
draw_step(step_x + step_spacing, step_y, "Panels", panels_ok)
|
||||
draw_step(step_x + step_spacing*2, step_y, "Layout", layout_ok)
|
||||
draw_step(step_x + step_spacing*3, step_y, "Export", export_ok)
|
||||
|
||||
// Pages input on far right of topbar
|
||||
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_section_title(300, 92, "Project Setup")
|
||||
draw_section_title(300, 470, "Actions")
|
||||
|
||||
rl.DrawText("Story Idea", 290, 96, 16, TEXT_SECONDARY)
|
||||
draw_input_field(idea_rec, controller.state.story_idea, selected_field == 0)
|
||||
|
||||
rl.DrawText("Genre", 290, 152, 16, TEXT_SECONDARY)
|
||||
draw_input_field(genre_rec, controller.state.story_genre, selected_field == 1)
|
||||
|
||||
rl.DrawText("Audience", 290, 208, 16, TEXT_SECONDARY)
|
||||
draw_input_field(audience_rec, controller.state.target_audience, selected_field == 2)
|
||||
|
||||
rl.DrawText("Export Path", 290, 264, 16, TEXT_SECONDARY)
|
||||
draw_input_field(export_rec, export_path, selected_field == 3)
|
||||
|
||||
rl.DrawText("Project Path", 290, 320, 16, TEXT_SECONDARY)
|
||||
draw_input_field(project_rec, project_path, selected_field == 5)
|
||||
|
||||
// Compact utility row below inputs
|
||||
draw_small_button(export_copy_btn, "Copy Export")
|
||||
draw_small_button(export_preset_btn, "Preset Ext")
|
||||
draw_small_button(path_fix_btn, "Fix Exp Ext")
|
||||
draw_small_button(project_fix_btn, "Fix Proj Ext")
|
||||
draw_small_button(project_from_export_btn, "Proj From Exp")
|
||||
draw_small_button(export_project_btn, "Exp From Proj")
|
||||
|
||||
rl.DrawText("Format", 290, 404, 16, TEXT_SECONDARY)
|
||||
draw_nav_item(fmt_pdf_btn, "PDF", export_format == .PDF)
|
||||
draw_nav_item(fmt_png_btn, "PNG", export_format == .PNG)
|
||||
draw_nav_item(fmt_cbz_btn, "CBZ", export_format == .CBZ)
|
||||
|
||||
rl.DrawText("Script Source", 700, 404, 16, TEXT_SECONDARY)
|
||||
draw_nav_item(script_src_local_btn, "Local", !use_deepseek_script)
|
||||
draw_nav_item(script_src_deepseek_btn, "DeepSeek", use_deepseek_script)
|
||||
if !has_deepseek_key {
|
||||
draw_summary_subline(1024, 408, "set DEEPSEEK_API_KEY", KEY_MISSING_COLOR)
|
||||
}
|
||||
|
||||
next_hint := gui_next_hint_with_source(controller, use_deepseek_script)
|
||||
recommended_label := recommended_label_from_hint(next_hint)
|
||||
script_btn_label := "Generate Script Local"
|
||||
if use_deepseek_script {
|
||||
script_btn_label = "Generate Script"
|
||||
}
|
||||
|
||||
draw_button_warning(new_btn, "New Project")
|
||||
if recommended_label == "Generate Script" || recommended_label == "Generate Script Local" {
|
||||
draw_button_recommended(script_btn, script_btn_label)
|
||||
} else {
|
||||
draw_button(script_btn, script_btn_label)
|
||||
}
|
||||
if recommended_label == "Generate Panels Local" {
|
||||
draw_button_recommended(panels_btn, "Generate Panels Local")
|
||||
} else {
|
||||
draw_button_state(panels_btn, "Generate Panels Local", can_generate_panels)
|
||||
}
|
||||
if recommended_label == "Layout" {
|
||||
draw_button_recommended(layout_btn, "Layout Pages")
|
||||
} else {
|
||||
draw_button_state(layout_btn, "Layout Pages", can_layout)
|
||||
}
|
||||
if recommended_label == "Export" {
|
||||
draw_button_recommended(export_btn, "Export")
|
||||
} else {
|
||||
draw_button_state(export_btn, "Export", can_export)
|
||||
}
|
||||
|
||||
draw_button_soft_accent(save_btn, "Save")
|
||||
draw_button_soft_accent(open_btn, "Open")
|
||||
draw_button_primary(next_btn, "Next Step")
|
||||
draw_button_primary(auto_btn, "Auto-All")
|
||||
draw_button_primary(auto_save_btn, "Auto-All + Save")
|
||||
|
||||
draw_button_soft_accent(autosave_btn, autosave_enabled ? "Autosave: yes" : "Autosave: no")
|
||||
rl.DrawText("Interval(s)", 500, 574, 16, TEXT_SECONDARY)
|
||||
draw_input_field(autosave_rec, autosave_interval_text, selected_field == 6)
|
||||
draw_small_button(autosave_15_btn, "15")
|
||||
draw_small_button(autosave_30_btn, "30")
|
||||
draw_small_button(autosave_60_btn, "60")
|
||||
draw_button_soft_accent(help_btn, "Help (/)")
|
||||
draw_button(clear_field_btn, "Clear Field")
|
||||
draw_button_soft_accent(reset_helpers_btn, "Reset Helpers")
|
||||
|
||||
label := "idea"
|
||||
if selected_field == 1 { label = "genre" }
|
||||
if selected_field == 2 { label = "audience" }
|
||||
if selected_field == 3 { label = "export path" }
|
||||
if selected_field == 4 { label = "local pages" }
|
||||
if selected_field == 5 { label = "project path" }
|
||||
if selected_field == 6 { label = "autosave interval" }
|
||||
if !compact_mode {
|
||||
hint_msg := button_readiness_hint(rl.GetMousePosition(), panels_btn, layout_btn, export_btn, can_generate_panels, can_layout, can_export)
|
||||
if len(hint_msg) > 0 {
|
||||
draw_hint_pill(rl.Rectangle{x = 1000, y = 580, width = 210, height = 22}, fmt.tprintf("%s", hint_msg), true)
|
||||
}
|
||||
}
|
||||
|
||||
draw_card(rl.Rectangle{x = 282, 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)
|
||||
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)
|
||||
progress := f32(0)
|
||||
if total_count > 0 {
|
||||
progress = f32(ready_count) / f32(total_count)
|
||||
}
|
||||
draw_progress_bar(300, status_y+84, status_w_loop-26, progress)
|
||||
draw_text_fitted(fmt.tprintf("Pipeline: %d/%d", ready_count, total_count), 300, status_y+102, 14, 122, 7, TEXT_TERTIARY)
|
||||
draw_text_fitted(fmt.tprintf("Next: %s", gui_next_hint_with_source(controller, use_deepseek_script)), 430, status_y+102, 14, int(status_w_loop-166), 7, TEXT_SECONDARY)
|
||||
draw_status_badge(rl.Rectangle{x = 300, y = f32(status_y+120), width = 118, height = 22}, fmt.tprintf("Dirty: %s", yn(is_dirty)), !is_dirty)
|
||||
draw_status_badge(rl.Rectangle{x = 430, y = f32(status_y+120), width = 188, height = 22}, fmt.tprintf("Autosave: %s (%ds)", yn(autosave_enabled), autosave_secs), autosave_enabled)
|
||||
save_meta := "save: never"
|
||||
if last_save_at >= 0 {
|
||||
save_meta = fmt.tprintf("save: %.0fs", now_draw-last_save_at)
|
||||
}
|
||||
export_meta := "export: never"
|
||||
if last_export_at >= 0 {
|
||||
export_meta = fmt.tprintf("export: %.0fs", now_draw-last_export_at)
|
||||
}
|
||||
draw_text_fitted(fmt.tprintf("%s | %s", save_meta, export_meta), 630, status_y+124, 13, int(status_w_loop-26), 7, TEXT_TERTIARY)
|
||||
path_fix_project_status_btn.y = f32(status_y + 114)
|
||||
path_fix_export_status_btn.y = f32(status_y + 114)
|
||||
path_fix_all_status_btn.y = f32(status_y + 114)
|
||||
draw_small_button_state(path_fix_project_status_btn, "P", !project_path_ok)
|
||||
draw_small_button_state(path_fix_export_status_btn, "E", !export_path_ok)
|
||||
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 {
|
||||
show_txt := "Top"
|
||||
sort_txt := "Asc"
|
||||
if controller.active_screen == .Script {
|
||||
if summary_opts.script_show_all { show_txt = "All" }
|
||||
if summary_opts.script_desc { sort_txt = "Desc" }
|
||||
} else {
|
||||
if summary_opts.layout_show_all { show_txt = "All" }
|
||||
if summary_opts.layout_desc { sort_txt = "Desc" }
|
||||
}
|
||||
show_btn_label := "Show:Top"
|
||||
if show_txt == "All" {
|
||||
show_btn_label = "Show:All"
|
||||
}
|
||||
sort_btn_label := "Sort:Asc"
|
||||
if sort_txt == "Desc" {
|
||||
sort_btn_label = "Sort:Desc"
|
||||
}
|
||||
draw_small_button(summary_show_btn, show_btn_label)
|
||||
draw_small_button(summary_sort_btn, sort_btn_label)
|
||||
}
|
||||
if controller.active_screen == .Script {
|
||||
draw_small_button(summary_prev_btn, "< Pg")
|
||||
draw_small_button(summary_next_btn, "Pg >")
|
||||
} else if controller.active_screen == .Panels {
|
||||
draw_small_button(summary_prev_btn, "< Pn")
|
||||
draw_small_button(summary_next_btn, "Pn >")
|
||||
}
|
||||
if !compact_mode {
|
||||
hint_label := "Ctrl+[ / Ctrl+]"
|
||||
if controller.active_screen == .Script || controller.active_screen == .Layout {
|
||||
hint_label = "Ctrl+H / Ctrl+J"
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
if !compact_mode {
|
||||
draw_hint_pill(rl.Rectangle{x = 300, y = f32(lower_y_loop + 206), width = f32(status_w_loop-18), height = 24}, "Tip: New/Open show confirm modal when dirty (Shift still quick-confirms)", true)
|
||||
}
|
||||
|
||||
if controller.active_screen == .Script {
|
||||
draw_script_detail_panel(controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200, summary_opts.script_page_cursor)
|
||||
draw_small_button(script_copy_page_btn, "Copy Page")
|
||||
draw_small_button(script_copy_all_btn, "Copy All")
|
||||
draw_text_fitted("Ctrl+[ / Ctrl+] page nav", log_x_loop+216, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-228), 7, TEXT_TERTIARY)
|
||||
} else if controller.active_screen == .Panels {
|
||||
retry, new_cursor := draw_panels_detail_panel(controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200, summary_opts.panel_cursor)
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if summary_opts.panel_cursor != new_cursor {
|
||||
summary_opts.panel_cursor = new_cursor
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
|
||||
}
|
||||
if retry {
|
||||
panel, _, ok := panel_by_flat_index(controller.state.script, summary_opts.panel_cursor)
|
||||
if ok {
|
||||
msg := action_regenerate_panel(&controller, panel.panel_id)
|
||||
push_status(&status_msg, &action_log, msg)
|
||||
}
|
||||
}
|
||||
|
||||
wheel := rl.GetMouseWheelMove()
|
||||
if wheel != 0 && panel_count > 0 {
|
||||
summary_opts.panel_cursor -= int(wheel)
|
||||
if summary_opts.panel_cursor < 0 {
|
||||
summary_opts.panel_cursor = 0
|
||||
}
|
||||
if summary_opts.panel_cursor >= panel_count {
|
||||
summary_opts.panel_cursor = panel_count - 1
|
||||
}
|
||||
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
|
||||
}
|
||||
|
||||
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")
|
||||
draw_subtle_strip(rl.Rectangle{x = f32(log_x_loop+12), y = f32(lower_y_loop), width = f32(main_w_loop-status_w_loop-26), height = 40})
|
||||
draw_small_button(log_reset_btn, "Default")
|
||||
draw_small_button(report_file_btn, "Report")
|
||||
draw_small_button(log_copy_btn, "LogCopy")
|
||||
draw_small_button(diag_file_btn, "DiagFile")
|
||||
draw_small_button(status_copy_btn, "Copy")
|
||||
draw_small_button(log_clear_btn, "Clear")
|
||||
draw_small_button(diag_copy_btn, "Diag")
|
||||
order_label := "newest"
|
||||
if log_oldest_first {
|
||||
order_label = "oldest"
|
||||
}
|
||||
draw_text_fitted(fmt.tprintf("View: %d lines, %s first", log_show_lines, order_label), log_x_loop+18, lower_y_loop+36, 13, 216, 7, TEXT_TERTIARY)
|
||||
draw_action_log(action_log, log_x_loop+18, lower_y_loop+52, log_show_lines, log_oldest_first)
|
||||
}
|
||||
draw_toast(action_log, log_x_loop+8, 70, main_w_loop-status_w_loop-18)
|
||||
if show_help_overlay {
|
||||
draw_help_overlay()
|
||||
}
|
||||
if show_confirm_overlay {
|
||||
draw_confirm_overlay(pending_confirm_action)
|
||||
draw_button_danger(confirm_yes_btn, "Confirm")
|
||||
draw_button_soft_accent(confirm_no_btn, "Cancel")
|
||||
}
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
core.dispose_state(state)
|
||||
state^ = controller.state
|
||||
controller.state = core.Comic_State{}
|
||||
return shared.ok()
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
import "../core"
|
||||
import "../shared"
|
||||
import "../ui"
|
||||
|
||||
set_status :: proc(status: ^string, msg: string) {
|
||||
delete(status^)
|
||||
status^ = fmt.aprintf("%s", msg)
|
||||
}
|
||||
|
||||
push_status :: proc(status: ^string, log: ^Action_Log, msg: string) {
|
||||
set_status(status, msg)
|
||||
action_log_push(log, status^)
|
||||
}
|
||||
|
||||
push_status_if_nonempty :: proc(status: ^string, log: ^Action_Log, msg: string) {
|
||||
if len(msg) > 0 {
|
||||
push_status(status, log, msg)
|
||||
}
|
||||
}
|
||||
|
||||
push_dirty_status :: proc(is_dirty: ^bool, status: ^string, log: ^Action_Log, msg: string) {
|
||||
is_dirty^ = true
|
||||
push_status(status, log, msg)
|
||||
}
|
||||
|
||||
screen_status_label :: proc(screen: ui.App_Screen) -> string {
|
||||
switch screen {
|
||||
case .Story: return "Story"
|
||||
case .Script: return "Script"
|
||||
case .Characters: return "Characters"
|
||||
case .Panels: return "Panels"
|
||||
case .Layout: return "Layout"
|
||||
case .Bubbles: return "Bubbles"
|
||||
case .Export: return "Export"
|
||||
case .Community: return "Community"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
navigate_screen_with_status :: proc(controller: ^ui.App_Controller, screen: ui.App_Screen) -> string {
|
||||
err := ui.navigate_to_screen(controller, screen)
|
||||
if !shared.is_ok(err) {
|
||||
return err.message
|
||||
}
|
||||
return fmt.aprintf("Screen: %s", screen_status_label(screen))
|
||||
}
|
||||
|
||||
request_confirmation :: proc(show_confirm_overlay, show_help_overlay: ^bool, pending_confirm_action: ^Pending_Confirm_Action, action: Pending_Confirm_Action, prompt: string) -> string {
|
||||
show_confirm_overlay^ = true
|
||||
show_help_overlay^ = false
|
||||
pending_confirm_action^ = action
|
||||
return prompt
|
||||
}
|
||||
|
||||
toggle_autosave_with_message :: proc(autosave_enabled: ^bool) -> string {
|
||||
autosave_enabled^ = !autosave_enabled^
|
||||
return fmt.aprintf("Autosave: %s", yn(autosave_enabled^))
|
||||
}
|
||||
|
||||
reset_helper_fields_with_message :: proc(export_path, local_script_pages, autosave_interval_text: ^string, f: core.Export_Format) -> string {
|
||||
reset_helper_fields(export_path, local_script_pages, autosave_interval_text, f)
|
||||
return "Reset helper fields to defaults"
|
||||
}
|
||||
|
||||
toggle_help_overlay :: proc(show_help_overlay: ^bool) {
|
||||
show_help_overlay^ = !show_help_overlay^
|
||||
}
|
||||
|
||||
close_help_overlay_if_open :: proc(show_help_overlay: ^bool) {
|
||||
if show_help_overlay^ {
|
||||
show_help_overlay^ = false
|
||||
}
|
||||
}
|
||||
|
||||
clear_action_log_with_message :: proc(log: ^Action_Log) -> string {
|
||||
action_log_dispose(log)
|
||||
return "Action log cleared"
|
||||
}
|
||||
|
||||
reset_log_view :: proc(log_show_lines: ^i32, log_oldest_first: ^bool) {
|
||||
log_show_lines^ = 6
|
||||
log_oldest_first^ = false
|
||||
}
|
||||
|
||||
reset_log_view_with_message :: proc(log_show_lines: ^i32, log_oldest_first: ^bool) -> string {
|
||||
reset_log_view(log_show_lines, log_oldest_first)
|
||||
return "Reset log view"
|
||||
}
|
||||
|
||||
toggle_log_lines_with_message :: proc(log_show_lines: ^i32) -> string {
|
||||
if log_show_lines^ == 6 {
|
||||
log_show_lines^ = 4
|
||||
} else {
|
||||
log_show_lines^ = 6
|
||||
}
|
||||
return fmt.aprintf("Log lines: %d", log_show_lines^)
|
||||
}
|
||||
|
||||
toggle_log_order_with_message :: proc(log_oldest_first: ^bool) -> string {
|
||||
log_oldest_first^ = !log_oldest_first^
|
||||
order := "newest"
|
||||
if log_oldest_first^ {
|
||||
order = "oldest"
|
||||
}
|
||||
return fmt.aprintf("Log order: %s first", order)
|
||||
}
|
||||
|
||||
selected_field_value :: proc(selected_field: int, state: core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: string) -> string {
|
||||
switch selected_field {
|
||||
case 0: return state.story_idea
|
||||
case 1: return state.story_genre
|
||||
case 2: return state.target_audience
|
||||
case 3: return export_path
|
||||
case 4: return local_script_pages
|
||||
case 5: return project_path
|
||||
case 6: return autosave_interval_text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
clear_selected_field :: proc(selected_field: int, state: ^core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: ^string) -> bool {
|
||||
switch selected_field {
|
||||
case 0:
|
||||
if len(state.story_idea) == 0 { return false }
|
||||
state.story_idea = ""
|
||||
case 1:
|
||||
if len(state.story_genre) == 0 { return false }
|
||||
state.story_genre = ""
|
||||
case 2:
|
||||
if len(state.target_audience) == 0 { return false }
|
||||
state.target_audience = ""
|
||||
case 3:
|
||||
if len(export_path^) == 0 { return false }
|
||||
export_path^ = ""
|
||||
case 4:
|
||||
if len(local_script_pages^) == 0 { return false }
|
||||
local_script_pages^ = ""
|
||||
case 5:
|
||||
if len(project_path^) == 0 { return false }
|
||||
project_path^ = ""
|
||||
case 6:
|
||||
if len(autosave_interval_text^) == 0 { return false }
|
||||
autosave_interval_text^ = ""
|
||||
case:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
clear_selected_field_with_message :: proc(selected_field: int, state: ^core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: ^string, is_dirty: ^bool) -> string {
|
||||
if clear_selected_field(selected_field, state, export_path, local_script_pages, project_path, autosave_interval_text) {
|
||||
is_dirty^ = true
|
||||
return "Cleared selected field"
|
||||
}
|
||||
return "Selected field already empty"
|
||||
}
|
||||
|
||||
paste_clipboard_into_selected_field_with_message :: proc(selected_field: int, state: ^core.Comic_State, export_path, local_script_pages, project_path, autosave_interval_text: ^string, is_dirty: ^bool) -> string {
|
||||
clip_raw := rl.GetClipboardText()
|
||||
if clip_raw == nil {
|
||||
return ""
|
||||
}
|
||||
clip_text := fmt.aprintf("%s", clip_raw)
|
||||
switch selected_field {
|
||||
case 0: state.story_idea = clip_text
|
||||
case 1: state.story_genre = clip_text
|
||||
case 2: state.target_audience = clip_text
|
||||
case 3: export_path^ = clip_text
|
||||
case 4: local_script_pages^ = clip_text
|
||||
case 5: project_path^ = clip_text
|
||||
case 6: autosave_interval_text^ = clip_text
|
||||
}
|
||||
is_dirty^ = true
|
||||
return "Pasted clipboard into selected field"
|
||||
}
|
||||
|
||||
Action_Log :: struct {
|
||||
entries: [8]string,
|
||||
owned: [8]bool,
|
||||
entry_times: [8]f64,
|
||||
count: int,
|
||||
last_push_at: f64,
|
||||
}
|
||||
|
||||
action_log_push :: proc(log: ^Action_Log, msg: string) {
|
||||
now := rl.GetTime()
|
||||
idx := log.count % len(log.entries)
|
||||
if log.owned[idx] {
|
||||
delete(log.entries[idx])
|
||||
}
|
||||
log.entries[idx] = fmt.aprintf("%s", msg)
|
||||
log.owned[idx] = true
|
||||
log.entry_times[idx] = now
|
||||
log.count += 1
|
||||
log.last_push_at = now
|
||||
}
|
||||
|
||||
action_log_dispose :: proc(log: ^Action_Log) {
|
||||
for i in 0..<len(log.entries) {
|
||||
if log.owned[i] {
|
||||
delete(log.entries[i])
|
||||
log.owned[i] = false
|
||||
}
|
||||
log.entry_times[i] = 0
|
||||
}
|
||||
log.count = 0
|
||||
log.last_push_at = 0
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
import "../core"
|
||||
import "../ui"
|
||||
|
||||
draw_stat_chip :: proc(x, y: i32, label: string, value: int) {
|
||||
rec := rl.Rectangle{x = f32(x), y = f32(y), width = 80, height = 28}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, CHIP_BG)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, CHIP_BORDER)
|
||||
draw_text_fitted(label, x+10, y+8, 12, 40, 6, CHIP_TEXT)
|
||||
draw_text_fitted(fmt.tprintf("%d", value), x+56, y+6, 14, 20, 7, TEXT_PRIMARY)
|
||||
}
|
||||
|
||||
draw_readiness_chip :: proc(x, y, w: i32, label: string, ok: bool) {
|
||||
bg := UNREADY_BG
|
||||
border := UNREADY_BORDER
|
||||
fg := UNREADY_TEXT
|
||||
if ok {
|
||||
bg = READY_BG
|
||||
border = READY_BORDER
|
||||
fg = READY_TEXT
|
||||
}
|
||||
rec := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 26}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, border)
|
||||
prefix := "○"
|
||||
if ok {
|
||||
prefix = "●"
|
||||
}
|
||||
draw_text_fitted(fmt.tprintf("%s %s", prefix, label), x+10, y+7, 12, int(w-20), 6, fg)
|
||||
}
|
||||
|
||||
ready_stage_count :: proc(controller: ui.App_Controller) -> (ready: int, total: int) {
|
||||
script_ok := len(controller.state.script.pages) > 0
|
||||
panels_ok := len(controller.state.panel_images) > 0
|
||||
layout_ok := len(controller.state.page_layouts) > 0
|
||||
export_ok := panels_ok && layout_ok
|
||||
ready = 0
|
||||
if script_ok { ready += 1 }
|
||||
if panels_ok { ready += 1 }
|
||||
if layout_ok { ready += 1 }
|
||||
if export_ok { ready += 1 }
|
||||
return ready, 4
|
||||
}
|
||||
|
||||
draw_progress_bar :: proc(x, y, w: i32, progress: f32) {
|
||||
track := rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = 12}
|
||||
rl.DrawRectangleRounded(track, RADIUS_BAR, 8, PROGRESS_TRACK)
|
||||
fill_w := i32(f32(w) * progress)
|
||||
if fill_w > 0 {
|
||||
fill := rl.Rectangle{x = f32(x), y = f32(y), width = f32(fill_w), height = 12}
|
||||
rl.DrawRectangleRounded(fill, RADIUS_BAR, 8, PROGRESS_FILL)
|
||||
}
|
||||
}
|
||||
|
||||
draw_readiness_row :: proc(controller: ui.App_Controller, x, y: i32) {
|
||||
script_ok := len(controller.state.script.pages) > 0
|
||||
panels_ok := len(controller.state.panel_images) > 0
|
||||
layout_ok := len(controller.state.page_layouts) > 0
|
||||
export_ok := panels_ok && layout_ok
|
||||
draw_readiness_chip(x, y, 120, "Script", script_ok)
|
||||
draw_readiness_chip(x+126, y, 120, "Panels", panels_ok)
|
||||
draw_readiness_chip(x+252, y, 120, "Layout", layout_ok)
|
||||
draw_readiness_chip(x+378, y, 120, "Export", export_ok)
|
||||
}
|
||||
|
||||
export_block_reason :: proc(state: core.Comic_State) -> string {
|
||||
if len(state.panel_images) == 0 && len(state.page_layouts) == 0 {
|
||||
return "need panels + layout"
|
||||
}
|
||||
if len(state.panel_images) == 0 {
|
||||
return "need panels"
|
||||
}
|
||||
if len(state.page_layouts) == 0 {
|
||||
return "need layout"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
draw_screen_summary :: proc(controller: ui.App_Controller, export_path: string, x, y, w: i32, opts: Summary_View_Options) {
|
||||
draw_card(rl.Rectangle{x = f32(x-18), y = f32(y-12), width = f32(w), height = 200})
|
||||
rl.DrawText("Screen Summary", x, y, 22, SUMMARY_TITLE)
|
||||
chip_base := x + w - 258
|
||||
if chip_base < x+210 {
|
||||
chip_base = x + 210
|
||||
}
|
||||
draw_stat_chip(chip_base, y-4, "Pages", len(controller.state.script.pages))
|
||||
draw_stat_chip(chip_base+86, y-4, "Panels", len(controller.state.panel_images))
|
||||
draw_stat_chip(chip_base+172, y-4, "Layout", len(controller.state.page_layouts))
|
||||
|
||||
switch controller.active_screen {
|
||||
case .Story:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Idea length: %d chars", len(controller.state.story_idea)), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Genre: %s", controller.state.story_genre), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+78, fmt.tprintf("Audience: %s", controller.state.target_audience), rl.DARKGRAY)
|
||||
rl.DrawText("Use Generate Script Local to begin", x, y+112, 18, SUMMARY_HINT)
|
||||
case .Script:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Title: %s", controller.state.script.title), rl.DARKGRAY)
|
||||
page_count := len(controller.state.script.pages)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Pages: %d | Characters: %d", page_count, len(controller.state.script.characters)), rl.DARKGRAY)
|
||||
if page_count == 0 {
|
||||
rl.DrawText("No script pages yet. Generate Script Local to continue.", x, y+86, 18, SUMMARY_HINT)
|
||||
} else {
|
||||
cursor := opts.script_page_cursor
|
||||
if cursor < 0 {
|
||||
cursor = 0
|
||||
}
|
||||
if cursor >= page_count {
|
||||
cursor = page_count - 1
|
||||
}
|
||||
page := controller.state.script.pages[cursor]
|
||||
draw_summary_line(x, y+78, fmt.tprintf("Viewing page %d/%d (script page #%d)", cursor+1, page_count, page.page_number), SUMMARY_ACCENT)
|
||||
draw_summary_line(x, y+100, fmt.tprintf("Panels on page: %d", len(page.panels)), rl.DARKGRAY)
|
||||
line_y := y + 124
|
||||
show_panels := len(page.panels)
|
||||
if show_panels > 2 {
|
||||
show_panels = 2
|
||||
}
|
||||
for i in 0..<show_panels {
|
||||
pn := page.panels[i]
|
||||
desc := pn.description
|
||||
if len(desc) == 0 {
|
||||
desc = "(no description)"
|
||||
}
|
||||
draw_summary_subline(x, line_y+i32(i*28), fit_text_for_width(fmt.tprintf("• P%d: %s", pn.panel_number, desc), int(w-36), 7), SUMMARY_SUBLINE)
|
||||
if len(pn.dialogue) > 0 {
|
||||
draw_summary_subline(x+12, line_y+i32(i*28)+14, fit_text_for_width(fmt.tprintf("\"%s\"", pn.dialogue[0].text), int(w-54), 7), SUMMARY_DIM)
|
||||
}
|
||||
}
|
||||
if len(page.panels) > show_panels {
|
||||
draw_summary_subline(x+w-158, y+166, fmt.tprintf("+%d more panels", len(page.panels)-show_panels), SUMMARY_ACCENT)
|
||||
}
|
||||
}
|
||||
case .Characters:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Character count: %d", len(controller.state.characters)), rl.DARKGRAY)
|
||||
rl.DrawText("Character editor is scaffolded", x, y+54, 18, rl.DARKGRAY)
|
||||
rl.DrawText("Use script generation to populate", x, y+78, 18, rl.DARKGRAY)
|
||||
case .Panels:
|
||||
script_panel_count := count_script_panels(controller.state.script)
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Panel images: %d", len(controller.state.panel_images)), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Script panels: %d", script_panel_count), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+78, fmt.tprintf("Script pages: %d", len(controller.state.script.pages)), rl.DARKGRAY)
|
||||
if script_panel_count == 0 {
|
||||
rl.DrawText("No script panels yet. Generate Script first.", x, y+112, 18, SUMMARY_HINT)
|
||||
} else {
|
||||
pidx := clamp_panel_cursor(script_panel_count, opts.panel_cursor)
|
||||
panel, page_num, _ := panel_by_flat_index(controller.state.script, pidx)
|
||||
status := "missing"
|
||||
if _, has_img := controller.state.panel_images[panel.panel_id]; has_img {
|
||||
status = "ready"
|
||||
}
|
||||
draw_summary_line(x, y+102, fmt.tprintf("Viewing panel %d/%d • page %d # %d", pidx+1, script_panel_count, page_num, panel.panel_number), SUMMARY_ACCENT)
|
||||
draw_summary_subline(x, y+124, fmt.tprintf("%s • %s", panel.panel_id, status), SUMMARY_SUBLINE)
|
||||
}
|
||||
case .Layout:
|
||||
draw_summary_line(x, y+30, fmt.tprintf("Layout pages: %d", len(controller.state.page_layouts)), rl.DARKGRAY)
|
||||
draw_summary_line(x, y+54, fmt.tprintf("Page size: %v", controller.state.page_size), rl.DARKGRAY)
|
||||
layout_show := len(controller.state.page_layouts)
|
||||
if !opts.layout_show_all && layout_show > 3 { layout_show = 3 }
|
||||
if layout_show == 0 {
|
||||
rl.DrawText("No layouts yet. Use Layout Auto after panels are ready.", x, y+86, 18, SUMMARY_HINT)
|
||||
} else {
|
||||
if opts.layout_desc {
|
||||
for i in 0..<layout_show {
|
||||
idx := len(controller.state.page_layouts)-1-i
|
||||
l := controller.state.page_layouts[idx]
|
||||
draw_summary_line(x, y+78+i32(i*22), fmt.tprintf("- P%d: %s (%d)", l.page_number, l.pattern_id, len(l.panels)), rl.DARKGRAY)
|
||||
}
|
||||
} else {
|
||||
for i in 0..<layout_show {
|
||||
l := controller.state.page_layouts[i]
|
||||
draw_summary_line(x, y+78+i32(i*22), fmt.tprintf("- P%d: %s (%d)", l.page_number, l.pattern_id, len(l.panels)), rl.DARKGRAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
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)
|
||||
draw_summary_line(x, y+78, fmt.tprintf("Target: %s", export_path), rl.DARKGRAY)
|
||||
if len(controller.state.page_layouts) > 0 {
|
||||
last := controller.state.page_layouts[len(controller.state.page_layouts)-1]
|
||||
draw_summary_subline(x, y+102, fmt.tprintf("Last layout pattern: %s", last.pattern_id), SUMMARY_DIM)
|
||||
}
|
||||
reason := export_block_reason(controller.state)
|
||||
if len(reason) > 0 {
|
||||
draw_summary_line(x, y+124, fmt.tprintf("Export blocked: %s", reason), ERROR)
|
||||
} else {
|
||||
rl.DrawText("Use Export button or Ctrl+E", x, y+124, 18, SUMMARY_HINT)
|
||||
}
|
||||
case .Community:
|
||||
rl.DrawText("Community features coming soon", x, y+30, 18, rl.DARKGRAY)
|
||||
rl.DrawText("Current focus: local GUI workflows", x, y+54, 18, rl.DARKGRAY)
|
||||
}
|
||||
}
|
||||
|
||||
clamp_script_cursor :: proc(page_count, cursor: int) -> int {
|
||||
if page_count <= 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor < 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor >= page_count {
|
||||
return page_count - 1
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
clamp_panel_cursor :: proc(panel_count, cursor: int) -> int {
|
||||
if panel_count <= 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor < 0 {
|
||||
return 0
|
||||
}
|
||||
if cursor >= panel_count {
|
||||
return panel_count - 1
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
panel_by_flat_index :: proc(script: core.Comic_Script, panel_idx: int) -> (core.Panel, int, bool) {
|
||||
if panel_idx < 0 {
|
||||
return core.Panel{}, 0, false
|
||||
}
|
||||
flat := 0
|
||||
for page in script.pages {
|
||||
for panel in page.panels {
|
||||
if flat == panel_idx {
|
||||
return panel, page.page_number, true
|
||||
}
|
||||
flat += 1
|
||||
}
|
||||
}
|
||||
return core.Panel{}, 0, false
|
||||
}
|
||||
|
||||
build_script_page_detail_text :: proc(state: core.Comic_State, cursor: int) -> string {
|
||||
page_count := len(state.script.pages)
|
||||
if page_count == 0 {
|
||||
return fmt.aprintf("No script pages available.")
|
||||
}
|
||||
idx := clamp_script_cursor(page_count, cursor)
|
||||
page := state.script.pages[idx]
|
||||
out := fmt.aprintf("Title: %s\nPage %d/%d (script page #%d)\nPanels: %d", state.script.title, idx+1, page_count, page.page_number, len(page.panels))
|
||||
for pn in page.panels {
|
||||
desc := pn.description
|
||||
if len(desc) == 0 {
|
||||
desc = "(no description)"
|
||||
}
|
||||
next := fmt.aprintf("%s\n\nPanel %d [%v]\n%s", out, pn.panel_number, pn.shot_type, desc)
|
||||
delete(out)
|
||||
out = next
|
||||
for d in pn.dialogue {
|
||||
line := fmt.aprintf("%s\n- %s: %s", out, d.speaker_id, d.text)
|
||||
delete(out)
|
||||
out = line
|
||||
}
|
||||
if len(pn.caption) > 0 {
|
||||
line := fmt.aprintf("%s\n caption: %s", out, pn.caption)
|
||||
delete(out)
|
||||
out = line
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
build_full_script_text :: proc(state: core.Comic_State) -> string {
|
||||
page_count := len(state.script.pages)
|
||||
if page_count == 0 {
|
||||
return fmt.aprintf("No script pages available.")
|
||||
}
|
||||
out := fmt.aprintf("Title: %s\nSynopsis: %s\nCharacters: %d\nPages: %d", state.script.title, state.script.synopsis, len(state.script.characters), page_count)
|
||||
for page in state.script.pages {
|
||||
head := fmt.aprintf("%s\n\n=== Page %d (%d panels) ===", out, page.page_number, len(page.panels))
|
||||
delete(out)
|
||||
out = head
|
||||
for pn in page.panels {
|
||||
desc := pn.description
|
||||
if len(desc) == 0 {
|
||||
desc = "(no description)"
|
||||
}
|
||||
row := fmt.aprintf("%s\nPanel %d: %s", out, pn.panel_number, desc)
|
||||
delete(out)
|
||||
out = row
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
draw_script_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) {
|
||||
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
|
||||
draw_section_title(x+18, y+6, "Script Detail")
|
||||
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
|
||||
page_count := len(controller.state.script.pages)
|
||||
if page_count == 0 {
|
||||
draw_summary_line(x+18, y+46, "No script pages yet. Run Generate Script Local.", SUMMARY_HINT)
|
||||
return
|
||||
}
|
||||
idx := clamp_script_cursor(page_count, cursor)
|
||||
page := controller.state.script.pages[idx]
|
||||
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d (#%d) • panels %d", idx+1, page_count, page.page_number, len(page.panels)), SUMMARY_ACCENT)
|
||||
line_y := y + 70
|
||||
line_step: i32 = 20
|
||||
line_max: i32 = (h - 84) / line_step
|
||||
lines_used: i32 = 0
|
||||
for pn in page.panels {
|
||||
if lines_used >= line_max {
|
||||
break
|
||||
}
|
||||
desc := pn.description
|
||||
if len(desc) == 0 {
|
||||
desc = "(no description)"
|
||||
}
|
||||
draw_summary_subline(x+18, line_y+lines_used*line_step, fit_text_for_width(fmt.tprintf("• P%d: %s", pn.panel_number, desc), int(w-40), 7), SUMMARY_SUBLINE)
|
||||
lines_used += 1
|
||||
if len(pn.dialogue) > 0 && lines_used < line_max {
|
||||
draw_summary_subline(x+30, line_y+lines_used*line_step, fit_text_for_width(fmt.tprintf("\"%s\"", pn.dialogue[0].text), int(w-52), 7), SUMMARY_DIM)
|
||||
lines_used += 1
|
||||
}
|
||||
}
|
||||
if len(page.panels) > 0 && lines_used >= line_max {
|
||||
draw_summary_subline(x+w-140, y+h-18, "…more", SUMMARY_ACCENT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
draw_panels_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (retry_clicked: bool, new_cursor: int) {
|
||||
new_cursor = cursor
|
||||
retry_clicked = false
|
||||
|
||||
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
|
||||
draw_section_title(x+18, y+6, "Panel Results")
|
||||
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
|
||||
panel_count := count_script_panels(controller.state.script)
|
||||
if panel_count == 0 {
|
||||
draw_summary_line(x+18, y+46, "No script panels yet. Generate Script first.", SUMMARY_HINT)
|
||||
return
|
||||
}
|
||||
idx := clamp_panel_cursor(panel_count, cursor)
|
||||
panel, page_num, ok := panel_by_flat_index(controller.state.script, idx)
|
||||
if !ok {
|
||||
draw_summary_line(x+18, y+46, "Panel index out of range.", ERROR)
|
||||
return
|
||||
}
|
||||
img, has_img := controller.state.panel_images[panel.panel_id]
|
||||
err_msg, has_err := controller.state.panel_errors[panel.panel_id]
|
||||
status := "missing"
|
||||
status_color := WARNING
|
||||
if has_err {
|
||||
status = "error"
|
||||
status_color = ERROR
|
||||
}
|
||||
if has_img {
|
||||
status = "ready"
|
||||
status_color = SUCCESS
|
||||
}
|
||||
draw_summary_line(x+18, y+46, fmt.tprintf("Panel %d/%d • page %d # %d • %s", idx+1, panel_count, page_num, panel.panel_number, status), status_color)
|
||||
|
||||
btn_label := "Regenerate"
|
||||
if status != "ready" {
|
||||
btn_label = "Generate"
|
||||
}
|
||||
btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24}
|
||||
draw_small_button_state(btn_rec, btn_label, true)
|
||||
if button_clicked(btn_rec) {
|
||||
retry_clicked = true
|
||||
}
|
||||
|
||||
draw_summary_subline(x+18, y+66, fit_text_for_width(fmt.tprintf("id: %s", panel.panel_id), int(w-120), 7), SUMMARY_SUBLINE)
|
||||
if has_err {
|
||||
draw_summary_subline(x+18, y+84, fit_text_for_width(fmt.tprintf("err: %s", err_msg), int(w-36), 7), ERROR)
|
||||
} else if has_img {
|
||||
draw_summary_subline(x+18, y+84, fit_text_for_width(fmt.tprintf("img: %dx%d seed:%d", img.width, img.height, img.seed), int(w-36), 7), SUMMARY_DIM)
|
||||
} else {
|
||||
draw_summary_subline(x+18, y+84, "img: not generated", SUMMARY_DIM)
|
||||
}
|
||||
desc := panel.description
|
||||
if len(desc) == 0 {
|
||||
desc = "(no description)"
|
||||
}
|
||||
draw_summary_subline(x+18, y+104, fit_text_for_width(fmt.tprintf("desc: %s", desc), int(w-36), 7), SUMMARY_SUBLINE)
|
||||
if has_img {
|
||||
draw_summary_subline(x+18, y+124, fit_text_for_width(fmt.tprintf("src: %s", img.url), int(w-36), 7), SUMMARY_DIM)
|
||||
}
|
||||
|
||||
list_y := y + 146
|
||||
row_h: i32 = 18
|
||||
rows: i32 = (h - 154) / row_h
|
||||
if rows < 1 {
|
||||
rows = 1
|
||||
}
|
||||
start := idx - int(rows/2)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + int(rows)
|
||||
if end > panel_count {
|
||||
end = panel_count
|
||||
start = end - int(rows)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
}
|
||||
line: i32 = 0
|
||||
for i in start..<end {
|
||||
row_panel, row_page, row_ok := panel_by_flat_index(controller.state.script, i)
|
||||
if !row_ok {
|
||||
continue
|
||||
}
|
||||
mark := " "
|
||||
row_color := TEXT_TERTIARY
|
||||
if i == idx {
|
||||
mark = ">"
|
||||
row_color = TEXT_PRIMARY
|
||||
}
|
||||
ready := "missing"
|
||||
if _, err_exists := controller.state.panel_errors[row_panel.panel_id]; err_exists {
|
||||
ready = "error"
|
||||
}
|
||||
if _, exists := controller.state.panel_images[row_panel.panel_id]; exists {
|
||||
ready = "ready"
|
||||
}
|
||||
|
||||
row_rec := rl.Rectangle{x = f32(x+18), y = f32(list_y+line*row_h), width = f32(w-36), height = f32(row_h)}
|
||||
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
|
||||
new_cursor = i
|
||||
}
|
||||
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
|
||||
row_color = TEXT_SECONDARY
|
||||
}
|
||||
|
||||
draw_summary_subline(x+18, list_y+line*row_h+2, fit_text_for_width(fmt.tprintf("%s %02d p%d#%d %s", mark, i+1, row_page, row_panel.panel_number, ready), int(w-36), 7), row_color)
|
||||
line += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package gui
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
fit_text_for_width :: proc(text: string, width_px, px_per_char: int) -> string {
|
||||
if px_per_char <= 0 {
|
||||
return text
|
||||
}
|
||||
max_chars := width_px / px_per_char
|
||||
if max_chars < 4 {
|
||||
max_chars = 4
|
||||
}
|
||||
if len(text) <= max_chars {
|
||||
return text
|
||||
}
|
||||
return fmt.tprintf("%s…", text[:max_chars-1])
|
||||
}
|
||||
|
||||
draw_text_fitted :: proc(text: string, x, y, font_size: i32, width_px, px_per_char: int, color: rl.Color) {
|
||||
display := fit_text_for_width(text, width_px, px_per_char)
|
||||
rl.DrawText(fmt.ctprintf("%s", display), x, y, font_size, color)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package gui
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// ── Backgrounds ──────────────────────────────────────────────────────────
|
||||
BG_BASE :: rl.Color{13, 13, 18, 255}
|
||||
BG_SIDEBAR :: rl.Color{18, 18, 24, 255}
|
||||
BG_TOPBAR :: rl.Color{18, 18, 24, 255}
|
||||
BG_CARD :: rl.Color{24, 24, 32, 255}
|
||||
BG_CARD_ALT :: rl.Color{28, 28, 38, 255} // slightly elevated card
|
||||
BG_STRIP :: rl.Color{22, 22, 30, 255} // subtle strip background
|
||||
BG_OVERLAY :: rl.Color{8, 8, 12, 180} // modal backdrop
|
||||
|
||||
// ── Borders ──────────────────────────────────────────────────────────────
|
||||
BORDER_CARD :: rl.Color{40, 40, 52, 255}
|
||||
BORDER_SUBTLE :: rl.Color{36, 36, 48, 255}
|
||||
BORDER_DIVIDER :: rl.Color{36, 36, 48, 255}
|
||||
|
||||
// ── Accent (Indigo-Violet) ───────────────────────────────────────────────
|
||||
ACCENT :: rl.Color{99, 102, 241, 255}
|
||||
ACCENT_HOVER :: rl.Color{120, 122, 248, 255}
|
||||
ACCENT_MUTED :: rl.Color{68, 70, 180, 255}
|
||||
ACCENT_SURFACE :: rl.Color{30, 30, 56, 255}
|
||||
ACCENT_GLOW :: rl.Color{99, 102, 241, 80}
|
||||
|
||||
// ── Text ─────────────────────────────────────────────────────────────────
|
||||
TEXT_PRIMARY :: rl.Color{228, 228, 240, 255}
|
||||
TEXT_SECONDARY :: rl.Color{148, 148, 168, 255}
|
||||
TEXT_TERTIARY :: rl.Color{98, 98, 118, 255}
|
||||
TEXT_DISABLED :: rl.Color{68, 68, 88, 255}
|
||||
TEXT_BRIGHT :: rl.Color{245, 245, 255, 255}
|
||||
|
||||
// ── Semantic: Success ────────────────────────────────────────────────────
|
||||
SUCCESS :: rl.Color{52, 211, 153, 255}
|
||||
SUCCESS_BG :: rl.Color{16, 42, 32, 255}
|
||||
SUCCESS_BORDER :: rl.Color{40, 100, 74, 255}
|
||||
SUCCESS_TEXT :: rl.Color{110, 231, 183, 255}
|
||||
|
||||
// ── Semantic: Warning ────────────────────────────────────────────────────
|
||||
WARNING :: rl.Color{251, 191, 36, 255}
|
||||
WARNING_BG :: rl.Color{50, 38, 14, 255}
|
||||
WARNING_BORDER :: rl.Color{120, 90, 30, 255}
|
||||
WARNING_TEXT :: rl.Color{253, 224, 120, 255}
|
||||
|
||||
// ── Semantic: Error ──────────────────────────────────────────────────────
|
||||
ERROR :: rl.Color{248, 113, 113, 255}
|
||||
ERROR_BG :: rl.Color{50, 18, 18, 255}
|
||||
ERROR_BORDER :: rl.Color{120, 50, 50, 255}
|
||||
ERROR_TEXT :: rl.Color{254, 178, 178, 255}
|
||||
|
||||
// ── Semantic: Danger (destructive buttons) ───────────────────────────────
|
||||
DANGER_BG :: rl.Color{153, 50, 58, 255}
|
||||
DANGER_BG_HOVER :: rl.Color{172, 60, 68, 255}
|
||||
DANGER_BORDER :: rl.Color{130, 42, 48, 255}
|
||||
|
||||
// ── Semantic: Warning-style buttons ──────────────────────────────────────
|
||||
WARN_BTN_BG :: rl.Color{80, 64, 30, 255}
|
||||
WARN_BTN_BG_HOVER :: rl.Color{95, 76, 36, 255}
|
||||
WARN_BTN_BORDER :: rl.Color{110, 88, 40, 255}
|
||||
WARN_BTN_TEXT :: rl.Color{253, 224, 120, 255}
|
||||
|
||||
// ── Buttons ──────────────────────────────────────────────────────────────
|
||||
BTN_BG :: rl.Color{32, 32, 44, 255}
|
||||
BTN_BG_HOVER :: rl.Color{40, 40, 54, 255}
|
||||
BTN_BORDER :: rl.Color{52, 52, 68, 255}
|
||||
BTN_BORDER_HOVER :: rl.Color{80, 82, 140, 255}
|
||||
BTN_TEXT :: TEXT_PRIMARY
|
||||
|
||||
BTN_SOFT_BG :: rl.Color{28, 30, 48, 255}
|
||||
BTN_SOFT_BG_HOVER :: rl.Color{36, 38, 58, 255}
|
||||
BTN_SOFT_BORDER :: rl.Color{60, 62, 100, 255}
|
||||
BTN_SOFT_TEXT :: rl.Color{160, 162, 220, 255}
|
||||
|
||||
BTN_DISABLED_BG :: rl.Color{24, 24, 32, 255}
|
||||
BTN_DISABLED_BORDER :: rl.Color{36, 36, 48, 255}
|
||||
BTN_DISABLED_TEXT :: rl.Color{68, 68, 88, 255}
|
||||
|
||||
// ── Small Buttons ────────────────────────────────────────────────────────
|
||||
SBTN_BG :: rl.Color{30, 30, 42, 255}
|
||||
SBTN_BG_HOVER :: rl.Color{40, 40, 54, 255}
|
||||
SBTN_BORDER :: rl.Color{50, 50, 66, 255}
|
||||
SBTN_BORDER_HOVER :: rl.Color{80, 82, 140, 255}
|
||||
SBTN_TEXT :: rl.Color{188, 188, 210, 255}
|
||||
|
||||
// ── Navigation ───────────────────────────────────────────────────────────
|
||||
NAV_BG :: rl.Color{24, 24, 32, 255}
|
||||
NAV_BG_HOVER :: rl.Color{32, 32, 44, 255}
|
||||
NAV_BORDER :: rl.Color{40, 40, 52, 255}
|
||||
NAV_BORDER_HOVER :: rl.Color{70, 72, 120, 255}
|
||||
NAV_TEXT :: rl.Color{168, 168, 188, 255}
|
||||
NAV_ACTIVE_BG :: ACCENT
|
||||
NAV_ACTIVE_TEXT :: TEXT_BRIGHT
|
||||
NAV_ACTIVE_BAR :: rl.Color{180, 182, 255, 255}
|
||||
|
||||
// ── Input Fields ─────────────────────────────────────────────────────────
|
||||
INPUT_BG :: rl.Color{16, 16, 24, 255}
|
||||
INPUT_BORDER :: rl.Color{40, 40, 54, 255}
|
||||
INPUT_FOCUS_BG :: rl.Color{20, 20, 30, 255}
|
||||
INPUT_FOCUS_BORDER :: ACCENT
|
||||
INPUT_FOCUS_RING :: ACCENT_GLOW
|
||||
INPUT_TEXT :: TEXT_PRIMARY
|
||||
INPUT_TEXT_FOCUS :: TEXT_BRIGHT
|
||||
|
||||
// ── Chips & Pills ────────────────────────────────────────────────────────
|
||||
PILL_BG :: rl.Color{28, 28, 38, 255}
|
||||
PILL_BORDER :: rl.Color{44, 44, 58, 255}
|
||||
PILL_TEXT :: TEXT_SECONDARY
|
||||
|
||||
PILL_ACCENT_BG :: ACCENT_SURFACE
|
||||
PILL_ACCENT_BORDER :: ACCENT_MUTED
|
||||
PILL_ACCENT_TEXT :: rl.Color{180, 182, 255, 255}
|
||||
|
||||
CHIP_BG :: rl.Color{28, 28, 38, 255}
|
||||
CHIP_BORDER :: rl.Color{44, 44, 58, 255}
|
||||
CHIP_TEXT :: TEXT_SECONDARY
|
||||
|
||||
CHIP_ACCENT_BG :: ACCENT_SURFACE
|
||||
CHIP_ACCENT_BORDER :: ACCENT_MUTED
|
||||
CHIP_ACCENT_TEXT :: PILL_ACCENT_TEXT
|
||||
|
||||
// ── Status Badges ────────────────────────────────────────────────────────
|
||||
BADGE_OK_BG :: SUCCESS_BG
|
||||
BADGE_OK_BORDER :: SUCCESS_BORDER
|
||||
BADGE_OK_TEXT :: SUCCESS_TEXT
|
||||
BADGE_BAD_BG :: ERROR_BG
|
||||
BADGE_BAD_BORDER :: ERROR_BORDER
|
||||
BADGE_BAD_TEXT :: ERROR_TEXT
|
||||
|
||||
// ── Readiness Chips ──────────────────────────────────────────────────────
|
||||
READY_BG :: SUCCESS_BG
|
||||
READY_BORDER :: SUCCESS_BORDER
|
||||
READY_TEXT :: SUCCESS_TEXT
|
||||
UNREADY_BG :: rl.Color{28, 28, 38, 255}
|
||||
UNREADY_BORDER :: rl.Color{44, 44, 58, 255}
|
||||
UNREADY_TEXT :: TEXT_TERTIARY
|
||||
|
||||
// ── Progress Bar ─────────────────────────────────────────────────────────
|
||||
PROGRESS_TRACK :: rl.Color{28, 28, 40, 255}
|
||||
PROGRESS_FILL :: ACCENT
|
||||
|
||||
// ── Toast ────────────────────────────────────────────────────────────────
|
||||
TOAST_SUCCESS :: rl.Color{28, 120, 80, 235}
|
||||
TOAST_WARNING :: rl.Color{140, 100, 30, 235}
|
||||
TOAST_ERROR :: rl.Color{150, 50, 50, 235}
|
||||
TOAST_BORDER :: rl.Color{255, 255, 255, 40}
|
||||
TOAST_SHADOW :: rl.Color{0, 0, 0, 60}
|
||||
|
||||
// ── Action Log ───────────────────────────────────────────────────────────
|
||||
LOG_ROW_ALT :: rl.Color{22, 22, 30, 255}
|
||||
LOG_TEXT :: rl.Color{158, 158, 178, 255}
|
||||
|
||||
// ── Section Titles ───────────────────────────────────────────────────────
|
||||
SECTION_TITLE_COLOR :: rl.Color{148, 150, 210, 255}
|
||||
SECTION_UNDERLINE :: rl.Color{44, 44, 60, 255}
|
||||
|
||||
// ── Screen Summary ───────────────────────────────────────────────────────
|
||||
SUMMARY_TITLE :: rl.Color{170, 172, 230, 255}
|
||||
SUMMARY_ACCENT :: rl.Color{99, 140, 220, 255}
|
||||
SUMMARY_HINT :: rl.Color{120, 90, 200, 255}
|
||||
SUMMARY_SUBLINE :: rl.Color{128, 128, 148, 255}
|
||||
SUMMARY_DIM :: rl.Color{98, 98, 118, 255}
|
||||
|
||||
// ── Pipeline Stepper ─────────────────────────────────────────────────────
|
||||
STEP_DONE_FILL :: SUCCESS
|
||||
STEP_DONE_BORDER :: SUCCESS_BORDER
|
||||
STEP_TODO_FILL :: rl.Color{36, 36, 48, 255}
|
||||
STEP_TODO_BORDER :: rl.Color{52, 52, 68, 255}
|
||||
STEP_LINE_DONE :: rl.Color{40, 100, 74, 255}
|
||||
STEP_LINE_TODO :: rl.Color{40, 40, 52, 255}
|
||||
STEP_LABEL_DONE :: SUCCESS_TEXT
|
||||
STEP_LABEL_TODO :: TEXT_TERTIARY
|
||||
|
||||
// ── Help Overlay ─────────────────────────────────────────────────────────
|
||||
HELP_TITLE :: TEXT_BRIGHT
|
||||
HELP_SECTION :: rl.Color{130, 132, 210, 255}
|
||||
HELP_LINE :: TEXT_SECONDARY
|
||||
HELP_CLOSE :: rl.Color{170, 148, 240, 255}
|
||||
|
||||
// ── Confirm Overlay ──────────────────────────────────────────────────────
|
||||
CONFIRM_ACCENT :: ACCENT
|
||||
CONFIRM_TITLE :: TEXT_BRIGHT
|
||||
CONFIRM_BODY :: TEXT_SECONDARY
|
||||
CONFIRM_HINT :: rl.Color{170, 148, 240, 255}
|
||||
|
||||
// ── Sidebar Shortcuts ────────────────────────────────────────────────────
|
||||
SIDEBAR_TITLE :: rl.Color{130, 132, 200, 255}
|
||||
SIDEBAR_TEXT :: TEXT_TERTIARY
|
||||
SIDEBAR_FOOTER :: rl.Color{80, 80, 100, 255}
|
||||
|
||||
// ── Brand ────────────────────────────────────────────────────────────────
|
||||
BRAND_TITLE :: TEXT_BRIGHT
|
||||
BRAND_SUBTITLE :: TEXT_TERTIARY
|
||||
|
||||
// ── Roundness Constants ──────────────────────────────────────────────────
|
||||
RADIUS_CARD :: f32(0.14)
|
||||
RADIUS_BUTTON :: f32(0.32)
|
||||
RADIUS_PILL :: f32(0.50)
|
||||
RADIUS_INPUT :: f32(0.24)
|
||||
RADIUS_NAV :: f32(0.28)
|
||||
RADIUS_CHIP :: f32(0.42)
|
||||
RADIUS_BADGE :: f32(0.42)
|
||||
RADIUS_TOAST :: f32(0.40)
|
||||
RADIUS_BAR :: f32(0.60)
|
||||
|
||||
// ── Recommended Halo ─────────────────────────────────────────────────────
|
||||
RECOMMEND_HALO_FILL :: rl.Color{50, 50, 100, 255}
|
||||
RECOMMEND_HALO_BORDER :: rl.Color{120, 122, 248, 255}
|
||||
|
||||
// ── DeepSeek key missing ─────────────────────────────────────────────────
|
||||
KEY_MISSING_COLOR :: ERROR
|
||||
@@ -0,0 +1,17 @@
|
||||
package gui
|
||||
|
||||
Summary_View_Options :: struct {
|
||||
script_show_all: bool,
|
||||
script_desc: bool,
|
||||
script_page_cursor: int,
|
||||
panel_cursor: int,
|
||||
layout_show_all: bool,
|
||||
layout_desc: bool,
|
||||
layout_page_cursor: int,
|
||||
}
|
||||
|
||||
Pending_Confirm_Action :: enum {
|
||||
None,
|
||||
Reset_Project,
|
||||
Open_Project,
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package gui
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_card :: proc(rec: rl.Rectangle) {
|
||||
rl.DrawRectangleRounded(rec, RADIUS_CARD, 8, BG_CARD)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CARD, 8, 1.0, BORDER_CARD)
|
||||
}
|
||||
|
||||
draw_subtle_strip :: proc(rec: rl.Rectangle) {
|
||||
rl.DrawRectangleRounded(rec, 0.20, 8, BG_STRIP)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, 0.20, 8, 1.0, BORDER_SUBTLE)
|
||||
}
|
||||
|
||||
draw_hint_pill :: proc(rec: rl.Rectangle, label: string, accent: bool) {
|
||||
bg := PILL_BG
|
||||
border := PILL_BORDER
|
||||
fg := PILL_TEXT
|
||||
if accent {
|
||||
bg = PILL_ACCENT_BG
|
||||
border = PILL_ACCENT_BORDER
|
||||
fg = PILL_ACCENT_TEXT
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_PILL, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_PILL, 8, 1.0, border)
|
||||
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 13, int(rec.width)-16, 7, fg)
|
||||
}
|
||||
|
||||
draw_topbar_chip :: proc(rec: rl.Rectangle, label: string, accent: bool) {
|
||||
bg := CHIP_BG
|
||||
border := CHIP_BORDER
|
||||
fg := CHIP_TEXT
|
||||
if accent {
|
||||
bg = CHIP_ACCENT_BG
|
||||
border = CHIP_ACCENT_BORDER
|
||||
fg = CHIP_ACCENT_TEXT
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_CHIP, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_CHIP, 8, 1.0, border)
|
||||
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 14, int(rec.width)-16, 8, fg)
|
||||
}
|
||||
|
||||
draw_status_badge :: proc(rec: rl.Rectangle, label: string, ok: bool) {
|
||||
bg := BADGE_BAD_BG
|
||||
border := BADGE_BAD_BORDER
|
||||
fg := BADGE_BAD_TEXT
|
||||
if ok {
|
||||
bg = BADGE_OK_BG
|
||||
border = BADGE_OK_BORDER
|
||||
fg = BADGE_OK_TEXT
|
||||
}
|
||||
rl.DrawRectangleRounded(rec, RADIUS_BADGE, 8, bg)
|
||||
rl.DrawRectangleRoundedLinesEx(rec, RADIUS_BADGE, 8, 1.0, border)
|
||||
draw_text_fitted(label, i32(rec.x)+8, i32(rec.y)+4, 13, int(rec.width)-16, 7, fg)
|
||||
}
|
||||
|
||||
draw_section_title :: proc(x, y: i32, label: string) {
|
||||
draw_text_fitted(label, x, y, 17, 180, 8, SECTION_TITLE_COLOR)
|
||||
rl.DrawLine(x, y+20, x+180, y+20, SECTION_UNDERLINE)
|
||||
}
|
||||
|
||||
draw_summary_line :: proc(x, y: i32, text: string, c: rl.Color) {
|
||||
fg := c
|
||||
if int(c.r)+int(c.g)+int(c.b) < 260 {
|
||||
fg = TEXT_PRIMARY
|
||||
}
|
||||
draw_text_fitted(text, x, y, 18, 438, 8, fg)
|
||||
}
|
||||
|
||||
draw_summary_subline :: proc(x, y: i32, text: string, c: rl.Color) {
|
||||
fg := c
|
||||
if int(c.r)+int(c.g)+int(c.b) < 260 {
|
||||
fg = TEXT_SECONDARY
|
||||
}
|
||||
draw_text_fitted(text, x, y, 16, 438, 7, fg)
|
||||
}
|
||||
Reference in new issue
Block a user