meltygui 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,658 @@
1
+ """Orchestration view functions and supporting definitions."""
2
+ from meltygui.hdr_color import pack_color
3
+ from meltygui.core.melty import Melty
4
+ from meltygui.core.core_render import render_func
5
+ from meltygui.state.orchestration_state import OrchestratorPanelState
6
+ import meltygui_imgui as imgui
7
+ import time
8
+
9
+
10
+ @render_func(use_cache=True, selectable=False, show_add_delete=False,
11
+ is_tree=False, show_name=True, shadow=True, tint=(0.719, 0.478, 0.208))
12
+ def draw_orchestrator(input_value=None, draw_state=None, style_manager=None,
13
+ panel_state: OrchestratorPanelState = None,
14
+ renaming_key="", rename_text="",
15
+ left_mouse_down=False, left_mouse_double_clicked=False, **kwargs):
16
+ # NOTE the click param names: "double_left_mouse_down" canonicalises
17
+ # to the SAME (input_id, DOWN) key as left_mouse_down (DOWN has no double
18
+ # promotion) but the per-frame name cache keeps one name per key - it
19
+ # silently drops every plain press. DOUBLE_CLICKED is its own key.
20
+ from meltygui.core.windowing.glfw_utils import request_render
21
+ from meltygui.core.cache.tile_cache import add_shadow
22
+ from meltygui.core.automation.orchestration_core import Orchestrator
23
+ from meltygui.core.automation.orchestration_core import _format_value
24
+ from meltygui.core.automation.orchestration_core import _wrap_text
25
+ from meltygui.core.automation.orchestration_core import cue_gesture
26
+ from meltygui.core.automation.orchestration_core import cue_get
27
+ from meltygui.core.automation.orchestration_core import cue_has_target
28
+ from meltygui.core.automation.orchestration_core import cue_press_frac
29
+ from meltygui.core.automation.orchestration_core import failure_report
30
+ from meltygui.core.automation.orchestration_core import generalized_commands
31
+ from meltygui.core.automation.orchestration_core import group_events
32
+ from meltygui.core.automation.orchestration_core import parse_argument
33
+ import meltygui.core.automation.orchestration_core
34
+
35
+ meltygui.core.automation.orchestration_core._window_draw_state = draw_state
36
+ Orchestrator._precondition_watch = set() # re-declared by the command rows below
37
+ root = getattr(Melty.vis, "root", None)
38
+ store = getattr(root, "orchestrations", None)
39
+ if store is None:
40
+ imgui.text("model not loaded")
41
+ return False, input_value
42
+ orchestrations = store.orchestrations
43
+
44
+ # ---- icons (glyph literals - the editor renders them as a font) ----
45
+ record_icon = f""
46
+ stop_icon = f""
47
+ play_icon = f""
48
+ plus_icon = f""
49
+ delete_icon = f""
50
+ restore_icon = f""
51
+ chevron_right_icon = f""
52
+ chevron_down_icon = f""
53
+ rename_icon = f""
54
+ check_icon = f"" # precondition rows: "target hittable"
55
+
56
+ # ---- styling (fast_dock recipe) ----
57
+ row_bg_value, row_text_value = 0.06, 0.95
58
+ factor, saturation = 0.85, 1.0
59
+ button_bg_value, button_text_value = 0.13, 1.25
60
+ hover_bg_boost, hover_text_boost = 0.05, 0.5
61
+ text_saturation = 0.8
62
+ dim_text_value = 0.45
63
+ record_tint = (0.85, 0.25, 0.25) # armed recording button / pulse
64
+ restore_on_tint = (0.4, 0.85, 0.5) # restore chip when checked
65
+
66
+ # ---- geometry, authored at ui_scale 1.0 and scaled once per frame ----
67
+ px = Melty.px
68
+ # [tint=(0.939, 0.453, 0.245)]
69
+ row_height = px(28.0)
70
+ row_gap = px(3.0)
71
+ toolbar_height = px(26.0)
72
+ toolbar_gap = px(8.0)
73
+ button_pad_x = px(10.0)
74
+ button_gap = px(6.0)
75
+ chip_width = px(24.0) # per-row icon buttons (play/restore/delete)
76
+ corner = px(6.0)
77
+ pad_x = px(8.0)
78
+ text_nudge_y = px(-1.0)
79
+ status_height = px(20.0)
80
+
81
+ if style_manager is None:
82
+ style_manager = Melty.style_manager
83
+ draw_list = imgui.get_window_draw_list()
84
+ origin_x, origin_y = imgui.get_cursor_screen_pos()
85
+ content_width = draw_state.content_width or (draw_state.width or 300)
86
+ mouse_x, mouse_y = imgui.get_mouse_pos()
87
+ hover_ok = draw_state._bounding_hovered
88
+ press = left_mouse_down
89
+ # [tint=(0.62, 0.47, 0.95)]
90
+ click = (press.x, press.y) if (press and hasattr(press, "x")) else None
91
+ clip = getattr(draw_state, "abs_clip_rect", None)
92
+ changed = False
93
+
94
+ # The engine trims the stop click off a take's tail with this rect.
95
+ Orchestrator.window_rect = (draw_state.abs_left, draw_state.abs_top,
96
+ draw_state.abs_left + (draw_state.width or content_width),
97
+ draw_state.abs_top + (draw_state.height or 200))
98
+
99
+ def _mix(tint, value, mix_factor, mix_saturation):
100
+ color = tint if (isinstance(tint, tuple) and len(tint) >= 3) else (0.5, 0.5, 0.5)
101
+ return style_manager.make_color_rgb(color[0], color[1], color[2], value=value,
102
+ factor=mix_factor, saturation_scale=mix_saturation)
103
+
104
+ def _u32(color, alpha=1.0):
105
+ return pack_color(color[0], color[1], color[2], alpha)
106
+
107
+ def _in(rect, x, y):
108
+ return rect[0] <= x <= rect[2] and rect[1] <= y <= rect[3]
109
+
110
+ def _button(x, y, width, height, label, tint, active=False, icon_only=False):
111
+ """Draw a toolbar/chip button, return (clicked, right_edge)."""
112
+ rect = (x, y, x + width, y + height)
113
+ hovered = hover_ok and _in(rect, mouse_x, mouse_y)
114
+ bg_value = button_bg_value + (0.08 if active else 0.0) \
115
+ + (hover_bg_boost if hovered else 0.0)
116
+ background = _mix(tint, bg_value, factor, saturation)
117
+ text_color = _mix(tint, button_text_value + (hover_text_boost if hovered else 0.0),
118
+ factor, text_saturation)
119
+ if active or hovered or not icon_only:
120
+ add_shadow((x, y, width, height), corner_radius=corner, clip=clip)
121
+ draw_list.add_rect_filled(rect[0], rect[1], rect[2], rect[3],
122
+ _u32(background), rounding=corner)
123
+ label_size = imgui.calc_text_size(label)
124
+ draw_list.add_text(x + (width - label_size[0]) / 2.0,
125
+ y + (height - label_size[1]) / 2.0 + text_nudge_y,
126
+ _u32(text_color), label)
127
+ clicked = click is not None and _in(rect, click[0], click[1])
128
+ return clicked, rect[2]
129
+
130
+ # ---- toolbar: Record/Stop - Play/Abort - + New ----
131
+ toolbar_x = origin_x + pad_x
132
+ toolbar_y = origin_y
133
+ engine_busy = (Orchestrator.replaying is not None or bool(Orchestrator._restore_steps)
134
+ or Orchestrator._task is not None)
135
+ is_recording = Orchestrator.recording is not None
136
+
137
+ # Record New ALWAYS creates a fresh orchestration and records into it;
138
+ # re-recording an existing row use that row's own record chip.
139
+ record_label = f"{stop_icon} Stop" if is_recording \
140
+ else f"{record_icon} Record New"
141
+ record_width = imgui.calc_text_size(record_label)[0] + 2 * button_pad_x
142
+ record_clicked, edge = _button(toolbar_x, toolbar_y, record_width, toolbar_height,
143
+ record_label, record_tint if is_recording else draw_state.tint,
144
+ active=is_recording)
145
+ if record_clicked and not engine_busy:
146
+ if is_recording:
147
+ Orchestrator.stop_recording()
148
+ else:
149
+ from meltygui.models.orchestration import Orchestration
150
+ target = Orchestration()
151
+ target.name = f"Orchestration {len(orchestrations) + 1}"
152
+ orchestrations[target.id] = target
153
+ Orchestrator.start_recording(target)
154
+ changed = True
155
+
156
+ if engine_busy:
157
+ abort_label = f"{stop_icon} Abort"
158
+ abort_width = imgui.calc_text_size(abort_label)[0] + 2 * button_pad_x
159
+ abort_clicked, edge = _button(edge + button_gap, toolbar_y, abort_width,
160
+ toolbar_height, abort_label, record_tint, active=True)
161
+ if abort_clicked:
162
+ Orchestrator.abort("stop button")
163
+
164
+ # ---- rows (one per orchestration, each expands to its detail tabs) ----
165
+ rows_top = toolbar_y + toolbar_height + toolbar_gap
166
+ row_left = origin_x + pad_x
167
+ row_right = origin_x + content_width - pad_x
168
+ row_stride = row_height + row_gap
169
+ row_y = rows_top
170
+ detail_row_height = px(18.0)
171
+ detail_indent = px(34.0)
172
+ tab_height = px(20.0)
173
+ tab_pad_x = px(8.0)
174
+ precondition_indent = detail_indent + px(26.0) # precondition rows sit under their command
175
+ cue_tint = (0.85, 0.65, 0.2) # cue / effect rows (undo anchors)
176
+ fail_tint = (0.92, 0.32, 0.3) # failed row / error bar
177
+ success_tint = (0.32, 0.78, 0.42) # success row wash / ok tag
178
+ dim_color = _mix(draw_state.tint, dim_text_value, factor, text_saturation)
179
+ fail_color = _mix(fail_tint, row_text_value + 0.2, 0.35, 1.2)
180
+ success_color = _mix(success_tint, row_text_value + 0.1, 0.35, 1.2)
181
+ expanded_map = panel_state.expanded if panel_state is not None else {}
182
+ tab_map = panel_state.tab if panel_state is not None else {}
183
+ dbl = (left_mouse_double_clicked.x, left_mouse_double_clicked.y) \
184
+ if (left_mouse_double_clicked and hasattr(left_mouse_double_clicked, "x")) else None
185
+
186
+ def _start_rename(key, orchestration):
187
+ draw_state.renaming_key = key
188
+ draw_state.rename_text = orchestration.name
189
+ if panel_state is not None:
190
+ panel_state._rename_focus = True
191
+ panel_state._rename_was_active = False
192
+ request_render()
193
+
194
+ def _commit_rename(orchestration):
195
+ text = (draw_state.rename_text or "").strip()
196
+ if text and text != orchestration.name:
197
+ orchestration.name = text
198
+ orchestration.custom_name = True # auto-name never overwrites this
199
+ request_render()
200
+ committed = True
201
+ else:
202
+ committed = False
203
+ draw_state.renaming_key = ""
204
+ return committed
205
+
206
+ def _detail_rows(rows_list, playing, fail_at=None):
207
+ """Both tabs render through here — same replay-progress highlight:
208
+ the row being replayed brightens, finished rows dim, effect rows
209
+ (cues / commands) wear the amber, the FAILED row (the cue that
210
+ never verified / the event the replay stopped at) reads red."""
211
+ nonlocal row_y
212
+ for row in rows_list:
213
+ start_index, end_index, row_kind, label, tag = row[:5]
214
+ if clip is None or not (row_y + detail_row_height < clip[1] or row_y > clip[3]):
215
+ running = playing and start_index <= Orchestrator._replay_index \
216
+ < max(end_index, start_index + 1)
217
+ if row_kind == "cue": # cue rows sit AT their index
218
+ failed_here = (fail_at is not None and not playing
219
+ and start_index == fail_at)
220
+ else:
221
+ failed_here = (fail_at is not None and not playing
222
+ and start_index <= fail_at
223
+ < max(end_index, start_index + 1))
224
+ if failed_here:
225
+ label_color = fail_color
226
+ elif running:
227
+ label_color = _mix(draw_state.tint, row_text_value + 0.4,
228
+ factor, text_saturation) # being replayed
229
+ elif playing and max(end_index, start_index + 1) <= Orchestrator._replay_index:
230
+ label_color = dim_color # already replayed
231
+ elif row_kind in ("cue", "expand", "window", "move", "goto", "set"):
232
+ label_color = _mix(cue_tint, row_text_value, 0.4, 1.2)
233
+ else:
234
+ label_color = _mix(draw_state.tint, row_text_value * 0.75,
235
+ factor, text_saturation)
236
+ draw_list.add_text(row_left + detail_indent, row_y + text_nudge_y,
237
+ _u32(label_color), label)
238
+ if tag:
239
+ tag_width = imgui.calc_text_size(tag)[0]
240
+ draw_list.add_text(row_right - px(4) - tag_width,
241
+ row_y + text_nudge_y, _u32(dim_color), tag)
242
+ row_y += detail_row_height
243
+
244
+ def _command_rows(key, orchestration, tint, rows_list, playing_raw, cmd_cursor,
245
+ fail_cue_index=None):
246
+ """Commands tab: each leaf-edit command's ARGUMENT is an inline edit
247
+ box — the recorded value pre-filled (defaults reproduce the take
248
+ exactly), an edit stores an override (amber text, × resets) and
249
+ routes play through change_value with the edited value. Highlight:
250
+ raw replay by event span, command playback by cue ordinal."""
251
+ nonlocal row_y, changed
252
+ overrides = getattr(orchestration, "overrides", None)
253
+ if overrides is None:
254
+ overrides = orchestration.overrides = {}
255
+ for ordinal, row in enumerate(rows_list):
256
+ start_index, end_index, row_kind, label, tag = row[:5]
257
+ arg = row[5] if len(row) > 5 else None
258
+ row_visible = clip is None or not (row_y + detail_row_height < clip[1]
259
+ or row_y > clip[3])
260
+ if row_visible:
261
+ if cmd_cursor is not None:
262
+ running = ordinal == cmd_cursor
263
+ finished = ordinal < cmd_cursor
264
+ else:
265
+ running = playing_raw and start_index <= Orchestrator._replay_index \
266
+ < max(end_index, start_index + 1)
267
+ finished = playing_raw \
268
+ and max(end_index, start_index + 1) <= Orchestrator._replay_index
269
+ if fail_cue_index is not None and ordinal == fail_cue_index \
270
+ and not running:
271
+ label_color = fail_color
272
+ elif running:
273
+ label_color = _mix(draw_state.tint, row_text_value + 0.4,
274
+ factor, text_saturation)
275
+ elif finished:
276
+ label_color = dim_color
277
+ elif arg is None:
278
+ label_color = _mix(cue_tint, row_text_value, 0.4, 1.2)
279
+ else:
280
+ label_color = _mix(draw_state.tint, row_text_value * 0.75,
281
+ factor, text_saturation)
282
+ # per-action run chip (the indent gutter): replays just THIS
283
+ # command's event span - overrides remap its gesture as in
284
+ # a full play - its cue verifying at the main
285
+ run_clicked, _ = _button(row_left + px(10), row_y, px(18.0),
286
+ px(16.0), play_icon, tint, icon_only=True)
287
+ if run_clicked and not is_recording and not engine_busy:
288
+ Orchestrator.play(orchestration, start=start_index,
289
+ end=max(end_index, start_index), generalize=True)
290
+ changed = True
291
+ draw_list.add_text(row_left + detail_indent, row_y + text_nudge_y,
292
+ _u32(label_color), label)
293
+ if arg is not None:
294
+ override_key = str(ordinal)
295
+ modified = override_key in overrides
296
+ value = overrides.get(override_key, arg)
297
+ value_text = value if isinstance(value, str) else _format_value(value)
298
+ input_width = px(150.0) if isinstance(arg, str) else px(72.0)
299
+ input_x = (row_left + detail_indent
300
+ + imgui.calc_text_size(label)[0] + px(6))
301
+ imgui.set_cursor_screen_pos((input_x, row_y - px(1)))
302
+ imgui.push_item_width(input_width)
303
+ if modified:
304
+ imgui.push_style_color(imgui.COLOR_TEXT, 1.0, 0.75, 0.3, 1.0)
305
+ entered, text = imgui.input_text(
306
+ f"##orch-arg-{key}-{ordinal}", value_text, 128)
307
+ imgui.set_item_allow_overlap()
308
+ if modified:
309
+ imgui.pop_style_color()
310
+ imgui.pop_item_width()
311
+ if entered:
312
+ ok, parsed = parse_argument(text, arg)
313
+ if ok:
314
+ if parsed == arg:
315
+ overrides.pop(override_key, None) # back to default
316
+ else:
317
+ overrides[override_key] = parsed
318
+ changed = True
319
+ request_render()
320
+ close_x = input_x + input_width + px(4)
321
+ draw_list.add_text(close_x, row_y + text_nudge_y,
322
+ _u32(label_color), ")")
323
+ if modified:
324
+ reset_clicked, _ = _button(close_x + px(14), row_y, px(18.0),
325
+ px(16.0), delete_icon, tint,
326
+ icon_only=True)
327
+ if reset_clicked:
328
+ overrides.pop(override_key, None)
329
+ changed = True
330
+ request_render()
331
+ if tag and arg is None:
332
+ tag_width = imgui.calc_text_size(tag)[0]
333
+ draw_list.add_text(row_right - px(4) - tag_width,
334
+ row_y + text_nudge_y, _u32(dim_color), tag)
335
+ row_y += detail_row_height
336
+ if not cue_has_target(orchestration.cues[ordinal]):
337
+ continue
338
+ # ---- preconditions for this command (any cue with a target): one indented row per
339
+ # unmet precondition (outermost first - the order the solver
340
+ # takes them), the cheapest fix as its tag, a run chip that
341
+ # satisfies preconditions up to and including THIS one ----
342
+ Orchestrator._precondition_watch.add((key, ordinal))
343
+ preconditions = Orchestrator._preconditions.get((key, ordinal))
344
+ if preconditions is None:
345
+ continue # not listed yet (next sync)
346
+ if not preconditions:
347
+ if row_visible:
348
+ draw_list.add_text(row_left + precondition_indent, row_y + text_nudge_y,
349
+ _u32(dim_color), f"{check_icon} target hittable")
350
+ row_y += detail_row_height
351
+ continue
352
+ for precondition in preconditions:
353
+ if clip is None or not (row_y + detail_row_height < clip[1] or row_y > clip[3]):
354
+ runnable = precondition["kind"] not in ("unresolved", "error", "hittable")
355
+ run_pre = False
356
+ if runnable:
357
+ run_pre, _ = _button(row_left + precondition_indent - px(22), row_y,
358
+ px(18.0), px(16.0), play_icon, tint, icon_only=True)
359
+ if run_pre and not is_recording and not engine_busy:
360
+ from meltygui.core.automation.value_core import precondition_task
361
+ cue = orchestration.cues[ordinal]
362
+ path = tuple(cue_get(cue, "chain") or [cue_get(cue, "name") or "?"])
363
+ Orchestrator.submit(precondition_task(
364
+ path, precondition["key"], orchestration=orchestration,
365
+ gesture=cue_gesture(cue), press_frac=cue_press_frac(cue)))
366
+ changed = True
367
+ pre_color = (fail_color if precondition["kind"] in ("unresolved", "error")
368
+ else dim_color if precondition["kind"] == "hittable"
369
+ else _mix(cue_tint, row_text_value, 0.4, 1.2))
370
+ label_text = precondition["label"]
371
+ if precondition["kind"] == "hittable":
372
+ label_text = f"{check_icon} {label_text}"
373
+ draw_list.add_text(row_left + precondition_indent, row_y + text_nudge_y,
374
+ _u32(pre_color), label_text)
375
+ fixes = precondition.get("fixes") or []
376
+ if fixes:
377
+ fix_tag = f"fix: {fixes[0][1]}" + (f" (+{len(fixes) - 1})" if len(fixes) > 1 else "")
378
+ fix_width = imgui.calc_text_size(fix_tag)[0]
379
+ draw_list.add_text(row_right - px(4) - fix_width,
380
+ row_y + text_nudge_y, _u32(dim_color), fix_tag)
381
+ row_y += detail_row_height
382
+
383
+ for key, orchestration in list(orchestrations.items()):
384
+ row_rect = (row_left, row_y, row_right, row_y + row_height)
385
+ header_visible = clip is None or not (row_rect[3] < clip[1]
386
+ or row_rect[1] > clip[3])
387
+ tint = orchestration.tint if isinstance(orchestration.tint, tuple) else draw_state.tint
388
+ is_playing = Orchestrator.replaying is orchestration
389
+ take_failure = (Orchestrator.last_failure
390
+ if (Orchestrator.last_failure is not None
391
+ and Orchestrator.last_failure.orchestration is orchestration)
392
+ else None)
393
+ take_success = (Orchestrator.last_success
394
+ if (Orchestrator.last_success is not None
395
+ and Orchestrator.last_success.orchestration is orchestration
396
+ and take_failure is None)
397
+ else None)
398
+ is_take = Orchestrator.recording is orchestration
399
+ is_expanded = bool(expanded_map.get(key))
400
+ renaming = renaming_key == key
401
+ row_hovered = hover_ok and _in(row_rect, mouse_x, mouse_y)
402
+ chip_y = row_y + (row_height - px(22)) / 2.0
403
+ deleted = False
404
+
405
+ if header_visible:
406
+ bg_value = row_bg_value + (hover_bg_boost if row_hovered else 0.0)
407
+ # every row floats on a soft drop shadow; a succeeded take's
408
+ # row washes green (failure red wins - checked above)
409
+ add_shadow((row_rect[0], row_rect[1], row_right - row_left, row_height),
410
+ corner_radius=corner, clip=clip)
411
+ bg_tint = success_tint if take_success is not None else tint
412
+ draw_list.add_rect_filled(row_rect[0], row_rect[1], row_rect[2], row_rect[3],
413
+ _u32(_mix(bg_tint, bg_value + (0.03 if take_success else 0.0),
414
+ factor, saturation)),
415
+ rounding=corner)
416
+
417
+ # chevron (expand/collapse) + play chips
418
+ chevron_clicked, chip_edge = _button(
419
+ row_left + px(4), chip_y, chip_width, px(22),
420
+ chevron_down_icon if is_expanded else chevron_right_icon,
421
+ tint, icon_only=True)
422
+ if chevron_clicked:
423
+ if is_expanded:
424
+ expanded_map.pop(key, None)
425
+ else:
426
+ expanded_map[key] = True
427
+ is_expanded = not is_expanded
428
+ request_render()
429
+ # play = every command through change_value to its override or
430
+ # its original value (a verbatim replay had no button worth
431
+ # keeping — Lukas 09-01; `play(generalize=False)` still exists
432
+ # for the tests and tooling)
433
+ play_clicked, chip_edge = _button(chip_edge + px(2), chip_y, chip_width, px(22),
434
+ play_icon, tint, icon_only=True)
435
+ if play_clicked and not is_recording and not engine_busy:
436
+ Orchestrator.play(orchestration, generalize=True)
437
+ changed = True
438
+ # per-row record chip: re-record INTO this orchestration (events
439
+ # and cues replaced; the auto-name refreshes too, unless the
440
+ # name was set by hand). Shows stop while THIS take records.
441
+ record_chip_clicked, chip_edge = _button(
442
+ chip_edge + px(2), chip_y, chip_width, px(22),
443
+ stop_icon if is_take else record_icon,
444
+ record_tint if is_take else tint,
445
+ active=is_take, icon_only=True)
446
+ if record_chip_clicked:
447
+ if is_take:
448
+ Orchestrator.stop_recording()
449
+ changed = True
450
+ elif not is_recording and not engine_busy:
451
+ Orchestrator.start_recording(orchestration)
452
+ changed = True
453
+
454
+ # right side, outermost in: delete · restore · rename · tag
455
+ delete_left = row_right - px(4) - chip_width
456
+ if row_hovered and not is_take and not is_playing and not renaming:
457
+ delete_clicked, _ = _button(delete_left, chip_y, chip_width, px(22),
458
+ delete_icon, tint, icon_only=True)
459
+ if delete_clicked:
460
+ orchestrations.pop(key, None)
461
+ expanded_map.pop(key, None)
462
+ tab_map.pop(key, None)
463
+ changed = True
464
+ deleted = True
465
+ restore_left = delete_left - button_gap - chip_width
466
+ restore_tint = restore_on_tint if orchestration.restore_on_finish else tint
467
+ restore_clicked, _ = _button(restore_left, chip_y, chip_width, px(22),
468
+ restore_icon, restore_tint,
469
+ active=orchestration.restore_on_finish,
470
+ icon_only=True)
471
+ if restore_clicked:
472
+ orchestration.restore_on_finish = not orchestration.restore_on_finish
473
+ changed = True
474
+ rename_left = restore_left - button_gap - chip_width
475
+ if row_hovered and not renaming:
476
+ rename_clicked, _ = _button(rename_left, chip_y, chip_width, px(22),
477
+ rename_icon, tint, icon_only=True)
478
+ if rename_clicked:
479
+ _start_rename(key, orchestration)
480
+ renaming = True
481
+
482
+ if is_playing:
483
+ tag = f"{Orchestrator._replay_index}/{len(orchestration.events)}"
484
+ else:
485
+ override_count = len(getattr(orchestration, "overrides", None) or {})
486
+ tag = f"{orchestration.duration:g}s · {len(orchestration.events)} ev" \
487
+ + (f" · {override_count} edited" if override_count else "")
488
+ if take_failure is not None:
489
+ tag += " · failed"
490
+ elif take_success is not None:
491
+ tag += " · ok"
492
+ tag_width = imgui.calc_text_size(tag)[0]
493
+ tag_left = rename_left - button_gap - tag_width
494
+ if is_playing:
495
+ tag_color = _mix(tint, dim_text_value, factor, text_saturation)
496
+ elif take_failure is not None:
497
+ tag_color = fail_color
498
+ elif take_success is not None:
499
+ tag_color = success_color
500
+ else:
501
+ tag_color = _mix(tint, dim_text_value, factor, text_saturation)
502
+ draw_list.add_text(tag_left,
503
+ row_y + (row_height - imgui.get_text_line_height()) / 2.0
504
+ + text_nudge_y, _u32(tag_color), tag)
505
+
506
+ # name: inline rename input, or the label (double-click to rename)
507
+ name_left = chip_edge + px(8)
508
+ if deleted:
509
+ pass
510
+ elif renaming:
511
+ imgui.set_cursor_screen_pos((name_left, row_y + px(3)))
512
+ imgui.push_item_width(max(px(90), min(px(240),
513
+ tag_left - name_left - px(10))))
514
+ if panel_state is not None and getattr(panel_state, "_rename_focus", False):
515
+ imgui.set_keyboard_focus_here()
516
+ panel_state._rename_focus = False
517
+ entered, text = imgui.input_text(f"##orch-rename-{key}",
518
+ draw_state.rename_text or "", 128,
519
+ imgui.INPUT_TEXT_ENTER_RETURNS_TRUE)
520
+ imgui.pop_item_width()
521
+ draw_state.rename_text = text
522
+ item_active = imgui.is_item_active()
523
+ was_active = (getattr(panel_state, "_rename_was_active", False)
524
+ if panel_state is not None else True)
525
+ if entered or (was_active and not item_active):
526
+ changed = _commit_rename(orchestration) or changed
527
+ elif panel_state is not None:
528
+ panel_state._rename_was_active = item_active
529
+ else:
530
+ text_value = row_text_value + (hover_text_boost if row_hovered else 0.0)
531
+ name_color = _mix((record_tint if is_take else tint), text_value,
532
+ factor, text_saturation)
533
+ label = orchestration.name + (" (recording)" if is_take else "")
534
+ draw_list.add_text(name_left,
535
+ row_y + (row_height - imgui.get_text_line_height()) / 2.0
536
+ + text_nudge_y, _u32(name_color), label)
537
+ if dbl is not None and _in((name_left, row_y, tag_left, row_y + row_height),
538
+ dbl[0], dbl[1]):
539
+ _start_rename(key, orchestration)
540
+
541
+ row_y += row_stride
542
+ if deleted:
543
+ continue
544
+
545
+ # ---- expanded detail: Commands (generalized) | Events (raw) ----
546
+ if is_expanded:
547
+ current_tab = tab_map.get(key, "commands")
548
+ tab_x = row_left + detail_indent
549
+ for tab_key, tab_label in (("commands", "Commands"), ("events", "Events")):
550
+ width = imgui.calc_text_size(tab_label)[0] + 2 * tab_pad_x
551
+ tab_clicked, tab_edge = _button(tab_x, row_y, width, tab_height,
552
+ tab_label, tint,
553
+ active=current_tab == tab_key)
554
+ if tab_clicked and current_tab != tab_key:
555
+ tab_map[key] = tab_key
556
+ current_tab = tab_key
557
+ request_render()
558
+ tab_x = tab_edge + button_gap
559
+ row_y += tab_height + px(4)
560
+ fail_at = None
561
+ if take_failure is not None:
562
+ if (take_failure.cue_index is not None
563
+ and take_failure.cue_index < len(orchestration.cues)):
564
+ fail_at = cue_get(orchestration.cues[take_failure.cue_index],
565
+ "at", 0)
566
+ else:
567
+ fail_at = take_failure.event_index
568
+ if current_tab == "events":
569
+ detail = group_events(orchestration.events, orchestration.cues)
570
+ if not detail:
571
+ detail = [(0, 0, "", "no events yet — press Record", "")]
572
+ _detail_rows(detail, is_playing, fail_at=fail_at)
573
+ else:
574
+ detail = generalized_commands(orchestration)
575
+ if detail:
576
+ _command_rows(key, orchestration, tint, detail, is_playing, None,
577
+ fail_cue_index=(take_failure.cue_index
578
+ if take_failure is not None else None))
579
+ else:
580
+ _detail_rows([(0, 0, "", "no commands yet — effects appear "
581
+ "as recording touches the undo stacks", "")],
582
+ False)
583
+ row_y += px(4)
584
+
585
+ if not orchestrations:
586
+ empty_color = _mix(draw_state.tint, dim_text_value, factor, text_saturation)
587
+ draw_list.add_text(row_left + px(4), rows_top + px(4), _u32(empty_color),
588
+ "No orchestrations — press Record to capture one.")
589
+ row_y += row_stride
590
+
591
+ # ---- status footer ----
592
+ if Orchestrator.status:
593
+ status_color = _mix(record_tint if is_recording else draw_state.tint,
594
+ row_text_value, factor, text_saturation)
595
+ draw_list.add_text(row_left + px(4), row_y + px(2), _u32(status_color),
596
+ Orchestrator.status)
597
+ row_y += status_height
598
+
599
+ # ---- error bar: the full reason for the most recent failure ----
600
+ failure = Orchestrator.last_failure
601
+ if failure is not None:
602
+ bar_top = row_y + px(4)
603
+ bar_line_height = imgui.get_text_line_height()
604
+ bar_pad_y = px(5.0)
605
+ dismiss_reserve = px(4) + px(18.0) + button_gap
606
+ message_wrap_width = max(px(60), (row_right - row_left) - px(16) - dismiss_reserve)
607
+ failed_name = str(getattr(failure.orchestration, "name", None) or "task")
608
+ where = ""
609
+ if failure.cue_index is not None:
610
+ where = f" (command {failure.cue_index + 1})"
611
+ elif failure.event_index is not None:
612
+ where = f" (event {failure.event_index})"
613
+ message = f"{failed_name}{where}: {failure.reason}"
614
+ message_lines = _wrap_text(message, message_wrap_width)
615
+ bar_height = max(px(24.0), bar_pad_y * 2 + bar_line_height * len(message_lines))
616
+ add_shadow((row_left, bar_top, row_right - row_left, bar_height),
617
+ corner_radius=corner, clip=clip)
618
+ draw_list.add_rect_filled(row_left, bar_top, row_right, bar_top + bar_height,
619
+ _u32(_mix(fail_tint, 0.045, 0.55, 1.1)),
620
+ rounding=corner)
621
+ message_rect = (row_left, bar_top, row_right - dismiss_reserve, bar_top + bar_height)
622
+ message_hovered = hover_ok and _in(message_rect, mouse_x, mouse_y)
623
+ # click anywhere on the message copies it (whole line, not just the
624
+ # visible part); "copied" shows in place for a beat
625
+ just_copied = (getattr(failure, "copied_at", None) is not None
626
+ and time.time() - failure.copied_at < 1.2)
627
+ if click is not None and _in(message_rect, click[0], click[1]):
628
+ try:
629
+ imgui.set_clipboard_text(failure_report(failure))
630
+ except Exception:
631
+ pass
632
+ failure.copied_at = time.time()
633
+ just_copied = True
634
+ request_render()
635
+ if message_hovered:
636
+ draw_list.add_rect_filled(message_rect[0], message_rect[1], message_rect[2],
637
+ message_rect[3], _u32(_mix(fail_tint, 0.09, 0.55, 1.1)),
638
+ rounding=corner)
639
+ # the whole reason, WRAPPED to the bar (a solver failure lists every
640
+ # fix it tried - one line clipped the part that mattered)
641
+ shown_lines = (["debug report copied to clipboard"] if just_copied else message_lines)
642
+ for line_index, line in enumerate(shown_lines):
643
+ draw_list.add_text(row_left + px(8),
644
+ bar_top + bar_pad_y + line_index * bar_line_height + text_nudge_y,
645
+ _u32(success_color if just_copied else fail_color), line)
646
+ if just_copied:
647
+ request_render() # let the confirmation fade
648
+ dismiss_clicked, _ = _button(row_right - px(4) - px(18.0),
649
+ bar_top + (bar_height - px(16.0)) / 2.0,
650
+ px(18.0), px(16.0), delete_icon, fail_tint,
651
+ icon_only=True)
652
+ if dismiss_clicked:
653
+ Orchestrator.last_failure = None
654
+ request_render()
655
+ row_y = bar_top + bar_height + px(2)
656
+
657
+ imgui.dummy(content_width, max(1.0, (row_y - origin_y) + row_gap))
658
+ return changed, input_value