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,1752 @@
1
+ """change_value(path, to): drive a UI value to a target through REAL input
2
+ events, generalized from recorded demonstrations.
3
+
4
+ change_value(("33071d", "alpha"), 7.5)
5
+ change_value("Lora Window B/[1]/lora_dropout", 0.2)
6
+ change_value(("990001", "name"), "some random new text")
7
+
8
+ The path is a selector (selectors.py) — leaf-anchored, nesting-free. The
9
+ target's EDITOR KIND (the cue's applicability signature) picks an archetype
10
+ executor; the recorded take of that kind supplies the press point as a
11
+ fraction of the demonstrated leaf's rect, re-applied to the resolved
12
+ target's live rect — which is what makes one alpha demonstration drive
13
+ lora_dropout, or any float field anywhere.
14
+
15
+ Archetypes:
16
+ drag (draw_float / draw_int) — press, PROBE a few px to measure the
17
+ live gain from the coalescing undo Change (the held drag updates
18
+ one Change per frame: the undo stack is the sensor), then a
19
+ secant servo to the target. Recorded gain is never trusted;
20
+ per-field speed=, clamps and nonlinearity are absorbed by
21
+ re-measuring every step. A stall (clamp) releases and aborts.
22
+ text (draw_str / draw_text) — click to focus, synthesized clear
23
+ (select-all + delete, End+backspace fallback), payload typed from
24
+ `to` in the event SHAPE a real keystroke produces (key + char),
25
+ exact-match verification with one clear-and-retype retry.
26
+ toggle (draw_bool) — click unless already at the target.
27
+
28
+ Preconditions are a small SOLVER (see `PRECONDITIONS` below): before the
29
+ executor runs, the target must be hittable at its press point. The unmet
30
+ precondition found first along the containment path — a closed window, a
31
+ collapsed ancestor, the target outside its scroll viewport, the press
32
+ point covered by another window — names the EFFECT KINDS that satisfy it;
33
+ every fix's MECHANISM is discovered from recorded takes (an effect cue of
34
+ that kind + the press offset relative to the view it happened on:
35
+ "expand", "raise", "scroll", a WindowMoveChange's header press), never
36
+ coded here, and generalizes to any subject of the same kind by that
37
+ rect-relative offset. Ranking is one number — the disturbance cost table
38
+ `Toggles.Orchestrator.fix_costs` (re-pick the press point 0, raise 1,
39
+ scroll / expand 2, move a window 3, +1 for the cue's anchor window) — the
40
+ cheapest applicable fix runs first, the predicate is re-checked (verify by
41
+ effect, never by assumption), the next candidate runs when it did not
42
+ help. Every attempt is logged into the abort message. A closed WINDOW (the
43
+ dock-row lookup is deferred) or a missing demonstration aborts with the
44
+ unmet need named.
45
+
46
+ Execution: a ValueTask generator stepped once per frame by
47
+ Orchestrator.pump (real input muted, Esc aborts, same contract as replay).
48
+ `task = change_value(...)` returns immediately; task.wait() blocks for test
49
+ harnesses that drive frames themselves.
50
+ """
51
+ import threading
52
+ import time
53
+
54
+ import meltygui.core.windowing.window_api as glfw
55
+
56
+ from meltygui.core.melty import Melty
57
+ from meltygui.core.runtime.toggles import Toggles
58
+ from meltygui.state.core_undo import UndoManager
59
+ from meltygui.core.automation.orchestration_core import Orchestrator
60
+ from meltygui.core.automation.orchestration_core import cue_get
61
+ from meltygui.core.automation.selector_core import resolve
62
+ from meltygui.core.automation.selector_core import parse
63
+ from meltygui.core.automation.selector_core import format_path
64
+ from meltygui.core.automation.selector_core import ancestor_chain
65
+ from meltygui.core.automation.selector_core import display_name
66
+ from meltygui.core.automation.selector_core import full_name
67
+ from meltygui.core.automation.selector_core import NoMatch
68
+ from meltygui.core.automation.selector_core import Ambiguous
69
+
70
+ # editor kind -> archetype. A take demonstrated on ANY field of a kind
71
+ # drives every field of that kind; add a row here when a new leaf editor
72
+ # gets a up.
73
+ _ARCHETYPE_BY_EDITOR = {"draw_float": "drag", "draw_int": "drag",
74
+ "draw_str": "text", "draw_text": "text",
75
+ "draw_bool": "toggle"}
76
+
77
+ # Archetypes that MUST have a recorded demonstration (the take carries the
78
+ # press fraction and, for text, the event shape). "toggle" degenerates to a
79
+ # centered click and runs take-less.
80
+ _NEEDS_TAKE = {"drag", "text"}
81
+
82
+
83
+ class _Abort(Exception):
84
+ """Raised inside a task generator to stop with an honest message."""
85
+
86
+
87
+ class _PreconditionDone(Exception):
88
+ """A precondition-only run (ValueTask.until) reached its precondition."""
89
+
90
+
91
+ def change_value(path, to, within=None, universe=None, eps=None):
92
+ """Drive the value at `path` to `to` with real input. Returns the
93
+ ValueTask immediately; the engine pumps it per frame. task.wait() for
94
+ blocking callers, task.error for the outcome."""
95
+ return Orchestrator.submit(ValueTask(path, to, within=within,
96
+ universe=universe, eps=eps))
97
+
98
+
99
+ # ── the library: takes indexed by what they demonstrate ──────────────────
100
+
101
+ def orchestration_store():
102
+ root = getattr(getattr(Melty, "vis", None), "root", None)
103
+ store = getattr(root, "orchestrations", None)
104
+ return getattr(store, "orchestrations", None) or {}
105
+
106
+
107
+ def terminal_cue(take):
108
+ """The take's last edit-stack Change cue — its demonstrated effect."""
109
+ for cue in reversed(getattr(take, "cues", []) or []):
110
+ if cue_get(cue, "kind") == "Change":
111
+ return cue
112
+ return None
113
+
114
+
115
+ def take_for(archetype):
116
+ """A recorded take whose terminal cue's editor maps to `archetype` —
117
+ one demonstration per widget kind covers the whole UI."""
118
+ for take in orchestration_store().values():
119
+ cue = terminal_cue(take)
120
+ if cue is not None and _ARCHETYPE_BY_EDITOR.get(cue_get(cue, "editor")) == archetype:
121
+ return take
122
+ return None
123
+
124
+
125
+ def effect_offset(kind, editor=None):
126
+ """How a demonstration produced effect `kind` ("expand", "raise",
127
+ "scroll", …): the press offset from the top-left of the view the
128
+ effect happened on (the effect cue's press_offset). The control sits
129
+ at the same offset on every subject of that kind — the expand arrow on
130
+ every collection header, the title on every window header — so ONE
131
+ demonstration serves any subject OF THAT KIND: with `editor` (the
132
+ subject's view function) a demonstration from the same editor wins
133
+ (a collapsed WINDOW's chevron is not where a collection's is — the
134
+ collection's offset read as "clipped" on a 32 px window header, 09-01),
135
+ any other demonstration of the effect is the fallback. A take recorded
136
+ before press_offset existed still carries press_frac + leaf_rect (the
137
+ press as a fraction of the subject's rect, and that rect's size):
138
+ frac × size IS the pixel offset — without this, nested gates whose name
139
+ no recorded take matched aborted with "no expand demonstration" (09-01)."""
140
+ matched = fallback = derived = None
141
+ for take in orchestration_store().values():
142
+ for cue in getattr(take, "cues", []) or []:
143
+ if cue_get(cue, "kind") != kind:
144
+ continue
145
+ offset = cue_get(cue, "press_offset")
146
+ if not offset:
147
+ frac, rect = cue_get(cue, "press_frac"), cue_get(cue, "leaf_rect")
148
+ if derived is None and frac and rect and rect[2] > 0 and rect[3] > 0:
149
+ derived = (float(frac[0]) * float(rect[2]),
150
+ float(frac[1]) * float(rect[3]))
151
+ continue
152
+ offset = (float(offset[0]), float(offset[1]))
153
+ if editor is not None and cue_get(cue, "editor") == editor:
154
+ if matched is None:
155
+ matched = offset
156
+ elif fallback is None:
157
+ fallback = offset
158
+ if matched is not None:
159
+ return matched
160
+ if fallback is not None:
161
+ return fallback
162
+ return derived
163
+
164
+
165
+ def expand_offset(editor=None):
166
+ return effect_offset("expand", editor=editor)
167
+
168
+
169
+ def move_offset():
170
+ """Where a recorded window move grabbed its window: the WindowMoveChange
171
+ cue anchors on the moved window at its PRE-drag corner and the take's
172
+ events tagged with that cue are relative to it, so the tagged `down`
173
+ event IS the header press offset. One recorded move teaches how to
174
+ grab any window; the drag delta is computed per use (gain is 1 px/px,
175
+ no servo). None when no move was recorded."""
176
+ for take in orchestration_store().values():
177
+ cues = getattr(take, "cues", []) or []
178
+ for index, cue in enumerate(cues):
179
+ if cue_get(cue, "kind") != "WindowMoveChange":
180
+ continue
181
+ for event in getattr(take, "events", []) or []:
182
+ if (len(event) > 5 and event[1] == "down" and event[2] == "left_mouse"
183
+ and event[5] == index):
184
+ return (float(event[3]), float(event[4]))
185
+ return None
186
+
187
+
188
+ def raise_offset():
189
+ """A raise demonstration's press offset on its window — a recorded
190
+ raise effect first, else the header press of a recorded move (pressing
191
+ a header raises the window too)."""
192
+ return effect_offset("raise") or move_offset()
193
+
194
+
195
+ def fragment_for_gate(gate_name=None):
196
+ """Fallback for demonstrations recorded before press_offset: a take
197
+ whose expand effect names THIS gate (a fragment for a different
198
+ collection clicks the wrong place), else the legacy bool-Change shape."""
199
+ candidates = []
200
+ for take in orchestration_store().values():
201
+ for cue in reversed(getattr(take, "cues", []) or []):
202
+ kind = cue_get(cue, "kind")
203
+ if kind == "expand":
204
+ candidates.append(("exact" if cue_get(cue, "name") == gate_name else "other",
205
+ take))
206
+ break
207
+ if kind == "Change":
208
+ if (cue_get(cue, "value_type") == "bool"
209
+ and cue_get(cue, "new") in (True, "True")
210
+ and cue_get(cue, "editor") not in _ARCHETYPE_BY_EDITOR):
211
+ candidates.append(("legacy", take))
212
+ break # a leaf-edit take, not an expand take
213
+ for wanted in ("exact", "legacy"): # never an expand fragment for ANOTHER gate
214
+ for grade, take in candidates:
215
+ if grade == wanted:
216
+ return take
217
+ return None
218
+
219
+
220
+ # ── live-tree helpers (module-level so tests can monkeypatch) ────────────
221
+
222
+ def _editor_of(ds):
223
+ return getattr(getattr(ds, "_view_func", None), "__name__", None)
224
+
225
+
226
+ def _current_value(ds):
227
+ return getattr(ds, "_raw_input_value", None)
228
+
229
+
230
+ def _live_rect(ds):
231
+ return (getattr(ds, "abs_left", 0) or 0, getattr(ds, "abs_top", 0) or 0,
232
+ getattr(ds, "width", 0) or 0, getattr(ds, "height", 0) or 0)
233
+
234
+
235
+ def _reachable(ds):
236
+ """The one universal guard: is this target actually hittable? The
237
+ STRUCTURAL check is authoritative — a closed window or a collapsed
238
+ ancestor makes the target unreachable whatever else says (a collapsed
239
+ subtree leaves stale hit boxes in the BVH and a stale rect on the
240
+ draw_state, which once passed a BVH-first check and pressed into
241
+ nothing). Geometry must be present too; BVH membership adds nothing
242
+ beyond that."""
243
+ for node in [ds] + ancestor_chain(ds):
244
+ if getattr(node, "closed", False):
245
+ return False
246
+ if node is not ds and getattr(node, "expanded", True) is False:
247
+ return False
248
+ left, top, width, height = _live_rect(ds)
249
+ return width > 0 and height > 0
250
+
251
+
252
+ def resolve_recorded(path, within=None, universe=None):
253
+ """Resolve a RECORDED chain by its minimal unique suffix (capture
254
+ maximal, match minimal): a cue's full chain reaches up to the root
255
+ ("Main Window"), which is never a cached tile and so never resolves as
256
+ a segment — the full chain as a path is only as resolvable as its most
257
+ fragile ancestor name. Try suffixes shortest→longest; the first that
258
+ resolves uniquely wins (Ambiguous keeps extending). Returns (ds, suffix
259
+ tried last); raises NoMatch carrying the deepest resolvable prefix so
260
+ the abort names what DID resolve."""
261
+ segments = parse(path)
262
+ last_error = None
263
+ for length in range(1, len(segments) + 1):
264
+ suffix = segments[-length:]
265
+ try:
266
+ return resolve(suffix, within=within, universe=universe), suffix
267
+ except Ambiguous as ambiguous:
268
+ last_error = ambiguous
269
+ continue
270
+ except NoMatch as missing:
271
+ last_error = missing
272
+ break
273
+ if isinstance(last_error, Ambiguous):
274
+ raise last_error
275
+ raise NoMatch(segments, hint=f"leaf {format_path(segments[-1:])} not live")
276
+
277
+
278
+ # ── preconditions ─────────────────────────────────────────────────────────
279
+ #
280
+ # Each precondition: predicate → (kind, node). `PRECONDITIONS` lists the
281
+ # fix kinds that satisfy each kind; the fixes' mechanisms come from takes
282
+ # (effect_offset / move_offset), the ranking from Toggles.Orchestrator.fix_costs.
283
+
284
+ PRECONDITIONS = {
285
+ "closed": ("open",), # a window in the chain is closed (note: deferred)
286
+ "collapsed": ("expand",), # an ancestor collection is folded
287
+ "outside": ("scroll",), # the target is scrolled out of its viewport
288
+ "clipped": ("re_pick", "scroll"), # the press point is clipped, part of the view shows
289
+ "obscured": ("re_pick", "raise", "move"), # another window covers the press point
290
+ }
291
+
292
+
293
+ def _window_of(ds):
294
+ node, steps = ds, 0
295
+ while node is not None and steps < 64:
296
+ parent = getattr(node, "parent_window", None)
297
+ if parent is None or parent is node:
298
+ return node
299
+ node, steps = parent, steps + 1
300
+ return node
301
+
302
+
303
+ def _hits_at(x, y):
304
+ """Every live view under (x, y), front first — the same hit test the
305
+ input handler presses into (tests substitute a fake)."""
306
+ try:
307
+ return list(Melty.bvh_query(x, y))
308
+ except Exception:
309
+ return []
310
+
311
+
312
+ def _visible_rect(ds):
313
+ """The target's rect ∩ its clip (what can actually be pressed); empty
314
+ when scrolled out of its viewport."""
315
+ left, top, width, height = _live_rect(ds)
316
+ rect = (left, top, left + width, top + height)
317
+ clip = getattr(ds, "abs_clip_rect", None)
318
+ if clip is None:
319
+ return rect
320
+ return (max(rect[0], clip[0]), max(rect[1], clip[1]),
321
+ min(rect[2], clip[2]), min(rect[3], clip[3]))
322
+
323
+
324
+ def _front_window_at(x, y):
325
+ """The frontmost meltygui window under (x, y). Read from the renderer's
326
+ own paint order (`Melty.paint_ordered_ds`, back → front, rebuilt from
327
+ live state every frame) — NOT the BVH: a blit-cached window that was
328
+ raised or moved over the target keeps its hit boxes / z stamp until
329
+ its wrapper next runs, so the BVH ranked the covered field in front of
330
+ the window covering it and the press went nowhere (a nudge of the
331
+ obscurer "fixed" it, 09-01). The BVH hit test stays the fallback for
332
+ a world without a paint order (tests)."""
333
+ ordered = getattr(Melty, "paint_ordered_ds", None) or []
334
+ for window in reversed(ordered):
335
+ if _window_hidden(window):
336
+ continue
337
+ left, top, right, bottom = _visible_rect(window) # what is DRAWN, not the raw width
338
+ if left <= x <= right and top <= y <= bottom:
339
+ return window
340
+ hits = _hits_at(x, y)
341
+ return _window_of(hits[0]) if hits else None
342
+
343
+
344
+ def _window_hidden(window):
345
+ """A window that paints nothing: closed, culled off-screen, or nested
346
+ inside a closed / COLLAPSED ancestor. A collapsed window itself is NOT
347
+ hidden — its header strip (the expand chevron!) is drawn and hittable
348
+ at its live rect. `abs_closed` reads True for a collapsed window, so
349
+ it must not be the test here: with it, a collapsed 'Loras' was skipped
350
+ by the obscurer scan, the root window won the point under its own
351
+ chevron and the expand fix reported "covered by 'Main Window'" for an
352
+ uncovered button (Lukas 09-01)."""
353
+ node, steps = window, 0
354
+ while node is not None and steps < 64:
355
+ if getattr(node, "closed", False) and getattr(node, "closable", True):
356
+ return True
357
+ if getattr(node, "_hidden_offscreen", False):
358
+ return True
359
+ parent = getattr(node, "parent_window", None)
360
+ if parent is None or parent is node:
361
+ return False
362
+ if getattr(parent, "expanded", True) is False:
363
+ return True # inside a collapsed window: not drawn
364
+ node, steps = parent, steps + 1
365
+ return False
366
+
367
+
368
+ def _window_chain(ds):
369
+ """Every window the target sits in, nearest first, up to its root."""
370
+ chain, node, steps = [], getattr(ds, "parent_window", None), 0
371
+ while node is not None and steps < 64:
372
+ chain.append(node)
373
+ parent = getattr(node, "parent_window", None)
374
+ if parent is None or parent is node:
375
+ break
376
+ node, steps = parent, steps + 1
377
+ return chain or [ds]
378
+
379
+
380
+ def _first_unmet(ds, press_point, header=False):
381
+ """The outermost unmet precondition on the way to `ds` at `press_point`:
382
+ (kind, node) — the node is what the fix acts on (the closed window, the
383
+ folded collection, the target for "outside", the OBSCURING window) —
384
+ or None when the target is hittable there. `header`: the press is a
385
+ header gesture on `ds` itself (raise / move) — a collapsed window with
386
+ no grabbable header is then its OWN collapsed gate (never for an
387
+ expand click, whose control is that very header)."""
388
+ nodes = [ds] + ancestor_chain(ds)
389
+ for node in reversed(nodes): # outermost first
390
+ if getattr(node, "closed", False):
391
+ return ("closed", node)
392
+ if node is not ds and getattr(node, "expanded", True) is False:
393
+ return ("collapsed", node)
394
+ if header and getattr(ds, "expanded", True) is False and not _header_grabbable(ds):
395
+ return ("collapsed", ds)
396
+ left, top, right, bottom = _visible_rect(ds)
397
+ if right - left <= 0 or bottom - top <= 0:
398
+ return ("outside", ds)
399
+ x, y = press_point
400
+ if not (left <= x <= right and top <= y <= bottom):
401
+ return ("clipped", ds) # the press point is clipped
402
+ front = _front_window_at(x, y)
403
+ if front is not None and front is not ds and front not in _window_chain(ds) \
404
+ and front is not _window_of(ds):
405
+ return ("obscured", front)
406
+ return None
407
+
408
+
409
+ def _scroll_container(ds):
410
+ """The nearest ancestor that scrolls (a scroll range, or a scroll offset
411
+ field), else the target's window."""
412
+ for node in ancestor_chain(ds):
413
+ if (getattr(node, "_max_scroll_y", 0) or 0) > 0:
414
+ return node
415
+ if getattr(node, "scroll_offset", None) not in (None, (0, 0)):
416
+ return node
417
+ return _window_of(ds)
418
+
419
+
420
+ def _escape_delta(rect, point, margin):
421
+ """The smallest (dx, dy) moving `rect` so it no longer contains
422
+ `point` (with `margin`); the negation moves the POINT out of the rect."""
423
+ left, top, right, bottom = rect
424
+ x, y = point
425
+ options = [(x + margin - left, 0.0), (x - margin - right, 0.0),
426
+ (0.0, y + margin - top), (0.0, y - margin - bottom)]
427
+ return min(options, key=lambda d: abs(d[0]) + abs(d[1]))
428
+
429
+
430
+ def _escape_rect_delta(rect, target, margin):
431
+ """The smallest (dx, dy) moving `rect` so it no longer overlaps the
432
+ whole `target` rect (plus `margin`) — a move uncovers the CONTROL, not
433
+ just one point on it: a point-sized escape left a 12 px sliver of the
434
+ header free and the next re-check found the rest still covered, the
435
+ windows nudged back and forth and the task gave up (Lukas 09-01: "drag
436
+ windows a little further")."""
437
+ left, top, right, bottom = rect
438
+ t_left, t_top, t_right, t_bottom = target
439
+ options = [(t_right + margin - left, 0.0), (t_left - margin - right, 0.0),
440
+ (0.0, t_bottom + margin - top), (0.0, t_top - margin - bottom)]
441
+ return min(options, key=lambda d: abs(d[0]) + abs(d[1]))
442
+
443
+
444
+ def _fix_cost(name, subject_ds, ds):
445
+ costs = Toggles.Orchestrator.fix_costs
446
+ cost = costs.get(name, 99)
447
+ if name == "move" and subject_ds is _window_of(ds):
448
+ cost += costs.get("anchor_window_penalty", 1) # the cues' frame moves with it
449
+ return cost
450
+
451
+
452
+ # ── fix mechanisms (generators; each yields once) ───────────────────────
453
+
454
+ def _header_rect(window_ds):
455
+ """The strip a header press may land in (re-picks for a window subject
456
+ stay inside it — pressing the body would not raise-by-header / move):
457
+ the header between the chevron margin and the buttons margin. Read
458
+ off the window's VISIBLE rect (rect ∩ clip), never its raw width: a
459
+ collapsed window's `width` read 224 while 100 px of header were drawn
460
+ and hittable, so the "safe" centre landed in empty space right of it
461
+ (Lukas 09-01)."""
462
+ left, top, right, _bottom = _visible_rect(window_ds)
463
+ width = max(0.0, right - left)
464
+ safe_left = min(Toggles.Orchestrator.header_safe_left_px, max(4.0, width / 2.0))
465
+ safe_right = max(safe_left + 1.0, width - Toggles.Orchestrator.header_safe_right_px)
466
+ return (left + safe_left, top + 2.0, left + safe_right,
467
+ top + Toggles.Orchestrator.header_height_px)
468
+
469
+
470
+ def _header_grabbable(window_ds):
471
+ """Whether the window's visible header has ROOM for a press between
472
+ the chevron margin and the buttons margin. A collapsed window shrunk
473
+ to its buttons has none — a header press there hits a button or
474
+ nothing, so for a header gesture it counts as a collapsed gate: expand
475
+ it first (Lukas 09-01, the move on a collapsed Loras)."""
476
+ left, _top, right, _bottom = _visible_rect(window_ds)
477
+ return (right - left) >= (Toggles.Orchestrator.header_safe_left_px
478
+ + Toggles.Orchestrator.header_safe_right_px)
479
+
480
+
481
+ def _header_point(window_ds, offset):
482
+ """A press point on `window_ds` for a header press (raise / move): the
483
+ CENTER of the header's safe strip. The demonstrated offset is not used
484
+ for position — where that user grabbed THAT window means nothing here:
485
+ its x landed on the collapse chevron once (folding the window instead
486
+ of raising it), its y a pixel above the visible top edge, which sits
487
+ inside abs_top (Lukas 09-01). The demo still proves the mechanism."""
488
+ left, top, right, bottom = _header_rect(window_ds)
489
+ return (left + right) / 2.0, (top + bottom) / 2.0
490
+
491
+
492
+ def _point_of(point):
493
+ """A gesture point is a (x, y) tuple or a CALLABLE returning one — the
494
+ live form is re-read every pump of the approach and once more at the
495
+ press, so a target that moves while the cursor travels (the third
496
+ expand of a nested set: the second's reflow shifts it, 09-01) is
497
+ still hit where it IS, not where it was when the gesture was planned."""
498
+ return point() if callable(point) else point
499
+
500
+
501
+ def _tracking(x, y, anchor):
502
+ """A live point that keeps the offset of (x, y) — a re-picked press spot
503
+ _satisfy settled on — from the live `anchor` point, so a target that
504
+ moves carries the re-pick along."""
505
+ ax, ay = anchor()
506
+ dx, dy = x - ax, y - ay
507
+ return lambda: (anchor()[0] + dx, anchor()[1] + dy)
508
+
509
+
510
+ def _click_at(point, ds=None):
511
+ """A click on `ds` (a header's imgui arrow_button, a flat_button). imgui
512
+ reacts only to an item it SUBMITTED that frame: a button activates on
513
+ the press frame and fires on the release frame, and drops between if
514
+ it misses one — so the target's tile is forced live for the press, the
515
+ frame between and the release (Melty skips the hover invalidate on
516
+ press frames, and a blit-served tile never sees the click while the
517
+ handler's own subscriptions still do: the "flaky" gate clicks, 09-01)."""
518
+ yield from _settle_at(point, ds)
519
+ x, y = _point_of(point)
520
+ _press(x, y, ds)
521
+ yield
522
+ _render_live(ds)
523
+ yield
524
+ _release(x, y, ds)
525
+ yield
526
+
527
+
528
+ def _drag_by(point, dx, dy, ds=None):
529
+ yield from _settle_at(point, ds)
530
+ x, y = _point_of(point)
531
+ _press(x, y, ds)
532
+ yield
533
+ duration = glide_seconds(abs(dx) + abs(dy))
534
+ started = time.monotonic()
535
+ while True:
536
+ t = min(1.0, (time.monotonic() - started) / duration)
537
+ _move(x + dx * t, y + dy * t)
538
+ yield
539
+ if t >= 1.0:
540
+ break
541
+ _release(x + dx, y + dy, ds)
542
+ yield
543
+
544
+
545
+ def _gate_press_rect(point):
546
+ """Where a re-pick may land for a gate's control: the expand chevron is
547
+ a few px wide, so a re-pick must stay ON it — anywhere else on the
548
+ header row is not the control. A press point that is covered inside
549
+ this radius has no re-pick and falls through to raise / move."""
550
+ radius = Toggles.Orchestrator.gate_hit_radius_px
551
+ x, y = point
552
+ return (x - radius, y - radius, x + radius, y + radius)
553
+
554
+
555
+ def _fragment_press_point(fragment, window_ds):
556
+ """The screen point a gate fragment's first press lands on once its
557
+ anchors are rebound to `window_ds` (what _replay_fragment does), or
558
+ None for a fragment without a press."""
559
+ origin = None
560
+ if window_ds is not None:
561
+ left = getattr(window_ds, "abs_left", None)
562
+ top = getattr(window_ds, "abs_top", None)
563
+ if left is not None and top is not None:
564
+ origin = (float(left), float(top))
565
+ for event in getattr(fragment, "events", []) or []:
566
+ if event[1] == "down" and event[2] == "left_mouse":
567
+ x, y = float(event[3]), float(event[4])
568
+ if len(event) > 5 and origin is not None:
569
+ return (x + origin[0], y + origin[1])
570
+ return (x, y)
571
+ return None
572
+
573
+
574
+ def _fix_expand(task, node, ds):
575
+ """Open a collapsed collection by pressing its expand control. The
576
+ control's press point is a SUB-GOAL under the same rules as the target
577
+ (preconditions apply recursively — Lukas 09-01: a window sitting over
578
+ the collapse button is answered by a raise of the gate's window or a
579
+ move of the obscurer BEFORE the click, exactly as an obscurer's own
580
+ covered header is for a move), so a gate the user could not click
581
+ either is opened the way the user would open it."""
582
+ offset = expand_offset(editor=_editor_of(node))
583
+ if offset is not None:
584
+ def live_point():
585
+ left, top, _w, _h = _live_rect(node)
586
+ return left + offset[0], top + offset[1]
587
+ # a click that did not open the gate gets ONE more go after the
588
+ # control's press is re-satisfied (a cover that slid back, a
589
+ # tile that missed the press frame)
590
+ for attempt in range(2):
591
+ x, y = yield from task._satisfy(node, live_point, depth=task._depth + 1,
592
+ rect=lambda: _gate_press_rect(live_point()))
593
+ yield from _click_at(_tracking(x, y, live_point), node)
594
+ for _ in range(Toggles.Orchestrator.cue_wait_frames):
595
+ if getattr(node, "expanded", True) is not False:
596
+ return
597
+ yield
598
+ else:
599
+ fragment = fragment_for_gate(display_name(node))
600
+ if fragment is None:
601
+ raise _Abort(f"'{display_name(node)}' is collapsed and no "
602
+ f"expand demonstration is recorded")
603
+ window_ds = getattr(node, "parent_window", None)
604
+ if _fragment_press_point(fragment, window_ds) is not None:
605
+ # the fragment replays relative to its window's LIVE origin, so
606
+ # its press point is read live (a move of the window carries
607
+ # it along); there is no re-pick (a zero-size rect) — a covered
608
+ # press point goes straight to the raise / move candidates
609
+ press_fn = lambda: _fragment_press_point(fragment, window_ds)
610
+ yield from task._satisfy(node, press_fn, depth=task._depth + 1,
611
+ rect=lambda: press_fn() * 2)
612
+ yield from _replay_fragment(fragment, window_ds)
613
+ for _ in range(Toggles.Orchestrator.cue_wait_frames):
614
+ if getattr(node, "expanded", True) is not False:
615
+ break
616
+ yield
617
+
618
+
619
+ def _fix_raise(task, window_ds, ds):
620
+ offset = raise_offset()
621
+ if offset is None:
622
+ raise _Abort("no raise demonstration recorded — record one (click any "
623
+ "window's header)")
624
+ # sub-goal: the header row must be hittable (re-picked inside the strip)
625
+ x, y = yield from task._satisfy(window_ds, lambda: _header_point(window_ds, offset),
626
+ depth=task._depth + 1,
627
+ rect=lambda: _header_rect(window_ds), header=True)
628
+ point = _tracking(x, y, lambda: _header_point(window_ds, offset))
629
+ yield from _click_at(point, window_ds)
630
+ # Verified by STATE: the window is in front at its header. The
631
+ # re-check after a fix only sees the unmet change, and a click that
632
+ # raised some OTHER window got it ok - "raise 'Loras' (ok)" with
633
+ # no raise effect anywhere, and the expand click that followed landed
634
+ # on the window still covering the chevron (Lukas 09-01).
635
+ for _ in range(Toggles.Orchestrator.layout_settle_pumps):
636
+ px, py = _point_of(point)
637
+ if _front_window_at(px, py) is window_ds:
638
+ return
639
+ yield
640
+ raise _Abort(f"'{display_name(window_ds)}' did not come to front")
641
+
642
+
643
+ def _fix_move(task, mover_ds, ds, point, rect=None):
644
+ """Move `mover_ds` (the obscurer, or the target's own window) by the
645
+ smallest delta that uncovers the target's press RECT — the re-pick
646
+ rect when the caller gave one (a gate's chevron zone, a header strip),
647
+ else the target's visible rect — and always at least `point`."""
648
+ offset = move_offset()
649
+ if offset is None:
650
+ raise _Abort("no window-move demonstration recorded — record one (drag "
651
+ "any window by its header)")
652
+ obscurer = task._unmet[1]
653
+ left, top, width, height = _live_rect(obscurer)
654
+ margin = Toggles.Orchestrator.uncover_margin_px
655
+ # What the move must clear: the subject's WHOLE visible rect, plus any
656
+ # re-pick rect and the point. Clearing only the re-pick box (a gate's
657
+ # ±6 px chevron zone) parked the obscurer a hair outside it, and the
658
+ # next re-pick / layout slop put the chevron back over its edge -
659
+ # "it didn't drag it far enough" (Lukas 09-01). A person drags the
660
+ # window off the thing, not off the pixel.
661
+ target = _visible_rect(ds)
662
+ extra = rect() if callable(rect) else rect
663
+ if extra is not None:
664
+ target = (min(target[0], extra[0]), min(target[1], extra[1]),
665
+ max(target[2], extra[2]), max(target[3], extra[3]))
666
+ x, y = point
667
+ target = (min(target[0], x), min(target[1], y), max(target[2], x), max(target[3], y))
668
+ dx, dy = _escape_rect_delta((left, top, left + width, top + height), target, margin)
669
+ if mover_ds is not obscurer:
670
+ dx, dy = -dx, -dy # the point rides with the window
671
+ # Move by GEOMETRY, like a replayed move - the mover's corner must
672
+ # travel by the delta. The re-check after a fix only sees the unmet
673
+ # CHANGE - and a header press RAISES its window, so a drag whose grab
674
+ # never took still reshuffled the pile ("obscured by A" became
675
+ # "obscured by B"), read as ok, and the solver moved A and B in circles
676
+ # for hundreds of rounds without a window moving an inch (Lukas 09-01).
677
+ # A grab that does not take gets ONE more try after re-settling.
678
+ tolerance = Toggles.Orchestrator.move_tolerance_px
679
+ for attempt in range(2):
680
+ x, y = yield from task._satisfy(mover_ds, lambda: _header_point(mover_ds, offset),
681
+ depth=task._depth + 1,
682
+ rect=lambda: _header_rect(mover_ds), header=True)
683
+ before = _live_rect(mover_ds)[:2]
684
+ yield from _drag_by(_tracking(x, y, lambda: _header_point(mover_ds, offset)),
685
+ dx, dy, mover_ds)
686
+ yield from _wait_layout(mover_ds)
687
+ after = _live_rect(mover_ds)[:2]
688
+ moved = (after[0] - before[0], after[1] - before[1])
689
+ if abs(moved[0] - dx) <= tolerance and abs(moved[1] - dy) <= tolerance:
690
+ return
691
+ if abs(moved[0]) + abs(moved[1]) > tolerance:
692
+ return # it moved, short of the delta: the re-check s
693
+ raise _Abort(f"'{display_name(mover_ds)}' did not move (header drag by "
694
+ f"({dx:.0f}, {dy:.0f}) not taken)")
695
+
696
+
697
+ def _fix_scroll(task, ds, point_fn):
698
+ container = _scroll_container(ds)
699
+ cl, ct, cr, cb = _visible_rect(container)
700
+ if cr - cl <= 0 or cb - ct <= 0:
701
+ cl, ct, cw, ch = _live_rect(container)
702
+ cr, cb = cl + cw, ct + ch
703
+ cx, cy = (cl + cr) / 2.0, (ct + cb) / 2.0
704
+ # the wheel scrolls to whatever is under the cursor: the viewport's
705
+ # centre is a sub-goal too (a window over it is raised / moved, or a
706
+ # re-pick inside the target's visible rect finds an open spot)
707
+ cx, cy = yield from task._satisfy(container, lambda: (cx, cy), depth=task._depth + 1,
708
+ rect=(cl, ct, cr, cb))
709
+ _left, top, _w, height = _live_rect(ds)
710
+ # wheel +1 moves content DOWN (reveals the top) - see the wrapper's
711
+ # scroll block in core_render; a target below the viewport needs
712
+ # negative wheel motion. Which side it is on: the target's own clip
713
+ # rect is rect ∩ viewport, so its bottom sits ABOVE the target's
714
+ # middle exactly when the target is below the viewport.
715
+ clip = getattr(ds, "abs_clip_rect", None)
716
+ clip_bottom = clip[3] if clip is not None else cy
717
+ direction = -1.0 if clip_bottom < top + height / 2.0 else 1.0
718
+ yield from _settle_at((cx, cy))
719
+ for _ in range(Toggles.Orchestrator.scroll_attempts):
720
+ Orchestrator._inject((0.0, "change", "scroll_y", direction))
721
+ yield
722
+ yield
723
+ if (_first_unmet(ds, point_fn()) or (None,))[0] != "outside":
724
+ return
725
+
726
+
727
+ def _candidates(task, unmet, ds, point_fn, rect=None):
728
+ """[(cost, label, generator)] for one unmet precondition, cheapest
729
+ first. A fix whose mechanism is not recorded is listed too — running
730
+ it aborts naming the missing demonstration — unless a cheaper recorded
731
+ one exists, so the abort names the FIRST thing worth recording."""
732
+ kind, node = unmet
733
+ point = point_fn()
734
+ out = []
735
+ if kind == "closed":
736
+ raise _Abort(f"window '{display_name(node)}' is closed")
737
+ if kind == "collapsed":
738
+ out.append((_fix_cost("expand", node, ds), f"expand '{display_name(node)}'",
739
+ lambda: _fix_expand(task, node, ds)))
740
+ elif kind in ("outside", "clipped"):
741
+ if kind == "clipped":
742
+ out.append((_fix_cost("re_pick", ds, ds), "re-pick the press point",
743
+ lambda: task._re_pick(ds, rect)))
744
+ out.append((_fix_cost("scroll", node, ds),
745
+ f"scroll '{display_name(_scroll_container(ds))}'",
746
+ lambda: _fix_scroll(task, ds, point_fn)))
747
+ elif kind == "obscured":
748
+ window = _window_of(ds)
749
+ out.append((_fix_cost("re_pick", ds, ds), "re-pick the press point",
750
+ lambda: task._re_pick(ds, rect)))
751
+ out.append((_fix_cost("raise", window, ds), f"raise '{display_name(window)}'",
752
+ lambda: _fix_raise(task, window, ds)))
753
+ for mover in (node, window):
754
+ if getattr(mover, "closable", True) is False:
755
+ continue
756
+ out.append((_fix_cost("move", mover, ds), f"move '{display_name(mover)}'",
757
+ lambda m=mover: _fix_move(task, m, ds, point, rect)))
758
+ out.sort(key=lambda c: c[0])
759
+ return [c for c in out if c[0] <= Toggles.Orchestrator.max_disturbance]
760
+
761
+
762
+ def _live_change_on(ds, since_frame):
763
+ """Newest edit-stack Change targeting `ds` recorded at/after
764
+ `since_frame`. A held drag COALESCES — the same Change's `new` advances
765
+ per frame — which is what makes mid-gesture reading possible."""
766
+ for change in reversed(UndoManager.stack.history):
767
+ if change.frame < since_frame:
768
+ break
769
+ if change.draw_state is ds:
770
+ return change
771
+ return None
772
+
773
+
774
+ # ── injection shorthand (absolute coordinates) ───────────────────────────
775
+
776
+ def _wait_layout(ds, since_frame=None):
777
+ """After a fix moved things (an expand reflowed the rows below it, a
778
+ scroll, a window move): wait until `ds` has been laid out again — its
779
+ wrapper ran after `since_frame` (draw_state.last_seen) — and its rect
780
+ held still for two consecutive frames, so the press point is read
781
+ from LIVE geometry. Read on the frame right after an expand, a leaf
782
+ still carried its pre-expand rect and the retargeted click landed on
783
+ the collection header above it (09-01). Bounded by layout_settle_pumps."""
784
+ last_rect = None
785
+ for _ in range(Toggles.Orchestrator.layout_settle_pumps):
786
+ rect = _live_rect(ds)
787
+ seen = getattr(ds, "last_seen", None)
788
+ laid_out = (since_frame is None or seen is None
789
+ or (isinstance(seen, (int, float)) and seen > since_frame))
790
+ if laid_out and rect == last_rect:
791
+ return
792
+ last_rect = rect
793
+ yield
794
+
795
+
796
+ def _wait_seconds(seconds):
797
+ """Hold for a recorded pause (wall clock; yields frames meanwhile)."""
798
+ deadline = time.monotonic() + max(0.0, seconds)
799
+ while time.monotonic() < deadline:
800
+ yield
801
+
802
+
803
+ def glide_seconds(distance):
804
+ """How long a synthesized move of `distance` px takes: a human hand's
805
+ speed (Toggles.Orchestrator.glide_px_per_second), clamped."""
806
+ return max(Toggles.Orchestrator.glide_min_s,
807
+ min(Toggles.Orchestrator.glide_max_s,
808
+ distance / max(1.0, Toggles.Orchestrator.glide_px_per_second)))
809
+
810
+
811
+ def _glide(start_x, start_y, point):
812
+ """Eased, WALL-CLOCK paced travel from (start_x, start_y) to `point`:
813
+ one move per pump along a smoothstep curve until glide_seconds elapse.
814
+ `point` may be live (see _point_of): the curve re-aims at the target's
815
+ CURRENT position every pump, so it lands on a target that moved."""
816
+ x, y = _point_of(point)
817
+ distance = ((x - start_x) ** 2 + (y - start_y) ** 2) ** 0.5
818
+ if distance < 0.5:
819
+ _move(x, y)
820
+ yield
821
+ return
822
+ duration = glide_seconds(distance)
823
+ started = time.monotonic()
824
+ while True:
825
+ x, y = _point_of(point)
826
+ fraction = min(1.0, (time.monotonic() - started) / duration)
827
+ eased = fraction * fraction * (3.0 - 2.0 * fraction)
828
+ _move(start_x + (x - start_x) * eased, start_y + (y - start_y) * eased)
829
+ yield
830
+ if fraction >= 1.0:
831
+ break
832
+
833
+
834
+ def _settle_at(point, ds=None):
835
+ """Move the virtual cursor to (x, y) and wait until a press there lands
836
+ on what is under it: the engine's settle rule (Orchestrator.press_ready
837
+ — hover registered by a render after stamp_io saw the move, no real
838
+ button held), AND the target's own tile reporting hover
839
+ (`_bounding_hovered`, set only when its wrapper RUNS — i.e. the tile has
840
+ left the blit cache and its imgui widget is being submitted live; a
841
+ press on a cached tile reaches only the enclosing window's move handle).
842
+ Bounded wait: a view that never reports hover (no bounding tracking)
843
+ proceeds after SETTLE_HOVER_PUMPS."""
844
+ # Glide from wherever the virtual cursor is to (x, y) over several
845
+ # pumps (eased) rather than teleporting: the real pointer travels, hover
846
+ # edges fire naturally along the way, and the progress is visible.
847
+ start_x, start_y = Orchestrator._virtual_x, Orchestrator._virtual_y
848
+ if not Orchestrator._cursor_settled:
849
+ start_x, start_y = _point_of(point) # first move of the run, no origin to glide from
850
+ yield from _glide(start_x, start_y, point)
851
+ while True:
852
+ x, y = _point_of(point)
853
+ if abs(x - Orchestrator._virtual_x) + abs(y - Orchestrator._virtual_y) > 0.5:
854
+ _move(x, y) # the target moved since the glide landed
855
+ yield
856
+ continue
857
+ if Orchestrator.press_ready((x, y)):
858
+ break
859
+ yield
860
+ waited = 0
861
+ while (ds is not None and not getattr(ds, "_bounding_hovered", False)
862
+ and waited < Toggles.Orchestrator.settle_hover_pumps):
863
+ waited += 1
864
+ yield
865
+
866
+
867
+ # change_value's own injections bypass the engine's continuous-mouse layer
868
+ # (glide=False): every move here is already paced (_glide, see servo) and
869
+ # every press already settled. Routed through that layer, an eased step
870
+ # over its teleport threshold spawned a SECONDARY glide and the press /
871
+ # release queued behind it: the press landed late, and a glide running
872
+ # while a button was virtually held dragged the cursor by the header.
873
+ def _press(x, y, ds=None):
874
+ """Press — with the target's tile forced LIVE on the press frame: imgui
875
+ only activates an item it submitted the frame it saw the click, and a
876
+ press frame otherwise serves the tile from the blit cache (the hover
877
+ invalidate is suppressed on press frames, see apply_move_to_front).
878
+ This is also what makes a press on a NON-front window work — the
879
+ press raises it and the raise reshuffles tiles that frame — so
880
+ front-ness is deliberately not a precondition: the Orchestrator's own
881
+ window (Play was just pressed in it) is in front of every target, and
882
+ a "must be front" gate could never be met (Lukas 09-01)."""
883
+ _render_live(ds)
884
+ Orchestrator._inject((0.0, "down", "left_mouse", x, y), glide=False)
885
+
886
+
887
+ def _render_live(ds):
888
+ """Dirty the target's tile so the coming frame submits its imgui item."""
889
+ invalidate = getattr(ds, "invalidate", None) if ds is not None else None
890
+ if callable(invalidate):
891
+ try:
892
+ invalidate()
893
+ except Exception:
894
+ pass
895
+
896
+
897
+ def _move(x, y):
898
+ Orchestrator._inject((0.0, "move", x, y), glide=False)
899
+
900
+
901
+ def _release(x, y, ds=None):
902
+ _render_live(ds) # a button FIRES on the release frame
903
+ Orchestrator._inject((0.0, "up", "left_mouse", x, y), glide=False)
904
+
905
+
906
+ def _key(key, mods=0):
907
+ Orchestrator._inject((0.0, "key", key, mods))
908
+
909
+
910
+ def _char(codepoint):
911
+ Orchestrator._inject((0.0, "char", codepoint))
912
+
913
+
914
+ def _leaf_point(ds, take, task=None):
915
+ """Press point: the caller's explicit point when it has one (a replay
916
+ remap presses where the tape recorded — the cursor is already there),
917
+ else the take's demonstrated press as a FRACTION of its leaf's rect,
918
+ re-applied to THIS leaf's live rect (the lora_dropout generalization).
919
+ No take / no press -> the leaf's center."""
920
+ if task is not None and task.press_point is not None:
921
+ return task.press_point
922
+ frac = None
923
+ if task is not None and getattr(task, "press_frac", None) is not None:
924
+ frac = task.press_frac # the cue's OWN press, first
925
+ if frac is None and take is not None:
926
+ cue = terminal_cue(take)
927
+ frac = cue_get(cue, "press_frac") if cue is not None else None
928
+ frac = frac or (0.5, 0.5)
929
+ left, top, width, height = _live_rect(ds)
930
+ return (left + frac[0] * width, top + frac[1] * height)
931
+
932
+
933
+ def _replay_fragment(fragment, window_ds):
934
+ """Replay a gate fragment with its anchors rebound to `window_ds`'s
935
+ live origin — the recorded window-relative geometry lands in the
936
+ TARGET window. Runs at 2x the configured replay speed (gates are means,
937
+ not the demonstration)."""
938
+ origin = None
939
+ if window_ds is not None:
940
+ left = getattr(window_ds, "abs_left", None)
941
+ top = getattr(window_ds, "abs_top", None)
942
+ if left is not None and top is not None:
943
+ origin = (left, top)
944
+ speed = max(0.05, Toggles.Orchestrator.replay_speed) * 2.0
945
+ t0 = time.monotonic()
946
+ for event in fragment.events:
947
+ while event[0] > (time.monotonic() - t0) * speed:
948
+ yield
949
+ _inject_rebased(event, origin)
950
+ # the fragment's events may have been QUEUED FOR a continuous-mouse
951
+ # glide (a reused take injects far from the cursor): hold until they
952
+ # have actually landed, so the caller's wait for the effect starts
953
+ # after the click, not before it
954
+ while Orchestrator._glide_queue:
955
+ yield
956
+ yield
957
+
958
+
959
+ def _inject_rebased(event, origin):
960
+ kind = event[1]
961
+ if origin is not None and kind in ("move", "down", "up"):
962
+ slot = 2 if kind == "move" else 3
963
+ if len(event) > slot + 2: # relativized: rebase to origin
964
+ Orchestrator._inject(event[:slot] + (event[slot] + origin[0],
965
+ event[slot + 1] + origin[1]))
966
+ return
967
+ Orchestrator._inject(event)
968
+
969
+
970
+ # ── the task ─────────────────────────────────────────────────────────────
971
+
972
+ class ValueTask:
973
+ """One change_value run: a generator stepped per frame by the engine.
974
+ error is None on success; result is the verified final value."""
975
+
976
+ def __init__(self, path, to, within=None, universe=None, eps=None):
977
+ self.path = path
978
+ self.to = to
979
+ self.pacing = {} # predefined pauses to honour (a replay remap sets these)
980
+ self.press_point = None # explicit press point (a replay remap: the recorded press)
981
+ self.gates_only = False # replay without override: open the gates where the tape presses
982
+ self.within = within
983
+ self.universe = universe
984
+ self.eps = eps
985
+ self.error = None
986
+ self.result = None
987
+ self.start_frame = 0
988
+ self.trace_target = None
989
+ self._verify_eps = 0.0
990
+ self._skip_verify = False
991
+ self._generator = None
992
+ self._done = threading.Event()
993
+ self._take = None
994
+ self._unmet = None
995
+ self._depth = 0
996
+ self._picked = None
997
+ # Fix labels running up the sub-goal chain. A fix's own sub-goal
998
+ # never re-offers the fix: raising W needs W's header hittable,
999
+ # and with the wrong tests the cheapest candidate for THAT was
1000
+ # "raise W" again - seven levels of the same raise until max_depth
1001
+ # stopped them, for every window in the tree, before the fix that
1002
+ # actually helps was ever tried at depth 1 (Lukas 09-01).
1003
+ self._active_fixes = []
1004
+ self.attempts = [] # "fix (result)" log - the abort message lists it
1005
+ self._last_fix_frame = None # Melty.frame_count when the last fix finished (layout wait)
1006
+ # A precondition-only run (the orchestrator window's per-precondition
1007
+ # run chip): satisfy preconditions up to and including the one with
1008
+ # this key (see precondition.py), then stop - no value change, no
1009
+ # verify. Outer preconditions along the way are satisfied too: the
1010
+ # solver is outermost-first, so "run the third one" performs the
1011
+ # first two, exactly as the full command would.
1012
+ self.until = None
1013
+ self.orchestration = None # the take this run belongs to (failure / success rows)
1014
+ # The command's press target: "leaf" (a value editor, the press at
1015
+ # the demonstrated fraction of its rect) or "header" (a WINDOW's
1016
+ # header strip - the move archetype; `to` is then the (dx, dy) the
1017
+ # window must travel). The precondition ladder is the same either
1018
+ # way; only the press point and the re-pick rect differ.
1019
+ self.gesture = "leaf"
1020
+ # The press as a FRACTION of the target's rect, from the cue that
1021
+ # demonstrated it - the press point for an editor with no archetype
1022
+ # (any widget: the cue still knows where on the target it pressed).
1023
+ self.press_frac = None
1024
+ # The press as an OFFSET from the target's live top-left (px) - a
1025
+ # gates-only replay presses where the recording pressed on the
1026
+ # target, wherever the target sits now (a reorder moved the target,
1027
+ # a scroll fix shifted it). Overrides press_frac if set.
1028
+ self.press_offset = None
1029
+ # The CONTROL the cue pressed, as (width, height, frac_x, frac_y)
1030
+ # around the press point: where a re-pick may land. A header
1031
+ # button is a 30 px square on a 4000 px collection: a re-pick
1032
+ # anywhere else on the collection is not the button. None = the
1033
+ # target's visible rect.
1034
+ self.control = None
1035
+
1036
+ def __str__(self):
1037
+ if self.until is not None:
1038
+ return f"precondition {self.until[0]} '{self.until[1]}' for {format_path(parse(self.path))}"
1039
+ if self.gates_only:
1040
+ return f"open gates for {format_path(parse(self.path))}"
1041
+ if self.gesture == "header":
1042
+ return f"move_window({format_path(parse(self.path))} by {self.to!r})"
1043
+ return f"change_value({format_path(parse(self.path))} → {self.to!r})"
1044
+
1045
+ def _press_point_fn(self, ds, take=None):
1046
+ """The press point this task aims at, live: the explicit point when
1047
+ one was given (a replay remap: the recorded press, re-anchored),
1048
+ else the header centre for a window gesture, else the demonstrated
1049
+ fraction of the leaf's rect."""
1050
+ if self.gesture == "header":
1051
+ # NEVER the cue's press offset here (see _header_point): a raise
1052
+ # recorded off a body click carries an offset deep in the
1053
+ # window, and a collapsed window is 32 px tall — the point read
1054
+ # as "clipped" on every round (Lukas 09-01, the stress test)
1055
+ return lambda: (self.press_point if self.press_point is not None
1056
+ else _header_point(ds, move_offset()))
1057
+ if self.press_offset is not None:
1058
+ def from_offset():
1059
+ if self.press_point is not None:
1060
+ return self.press_point
1061
+ left, top, _width, _height = _live_rect(ds)
1062
+ return (left + self.press_offset[0], top + self.press_offset[1])
1063
+ return from_offset
1064
+ if self.press_frac is not None:
1065
+ def from_frac():
1066
+ if self.press_point is not None:
1067
+ return self.press_point
1068
+ left, top, width, height = _live_rect(ds)
1069
+ return (left + self.press_frac[0] * width, top + self.press_frac[1] * height)
1070
+ return from_frac
1071
+ return lambda: _leaf_point(ds, take, self)
1072
+
1073
+ def _press_rect_fn(self, ds):
1074
+ """The rect a re-pick may land in: the header strip for a window
1075
+ gesture, the leaf's visible rect otherwise (None = default)."""
1076
+ if self.gesture == "header":
1077
+ return lambda: _header_rect(ds)
1078
+ if self.control is not None:
1079
+ point_fn = self._press_point_fn(ds)
1080
+
1081
+ def control_rect():
1082
+ width, height, frac_x, frac_y = self.control
1083
+ x, y = point_fn()
1084
+ left, top = x - frac_x * width, y - frac_y * height
1085
+ return (left, top, left + width, top + height)
1086
+ return control_rect
1087
+ return None
1088
+
1089
+ def fail(self, message):
1090
+ self.error = self.error or str(message)
1091
+ self._done.set()
1092
+
1093
+ def finish(self):
1094
+ self._done.set()
1095
+
1096
+ def wait(self, timeout=None):
1097
+ self._done.wait(timeout)
1098
+ return self.error is None
1099
+
1100
+ # ----- generator body (engine steps it; yield = wait one frame) ----
1101
+
1102
+ def run(self):
1103
+ try:
1104
+ yield from self._run()
1105
+ except _PreconditionDone:
1106
+ self.result = "satisfied"
1107
+ except _Abort as abort:
1108
+ self.error = str(abort)
1109
+
1110
+ def _run(self):
1111
+ ds = yield from self._resolve_structural()
1112
+ if self._last_fix_frame is not None:
1113
+ yield from _wait_layout(ds, since_frame=self._last_fix_frame)
1114
+ if self.until is not None:
1115
+ # the chosen precondition was structural and is now open (or was
1116
+ # never unmet); the geometric ones still run but the ladder is
1117
+ # identical to the command's own - _apply_fixes stops the run
1118
+ # the moment the chosen one is satisfied
1119
+ take = take_for(_ARCHETYPE_BY_EDITOR.get(_editor_of(ds)))
1120
+ yield from self._satisfy(ds, self._press_point_fn(ds, take),
1121
+ rect=self._press_rect_fn(ds),
1122
+ header=self.gesture == "header")
1123
+ self.result = "satisfied"
1124
+ self._skip_verify = True
1125
+ return
1126
+ if self.gesture == "header":
1127
+ yield from self._run_move(ds)
1128
+ return
1129
+ if self.gates_only:
1130
+ # a recorded gesture about to play verbatim: make its target
1131
+ # hittable at the RECORDED press point (collapsed parents opened
1132
+ # by _resolve_structural; here the geometric ones - scrolled
1133
+ # out, covered), then hand back to the tape
1134
+ yield from self._satisfy(ds, self._press_point_fn(ds), rect=self._press_rect_fn(ds))
1135
+ self.result = ds
1136
+ return
1137
+ editor = _editor_of(ds)
1138
+ archetype = _ARCHETYPE_BY_EDITOR.get(editor)
1139
+ if archetype is None:
1140
+ raise _Abort(f"no archetype for editor {editor!r}")
1141
+ take = take_for(archetype)
1142
+ if take is None and archetype in _NEEDS_TAKE:
1143
+ raise _Abort(f"no {archetype} demonstration recorded — record one "
1144
+ f"({'drag any float field' if archetype == 'drag' else 'edit any text field'})")
1145
+ self._take = take
1146
+ # Geometric preconditions at the press point the executor will use
1147
+ # (a re-pick fix moves task.press_point; _leaf_point honours it).
1148
+ yield from self._satisfy(ds, lambda: _leaf_point(ds, take, self))
1149
+ self.trace_target = ds # engine trace reads its hover/active state
1150
+ runner = {"drag": _run_drag, "text": _run_text, "toggle": _run_toggle}[archetype]
1151
+ yield from runner(self, ds, take)
1152
+ if not self._skip_verify:
1153
+ yield from self._verify(ds)
1154
+
1155
+ def _run_move(self, window):
1156
+ """The move archetype: grab the window by its header (the press a
1157
+ sub-goal like any other — a window over the header is raised /
1158
+ moved first, a re-pick stays inside the strip), drag by `to` =
1159
+ (dx, dy) at the recorded pace, verify by GEOMETRY: the window's
1160
+ live corner moved by the delta (within move_tolerance_px — a hard
1161
+ display limit or a collision that stopped it short is an honest
1162
+ abort naming how far it got)."""
1163
+ if move_offset() is None and not self.gates_only:
1164
+ raise _Abort("no window-move demonstration recorded — record one (drag "
1165
+ "any window by its header)")
1166
+ if self.gates_only:
1167
+ dx = dy = 0.0 # the tape does the moving; only the press matters here
1168
+ else:
1169
+ try:
1170
+ dx, dy = float(self.to[0]), float(self.to[1])
1171
+ except (TypeError, ValueError, IndexError):
1172
+ raise _Abort(f"move needs a (dx, dy) delta, got {self.to!r}")
1173
+ point_fn = self._press_point_fn(window)
1174
+ yield from self._wait_pacing("before_press")
1175
+ x, y = yield from self._satisfy(window, point_fn, rect=self._press_rect_fn(window),
1176
+ header=True)
1177
+ header = lambda: _header_point(window, move_offset())
1178
+ before = _live_rect(window)[:2]
1179
+ if self.gates_only:
1180
+ self.result = window # the tape presses and drags back
1181
+ return
1182
+ yield from _drag_by(_tracking(x, y, header), dx, dy, window)
1183
+ tolerance = Toggles.Orchestrator.move_tolerance_px
1184
+ moved = (0.0, 0.0)
1185
+ for _ in range(Toggles.Orchestrator.cue_wait_frames):
1186
+ after = _live_rect(window)[:2]
1187
+ moved = (after[0] - before[0], after[1] - before[1])
1188
+ if abs(moved[0] - dx) <= tolerance and abs(moved[1] - dy) <= tolerance:
1189
+ self.result = after
1190
+ self._skip_verify = True
1191
+ return
1192
+ yield
1193
+ raise _Abort(f"window '{display_name(window)}' moved by ({moved[0]:.0f}, {moved[1]:.0f}), "
1194
+ f"wanted ({dx:.0f}, {dy:.0f}) — stopped short (display edge / collision?)")
1195
+
1196
+ def _wait_pacing(self, key):
1197
+ yield from _wait_seconds(self.pacing.get(key, 0.0))
1198
+
1199
+ def _resolve_structural(self):
1200
+ """Resolve the path and open the STRUCTURAL gates (closed windows,
1201
+ collapsed ancestors) until the leaf is live with geometry — the
1202
+ geometric preconditions (viewport, cover) need the press point,
1203
+ which needs the take, which needs the resolved editor."""
1204
+ # one gate opens per pass, so the bound is on STALLS - the same gate
1205
+ # unmet for consecutive passes (it re-closed, or the fix missed) -
1206
+ # never on the number of gates; three nested collections take three
1207
+ # passes, not three failures (09-01). The upper cap only stops a
1208
+ # pathological chain.
1209
+ stalls, last_gate = 0, None
1210
+ for _pass in range(max(64, Toggles.Orchestrator.gate_attempts * 4)):
1211
+ missing = None
1212
+ try:
1213
+ ds, _suffix = resolve_recorded(self.path, within=self.within,
1214
+ universe=self.universe)
1215
+ except NoMatch as error:
1216
+ ds, missing = None, error
1217
+ except Ambiguous as ambiguous:
1218
+ raise _Abort(str(ambiguous))
1219
+ if ds is not None and _reachable(ds):
1220
+ return ds
1221
+ frontier = ds
1222
+ if frontier is None:
1223
+ segments = parse(self.path)
1224
+ for length in range(len(segments) - 1, 0, -1):
1225
+ try:
1226
+ frontier = resolve(segments[:length], within=self.within,
1227
+ universe=self.universe)
1228
+ break
1229
+ except (NoMatch, Ambiguous):
1230
+ continue
1231
+ unmet = _first_unmet(frontier, _leaf_point(frontier, None)) if frontier is not None else None
1232
+ if unmet is None or unmet[0] not in ("closed", "collapsed"):
1233
+ if missing is not None:
1234
+ raise _Abort(str(missing)) # the NoMatch, default "unreachable"
1235
+ raise _Abort(f"{format_path(parse(self.path))} resolved but is not "
1236
+ f"hittable, and no closed gate found on its path")
1237
+ gate = (unmet[0], id(unmet[1]))
1238
+ stalls = stalls + 1 if gate == last_gate else 0
1239
+ last_gate = gate
1240
+ if stalls >= max(1, Toggles.Orchestrator.gate_attempts):
1241
+ raise _Abort(f"{display_name(unmet[1])} kept {'closing' if unmet[0] == 'closed' else 'collapsing'} "
1242
+ f"— gave up after {Toggles.Orchestrator.gate_attempts} attempts")
1243
+ yield from self._apply_fixes(unmet, frontier,
1244
+ lambda f=frontier: _leaf_point(f, None))
1245
+ yield # a layout frame before re-resolving
1246
+ raise _Abort("gates kept closing — gave up after "
1247
+ f"{max(64, Toggles.Orchestrator.gate_attempts * 4)} passes")
1248
+
1249
+ def _satisfy(self, ds, point_fn, depth=0, rect=None, header=False):
1250
+ """Make `ds` hittable at the press point `point_fn()` returns — a
1251
+ CALLABLE, re-evaluated after every fix, because fixes move things
1252
+ (a scroll shifts the target, a move shifts a header); a re-pick
1253
+ replaces it with a constant inside `rect` (default the view's
1254
+ visible rect). Finds the first unmet precondition, runs its
1255
+ candidate fixes cheapest-first, re-checks after each. Returns the
1256
+ final point. Used for the target and, recursively, for a fix's own
1257
+ press point (the obscurer's header can itself be covered — a
1258
+ sub-goal under the same rules, depth-limited)."""
1259
+ if depth > Toggles.Orchestrator.fix_depth:
1260
+ raise _Abort("precondition fixes nested too deep")
1261
+ saved_depth, self._depth = self._depth, depth
1262
+ since = len(self.attempts) # this scope's own attempts (the message lists just these)
1263
+ try:
1264
+ for _round in range(max(1, Toggles.Orchestrator.gate_attempts) * 2):
1265
+ unmet = _first_unmet(ds, point_fn(), header=header)
1266
+ if unmet is None:
1267
+ break
1268
+ point_fn = yield from self._apply_fixes(unmet, ds, point_fn, rect,
1269
+ since=since, header=header)
1270
+ yield
1271
+ point = point_fn()
1272
+ unmet = _first_unmet(ds, point, header=header)
1273
+ if unmet is not None:
1274
+ raise _Abort(self._unmet_message(unmet, ds, since=since))
1275
+ if depth == 0:
1276
+ self.press_point = point
1277
+ return point
1278
+ finally:
1279
+ self._depth = saved_depth
1280
+
1281
+ def _apply_fixes(self, unmet, ds, point_fn, rect=None, since=0, header=False):
1282
+ """Run the candidates for one unmet precondition until the
1283
+ precondition changes (fixed, or a different one surfaced). Returns
1284
+ the press point callable (a re-pick replaces it). `since` = where
1285
+ this scope's attempts start in the flat log."""
1286
+ self._unmet = unmet
1287
+ for cost, label, make in _candidates(self, unmet, ds, point_fn, rect):
1288
+ if label in self._active_fixes:
1289
+ continue # this fix's own sub-goal: not a candidate
1290
+ self._picked = None
1291
+ self._active_fixes.append(label)
1292
+ try:
1293
+ yield from make()
1294
+ except _Abort as abort:
1295
+ # a nested failure's own attempts are already in the flat
1296
+ # log; keep its HEAD here, never its transcript (re-embedding
1297
+ # it at every level blew the report past a megabyte, 09-01)
1298
+ self.attempts.append(f"{label} ({_abort_head(abort)})")
1299
+ continue
1300
+ finally:
1301
+ self._active_fixes.remove(label)
1302
+ self._unmet = unmet # a nested fix overwrote it
1303
+ self._last_fix_frame = Melty.frame_count
1304
+ if self._picked is not None:
1305
+ point_fn = (lambda p=self._picked: p)
1306
+ else:
1307
+ yield from _wait_layout(ds) # the fix moved things: re-checking geometry
1308
+ after = _first_unmet(ds, point_fn(), header=header)
1309
+ if after is None or after != unmet:
1310
+ self.attempts.append(f"{label} (ok, cost {cost})")
1311
+ if self.until is not None and precondition_key(unmet) == self.until:
1312
+ raise _PreconditionDone()
1313
+ return point_fn
1314
+ self.attempts.append(f"{label} (still {unmet[0]})")
1315
+ raise _Abort(self._unmet_message(unmet, ds, since=since))
1316
+
1317
+ def _re_pick(self, ds, rect=None):
1318
+ """The zero-cost fix: another press point inside the visible part
1319
+ of the target (or `rect`) that is NOT covered — a partially
1320
+ obscured target needs no window touched."""
1321
+ if callable(rect):
1322
+ rect = rect() # a live rect follows the fixes that moved things
1323
+ left, top, right, bottom = rect if rect is not None else _visible_rect(ds)
1324
+ window = _window_of(ds)
1325
+ for fx, fy in ((0.5, 0.5), (0.2, 0.5), (0.8, 0.5), (0.5, 0.2), (0.5, 0.8),
1326
+ (0.1, 0.1), (0.9, 0.1), (0.1, 0.9), (0.9, 0.9)):
1327
+ x = left + fx * (right - left)
1328
+ y = top + fy * (bottom - top)
1329
+ front = _front_window_at(x, y)
1330
+ if front is None or front is window:
1331
+ self._picked = (x, y)
1332
+ return
1333
+ yield
1334
+ raise _Abort("no uncovered point on the target")
1335
+
1336
+ def _unmet_message(self, unmet, ds, since=0):
1337
+ kind, node = unmet
1338
+ what = {"closed": f"window '{display_name(node)}' is closed",
1339
+ "collapsed": f"'{display_name(node)}' is collapsed",
1340
+ "outside": f"'{display_name(ds)}' is outside its scroll viewport",
1341
+ "clipped": f"'{display_name(ds)}' is clipped at its press point",
1342
+ "obscured": f"'{display_name(ds)}' is covered by '{display_name(node)}'"}[kind]
1343
+ attempts = self.attempts[since:]
1344
+ tried = "; ".join(attempts) if attempts else "nothing applicable"
1345
+ return f"{what} — tried: {tried}"
1346
+
1347
+ def _verify(self, ds):
1348
+ """The parameterized cue: a Change on the TARGET whose new value is
1349
+ `to` — expectation from the caller, mechanism from the take."""
1350
+ change = None
1351
+ for _ in range(Toggles.Orchestrator.cue_wait_frames):
1352
+ change = _live_change_on(ds, self.start_frame)
1353
+ if change is not None and _value_matches(change.new, self.to, self._verify_eps):
1354
+ self.result = change.new
1355
+ return
1356
+ yield
1357
+ reached = change.new if change is not None else "unchanged"
1358
+ raise _Abort(f"value is {reached!r}, wanted {self.to!r}")
1359
+
1360
+
1361
+ def _abort_head(abort):
1362
+ """A precondition failure's one-line cause, without its attempts."""
1363
+ return str(abort).split(" — tried:")[0]
1364
+
1365
+
1366
+ def _value_matches(value, target, eps):
1367
+ if isinstance(target, bool) or isinstance(target, str):
1368
+ return value == target
1369
+ if isinstance(target, (int, float)) and isinstance(value, (int, float)):
1370
+ return abs(float(value) - float(target)) <= max(eps, 1e-9)
1371
+ return value == target
1372
+
1373
+
1374
+ # ── preconditions as data (the orchestrator window lists and runs them) ──
1375
+
1376
+ def precondition_key(unmet):
1377
+ """The identity of an unmet precondition across frames: its kind and
1378
+ the display name of the node the fix acts on — what the window's run
1379
+ chip hands back as ValueTask.until."""
1380
+ kind, node = unmet
1381
+ return (kind, display_name(node))
1382
+
1383
+
1384
+ def list_preconditions(path, within=None, universe=None, gesture="leaf", press_frac=None):
1385
+ """Every precondition currently unmet on the way to `path`'s target,
1386
+ outermost first, as rows the orchestrator window renders:
1387
+ {"key", "kind", "node", "label", "fixes": [(cost, label), …]} — the
1388
+ same walk the solver takes (structural gates on the resolved chain or
1389
+ on the deepest resolvable frontier, then the first geometric one at
1390
+ the demonstrated press point), without running anything. Empty when
1391
+ the target is hittable now; a NoMatch / Ambiguous path yields one
1392
+ "unresolved" row."""
1393
+ task = ValueTask(path, None, within=within, universe=universe)
1394
+ task.gesture = gesture
1395
+ task.press_frac = press_frac
1396
+ try:
1397
+ ds, _suffix = resolve_recorded(path, within=within, universe=universe)
1398
+ except (NoMatch, Ambiguous) as error:
1399
+ ds = None
1400
+ frontier = None
1401
+ for length in range(len(parse(path)) - 1, 0, -1):
1402
+ try:
1403
+ frontier = resolve(parse(path)[:length], within=within, universe=universe)
1404
+ break
1405
+ except (NoMatch, Ambiguous):
1406
+ continue
1407
+ if frontier is None:
1408
+ return [{"key": ("unresolved", format_path(parse(path))), "kind": "unresolved",
1409
+ "node": format_path(parse(path)), "label": str(error), "fixes": []}]
1410
+ else:
1411
+ frontier = ds
1412
+ rows = []
1413
+ nodes = [frontier] + ancestor_chain(frontier)
1414
+ for node in reversed(nodes): # outermost first, like _first_unmet
1415
+ if getattr(node, "closed", False):
1416
+ rows.append(("closed", node))
1417
+ elif node is not frontier and getattr(node, "expanded", True) is False:
1418
+ rows.append(("collapsed", node))
1419
+ if ds is not None and not rows and _reachable(ds):
1420
+ take = take_for(_ARCHETYPE_BY_EDITOR.get(_editor_of(ds)))
1421
+ point = task._press_point_fn(ds, take)()
1422
+ unmet = _first_unmet(ds, point, header=gesture == "header")
1423
+ if unmet is not None:
1424
+ rows.append(unmet)
1425
+ elif ds is None and not rows:
1426
+ rows.append(("unresolved", frontier))
1427
+ out = []
1428
+ for unmet in rows:
1429
+ kind, node = unmet
1430
+ fixes = []
1431
+ if kind == "unresolved":
1432
+ out.append({"key": precondition_key(unmet), "kind": kind, "node": display_name(node),
1433
+ "label": f"{format_path(parse(path))} does not resolve below "
1434
+ f"'{display_name(node)}' (open / expand it first)",
1435
+ "fixes": []})
1436
+ continue
1437
+ if kind != "unresolved":
1438
+ try:
1439
+ subject = ds if ds is not None else node
1440
+ point_fn = task._press_point_fn(subject)
1441
+ fixes = [(cost, label) for cost, label, _make
1442
+ in _candidates(task, unmet, subject, point_fn,
1443
+ task._press_rect_fn(subject))]
1444
+ except _Abort as abort:
1445
+ fixes = [(99, str(abort))]
1446
+ out.append({"key": precondition_key(unmet), "kind": kind,
1447
+ "node": display_name(node),
1448
+ "label": task._unmet_message(unmet, ds if ds is not None else node)
1449
+ .split(" — tried:")[0],
1450
+ "fixes": fixes})
1451
+ return out
1452
+
1453
+
1454
+ def describe_target(path, within=None, universe=None, gesture="leaf", press_frac=None):
1455
+ """What the solver sees for `path` right now — the facts every
1456
+ precondition verdict is computed from, for the window's hittable row
1457
+ and the failure report: the resolved leaf (name, editor, rect, visible
1458
+ rect, window), the press point it would use, and the front window at
1459
+ that point. Never raises."""
1460
+ out = {"path": format_path(parse(path))}
1461
+ try:
1462
+ ds, _suffix = resolve_recorded(path, within=within, universe=universe)
1463
+ except (NoMatch, Ambiguous) as error:
1464
+ out["resolved"] = None
1465
+ out["error"] = str(error)
1466
+ return out
1467
+ out["resolved"] = full_name(ds)
1468
+ out["editor"] = _editor_of(ds)
1469
+ out["rect"] = tuple(round(v, 1) for v in _live_rect(ds))
1470
+ out["visible"] = tuple(round(v, 1) for v in _visible_rect(ds))
1471
+ out["reachable"] = _reachable(ds)
1472
+ window = _window_of(ds)
1473
+ out["window"] = display_name(window) if window is not None else None
1474
+ out["chain"] = [display_name(w) for w in _window_chain(ds)]
1475
+ take = take_for(_ARCHETYPE_BY_EDITOR.get(_editor_of(ds)))
1476
+ out["take"] = getattr(take, "name", None) if take is not None else None
1477
+ probe = ValueTask(path, None, within=within, universe=universe)
1478
+ probe.gesture = gesture
1479
+ probe.press_frac = press_frac
1480
+ point = probe._press_point_fn(ds, take)()
1481
+ out["point"] = (round(point[0], 1), round(point[1], 1))
1482
+ front = _front_window_at(point[0], point[1])
1483
+ out["front"] = display_name(front) if front is not None else None
1484
+ unmet = _first_unmet(ds, point, header=gesture == "header")
1485
+ out["unmet"] = (unmet[0], display_name(unmet[1])) if unmet is not None else None
1486
+ return out
1487
+
1488
+
1489
+ def precondition_task(path, key, orchestration=None, within=None, universe=None,
1490
+ gesture="leaf", press_frac=None):
1491
+ """A ValueTask that satisfies preconditions up to and including `key`
1492
+ (from list_preconditions) and stops — the window's run chip. Submit it
1493
+ to the Orchestrator like any task."""
1494
+ task = ValueTask(path, None, within=within, universe=universe)
1495
+ task.until = tuple(key)
1496
+ task.orchestration = orchestration
1497
+ task.gesture = gesture
1498
+ task.press_frac = press_frac
1499
+ return task
1500
+
1501
+
1502
+ def move_window(path, delta, within=None, universe=None):
1503
+ """Move the window `path` names by `delta` = (dx, dy) through a real
1504
+ header drag — the move archetype (see ValueTask._run_move). The header
1505
+ press is a precondition sub-goal like any leaf press."""
1506
+ task = ValueTask(path, (float(delta[0]), float(delta[1])), within=within, universe=universe)
1507
+ task.gesture = "header"
1508
+ return Orchestrator.submit(task)
1509
+
1510
+
1511
+ # ── archetype executors ───────────────────────────────────────────────────
1512
+
1513
+ def _read_value(ds, since_frame):
1514
+ """The target's live numeric value: the coalescing Change's `new` when
1515
+ one has been recorded this run, else the draw_state's own held value
1516
+ (`_raw_input_value` — updated the frame the wrapper runs, so it sees an
1517
+ edit the undo record missed or lagged). None when neither is numeric."""
1518
+ change = _live_change_on(ds, since_frame)
1519
+ if change is not None and isinstance(change.new, (int, float)):
1520
+ return float(change.new)
1521
+ value = _current_value(ds)
1522
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
1523
+ return float(value)
1524
+ return None
1525
+
1526
+
1527
+ def _run_drag(task, ds, take):
1528
+ """Press at the demonstrated fraction, confirm the widget took the press
1529
+ (imgui active), probe to measure the LIVE gain, secant-servo to the
1530
+ target, release. The recorded gain is never used — the probe replaces
1531
+ all calibration. The sensor is the live value (_read_value): undo
1532
+ Change first, the draw_state's held value as the fallback."""
1533
+ probe_px = Toggles.Orchestrator.servo_probe_px
1534
+ max_step = Toggles.Orchestrator.servo_max_step_px
1535
+ point = lambda: _leaf_point(ds, take, task) # live: the leaf may shift while we travel
1536
+ since = task.start_frame
1537
+ yield from _settle_at(point, ds)
1538
+ yield from _wait_seconds(task.pacing.get("before_press", 0.0))
1539
+ x, y = point()
1540
+ baseline = _read_value(ds, since)
1541
+ _press(x, y, ds)
1542
+ yield
1543
+ # the widget must be ACTIVE before the probe moves - a press whose frame
1544
+ # served the tile from cache never reaches imgui (active flickers off);
1545
+ # keep the tile live while waiting (a raise on the press frame
1546
+ # reshuffles tiles, and imgui drops an active item it misses that frame)
1547
+ for _ in range(Toggles.Orchestrator.settle_hover_pumps):
1548
+ if getattr(ds, "_imgui_is_active", False):
1549
+ break
1550
+ _render_live(ds)
1551
+ yield
1552
+ else:
1553
+ _release(x, y, ds)
1554
+ yield
1555
+ raise _Abort("press did not activate the widget (tile not live at the press?)")
1556
+ yield from _wait_seconds(task.pacing.get("before_drag", 0.0))
1557
+ if baseline is None:
1558
+ _release(x, y, ds)
1559
+ raise _Abort("target holds no numeric value to drag")
1560
+ # Probe TOWARD the target: a value pinned at a min/max limit ignores a
1561
+ # push further into the clamp (alpha at its 99.264 ceiling ate a +8 px
1562
+ # probe), while a push toward the target always has room.
1563
+ direction = -1.0 if float(task.to) < baseline else 1.0
1564
+ # The probe GROWS until the field responds: an integer drag accumulates
1565
+ # fractional units (rank at 16 px per unit) and ignores a short probe, so
1566
+ # 8 px is only the first try - double up to servo_max_step_px.
1567
+ origin_x = x
1568
+ step = probe_px * direction
1569
+ current = baseline
1570
+ while abs(step) <= max_step:
1571
+ x = origin_x + step
1572
+ _move(x, y)
1573
+ yield
1574
+ yield # a frame for the value to settle
1575
+ current = _read_value(ds, since)
1576
+ if current is not None and current != baseline:
1577
+ break
1578
+ step *= 2.0
1579
+ else:
1580
+ step = origin_x - x # nothing moved, measure the reverse from here
1581
+ if current is None or current == baseline:
1582
+ # nothing toward the target: the other way separates pinned-at-the-limit
1583
+ # (the target's beyond the range) from not-draggable-at-all
1584
+ x = origin_x - (x - origin_x)
1585
+ _move(x, y)
1586
+ yield
1587
+ yield
1588
+ reverse = _read_value(ds, since)
1589
+ _release(x, y, ds)
1590
+ yield
1591
+ if reverse is not None and reverse != baseline:
1592
+ raise _Abort(f"value {baseline:g} is at its limit in the direction of "
1593
+ f"{float(task.to):g} — target outside the field's range")
1594
+ raise _Abort(f"probe drag changed nothing (value {baseline!r}, widget active="
1595
+ f"{bool(getattr(ds, '_imgui_is_active', False))}) — not a draggable value?")
1596
+ gain = (current - baseline) / step
1597
+ # convergence tolerance within half a pixel's worth of value (the widget's own
1598
+ # tolerance), unless the caller pinned one
1599
+ eps = task.eps if task.eps is not None else max(abs(gain) * 0.5, 1e-9)
1600
+ task._verify_eps = eps
1601
+ # ---- easing travel: spread the estimated distance over the RECORDED
1602
+ # drag_duration on a smooth curve (a hand, not a lurch), refining the
1603
+ # distance estimate from the value as it comes in; the step servo below
1604
+ # then only closes the last quantum ----
1605
+ duration = task.pacing.get("drag_duration", 0.0)
1606
+ if duration > 0.0 and gain != 0.0:
1607
+ travel = (float(task.to) - baseline) / gain # px from the press, first estimate
1608
+ start_x = x # the curve begins where the probe left off
1609
+ started = time.monotonic()
1610
+ heading = 1.0 if travel >= 0 else -1.0
1611
+ while True:
1612
+ fraction = min(1.0, (time.monotonic() - started) / duration)
1613
+ eased = fraction * fraction * (3.0 - 2.0 * fraction)
1614
+ goal = start_x + (origin_x + travel - start_x) * eased
1615
+ # MONOTONIC: the endpoint estimate may shrink as the gain refines,
1616
+ # but the cursor never reverses mid-drag (a quantized field's
1617
+ # noisy secant had it fighting itself)
1618
+ if (goal - x) * heading > 0:
1619
+ x = goal
1620
+ _move(x, y)
1621
+ yield
1622
+ fresh = _read_value(ds, since)
1623
+ # whole-drag secant: in once the cursor is a probe-length past
1624
+ # the press (nearer, the ratio is noisy; one tick at the origin
1625
+ # once divided by 1e-9 and pinned the travel at zero); the
1626
+ # endpoint moves at most a quarter of the way per tick (damped)
1627
+ if (fresh is not None and fresh != baseline
1628
+ and abs(x - origin_x) >= probe_px):
1629
+ measured = (fresh - baseline) / (x - origin_x)
1630
+ current = fresh
1631
+ if measured != 0.0 and (measured > 0) == (gain > 0):
1632
+ gain = gain + (measured - gain) * 0.25
1633
+ travel = (float(task.to) - baseline) / gain
1634
+ if fraction >= 1.0:
1635
+ break
1636
+ yield
1637
+ yield
1638
+ fresh = _read_value(ds, since)
1639
+ if fresh is not None:
1640
+ current = fresh
1641
+ stalls = 0
1642
+ damping = 1.0
1643
+ last_residual = None
1644
+ for _ in range(Toggles.Orchestrator.servo_max_steps):
1645
+ residual = float(task.to) - current
1646
+ if abs(residual) <= eps:
1647
+ break
1648
+ dx = max(-max_step, min(max_step, residual / gain))
1649
+ # damped: overshooting flips the residual's sign - halve the stride
1650
+ # from then on so the close never ping-pongs across the target
1651
+ if last_residual is not None and (residual > 0) != (last_residual > 0):
1652
+ damping *= 0.5
1653
+ last_residual = residual
1654
+ dx *= damping
1655
+ x += dx
1656
+ _move(x, y)
1657
+ # A move lands imgui at the NEXT frame's io stamp and the Change
1658
+ # lands after that frame's render: read two pumps later (the probe
1659
+ # already did; the loop read one pump early and called every step a
1660
+ # stall).
1661
+ yield
1662
+ yield
1663
+ fresh = _read_value(ds, since)
1664
+ if fresh is None or fresh == current:
1665
+ stalls += 1
1666
+ if stalls >= 3: # three settled reads, no motion
1667
+ _release(x, y, ds)
1668
+ yield
1669
+ raise _Abort(f"stalled at {current:g} heading for {float(task.to):g} "
1670
+ f"(min/max clamp?)")
1671
+ yield # give a next frame one more chance
1672
+ continue
1673
+ stalls = 0
1674
+ gain = (fresh - current) / dx # re-measure every step
1675
+ current = fresh
1676
+ else:
1677
+ _release(x, y, ds)
1678
+ yield
1679
+ raise _Abort(f"did not converge (at {current:g}, wanted {float(task.to):g})")
1680
+ _release(x, y, ds)
1681
+ yield
1682
+
1683
+
1684
+ def _run_text(task, ds, take):
1685
+ """Click to focus, synthesized clear, payload typed from `to` in the
1686
+ event shape a real keystroke produces, one clear-and-retype retry."""
1687
+ text = str(task.to)
1688
+ point = lambda: _leaf_point(ds, take, task)
1689
+ since = task.start_frame
1690
+ yield from _settle_at(point, ds)
1691
+ x, y = point()
1692
+ _press(x, y, ds)
1693
+ yield
1694
+ _release(x, y, ds)
1695
+ yield
1696
+ for attempt in range(2):
1697
+ # ---- clear: select-all + delete; End+Backspace fallback ----
1698
+ _key(glfw.KEY_A, glfw.MOD_CONTROL)
1699
+ yield
1700
+ _key(glfw.KEY_DELETE)
1701
+ yield
1702
+ yield
1703
+ remaining = _current_value(ds)
1704
+ if isinstance(remaining, str) and remaining:
1705
+ _key(glfw.KEY_END)
1706
+ yield
1707
+ for _ in range(len(remaining)):
1708
+ _key(glfw.KEY_BACKSPACE)
1709
+ yield
1710
+ yield
1711
+ # ---- payload: key + char per character, like a real keystroke ----
1712
+ for index, character in enumerate(text):
1713
+ keycode = _keycode_for(character)
1714
+ if keycode is not None:
1715
+ _key(keycode, glfw.MOD_SHIFT if character.isupper() else 0)
1716
+ _char(ord(character))
1717
+ if index % 4 == 3:
1718
+ yield # pace: 4 chars a frame
1719
+ yield
1720
+ yield
1721
+ change = _live_change_on(ds, since)
1722
+ if change is not None and change.new == text:
1723
+ return # _verify confirms exactly
1724
+ if attempt == 0:
1725
+ continue # once more: clear + retype
1726
+ # fall through - _verify delivers an honest comparison with what we reached
1727
+
1728
+
1729
+ def _keycode_for(character):
1730
+ """Best-effort glfw keycode for an ASCII character (letters, digits,
1731
+ space). Anything else rides the char event alone."""
1732
+ if character.isascii() and character.isalpha():
1733
+ return ord(character.upper())
1734
+ if character.isdigit():
1735
+ return ord(character)
1736
+ if character == " ":
1737
+ return glfw.KEY_SPACE
1738
+ return None
1739
+
1740
+
1741
+ def _run_toggle(task, ds, take):
1742
+ if isinstance(task.to, bool) and _current_value(ds) == task.to:
1743
+ task.result = task.to # nothing to do, nothing to verify
1744
+ task._skip_verify = True
1745
+ return
1746
+ point = lambda: _leaf_point(ds, take, task)
1747
+ yield from _settle_at(point, ds)
1748
+ x, y = point()
1749
+ _press(x, y, ds)
1750
+ yield
1751
+ _release(x, y, ds)
1752
+ yield