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,1665 @@
1
+ """The set/get-anywhere API — read and write a view parameter at whichever
2
+ input source is actually DRIVING it (signature default, caller kwarg, mode
3
+ entry, @defaults decoration, class var, override comment, instance attr...).
4
+
5
+ `SourcePriority` is the hardcoded ranking of those sources; `_driving_source`
6
+ is the single pick every entry point shares, so `get_source_for`,
7
+ `from_anywhere`, `anywhere_value` and `set_anywhere` can never disagree.
8
+ The source registry itself is collected by `collect_input_sources`
9
+ (new_core_view) — imported lazily inside `_sources_for` to keep this module
10
+ free of an import cycle with the view code that calls it.
11
+ """
12
+
13
+ from enum import Enum
14
+
15
+ from meltygui.core.rendering.core_decoration import Core
16
+
17
+ # We want to use the order of the tab elements as a way of determining the
18
+ # source of the input. From the function's perspective it just has parameters
19
+ # injected. Melty is routing data from many different sources and does its
20
+ # best to pick sane defaults when there are multiple sources. Which source
21
+ # takes priority depends on how the code is written, and the code may change.
22
+ # Rather than trying to infer the priority, we're just hard coding it here -
23
+ # edit the order of the elements to change the runtime behavior of meltygui.
24
+ # (Below is a rough memory of which sources take priority; reorder freely.)
25
+ class SourcePriority(Enum):
26
+ LIVE_COMMENT = 0 # the `# [tint=...]` override comment - the
27
+ # wrapper SPLATS it over kwargs after every
28
+ # other kwargs merges, so at runtime it
29
+ # beats @defaults, callers, and codecs alike
30
+ MODE = 1
31
+ WINDOW_DECORATION = 2 # @window(...) on the func or class, and
32
+ # @glfw_window(...) on the func - outranks
33
+ # @defaults (the window kwargs drive the
34
+ # window that renders the value)
35
+ CALLER = 3 # call-site kwargs - EXPLICITLY passed, so in
36
+ # the wrapper's gauntlet they beat every
37
+ # injected default layer, @defaults included
38
+ # (verified live: draw_code_tabs_and_cache's
39
+ # child_kwargs= wins over the GeneralParse
40
+ # class @defaults). DEPTH is the natural
41
+ # tiebreaker (see _source_priority) - no
42
+ # CALLER_0/CALLER_1 members needed
43
+ MODE_CHILD_KWARGS = 4 # child_kwargs={...} inside the PARENT's mode
44
+ # entry (Modes.NEW_CODE). Same merge into the
45
+ # child call as CHILD_KWARGS below. but on the
46
+ # parent it's the MODE setting child_kwargs -
47
+ # and mode outranks the parent's caller and
48
+ # @defaults - so it wins the dict whenever the
49
+ # mode entry carries one
50
+ CHILD_KWARGS = 5 # the PARENT view's child_kwargs={...} - an
51
+ # explicit call kwarg on the child, so
52
+ # caller-strength: above @defaults for the
53
+ # same reason as CALLER; the dict itself
54
+ # lives at any of the parent's OWN sources,
55
+ # resolved via from_anywhere("child_kwargs",
56
+ # parent)
57
+ AT_DEFAULT_CODE_TYPE = 6 # @defaults on a PARSED class in the value tree
58
+ AT_DEFAULT_OBJ_TYPE = 7 # @defaults on the value's runtime class
59
+ DECORATION = 8 # @render_func(...) kwargs on the view func
60
+ INSTANCE_ATTR = 9 # whitelisted instance attr on the value object
61
+ # (core_render.OBJ_ATTR_PARAMS, e.g.
62
+ # Lora.tint) - injected via setdefault, so
63
+ # every kwargs-borne source above wins
64
+ CLASS_VAR = 10 # class-body assignment on the value's class -
65
+ # reaches the view through the SAME getattr
66
+ # injection as INSTANCE_ATTR, where the
67
+ # instance var shadows it (Python lookup
68
+ # order), so it ranks below the instance
69
+ CODEC = 11 # the active codec's render_kwargs - the
70
+ # wrapper's lowest kwargs MERGE layer
71
+ # (core_render `render_kwargs | ...`);
72
+ # loses to every getattr-injected source
73
+ # above, drives only the layers below
74
+ RENDER_FUNC = 12 # signature defaults (def draw_x(speed=3)) —
75
+ # the WEAKEST code source: Python only applies
76
+ # a default when the name is absent from
77
+ # kwargs entirely, so every kwargs-borne
78
+ # source above wins. Ranked at 2 it used to
79
+ # mask caller/child_kwargs as _setting_source
80
+ # for any signature-defaulted param
81
+ # (syntax_highlight on draw_text), sending
82
+ # set_anywhere's ds_fallback when a draw_state
83
+ # write the explicit kwarg then shadowed
84
+ DRAW_STATE = 13 # the draw_state's own attrs (ds.tint - the
85
+ # style cascade's lowest fallback, persisted
86
+ # with window state) - the default: drives
87
+ # it when nothing else sets the param
88
+
89
+
90
+ # The tab's kind captions → priority. Kinds are the single naming source
91
+ # (collect_input_sources stamps them); this is just the lookup.
92
+ _KIND_TO_PRIORITY = {
93
+ "mode": SourcePriority.MODE,
94
+ "signature": SourcePriority.RENDER_FUNC,
95
+ "code class default": SourcePriority.AT_DEFAULT_CODE_TYPE,
96
+ "class default": SourcePriority.AT_DEFAULT_OBJ_TYPE,
97
+ "code comment": SourcePriority.LIVE_COMMENT,
98
+ "class var": SourcePriority.CLASS_VAR,
99
+ "decoration": SourcePriority.DECORATION,
100
+ "window decoration": SourcePriority.WINDOW_DECORATION, # @window on the func
101
+ "class decoration": SourcePriority.WINDOW_DECORATION, # @window on the class
102
+ "glfw window decoration": SourcePriority.WINDOW_DECORATION, # @glfw_window on the func
103
+ "instance attr": SourcePriority.INSTANCE_ATTR,
104
+ "attr default": SourcePriority.AT_DEFAULT_OBJ_TYPE, # @defaults(attr="x", ...)
105
+ "child kwargs": SourcePriority.CHILD_KWARGS,
106
+ "mode child kwargs": SourcePriority.MODE_CHILD_KWARGS,
107
+ "codec": SourcePriority.CODEC,
108
+ "draw state": SourcePriority.DRAW_STATE,
109
+ }
110
+
111
+
112
+ def _source_priority(kind):
113
+ """Sort key for a source's kind caption: (SourcePriority value, depth).
114
+ Caller rows ("caller", "caller +1", ...) share one enum member with their
115
+ walk depth as the tiebreaker, so priority never hardcodes caller depth."""
116
+ if isinstance(kind, str) and kind.startswith("caller"):
117
+ depth = int(kind.split("+", 1)[1]) if "+" in kind else 0
118
+ return (SourcePriority.CALLER.value, depth)
119
+ p = _KIND_TO_PRIORITY.get(kind)
120
+ return (p.value, 0) if p is not None else (len(SourcePriority) + 1, 0)
121
+
122
+
123
+ def _sources_for(draw_state, class_to_show=None):
124
+ """collect_input_sources for a TARGET draw_state outside the context menu:
125
+ the host-caching ContextMenuState rides on the draw_state so repeat calls
126
+ (a drag writing per-release, say) reuse the parsed hosts. class_to_show
127
+ defaults to the value's runtime class, so the @defaults/class-var rows
128
+ resolve the same way here as under the context menu."""
129
+ from meltygui.state.inspection_state import ContextMenuState
130
+ from meltygui.core.rendering.render_dispatch import collect_input_sources
131
+ if not draw_state._call_site_captured and not draw_state._call_site_requested:
132
+ draw_state._call_site_requested = True
133
+ draw_state.invalidate_up(max_depth=6)
134
+ if class_to_show is None:
135
+ raw = draw_state._raw_input_value
136
+ # A view can SHOW a class itself (a @window class like Toggles):
137
+ # its class rows (@window(cls) / class vars / @defaults) anchor on
138
+ # THAT class - `raw.__class__` would hand them `type`.
139
+ class_to_show = raw if isinstance(raw, type) else getattr(raw, "__class__", None)
140
+ cm_state = getattr(draw_state, "_sa_cm_state", None)
141
+ if cm_state is None:
142
+ cm_state = ContextMenuState()
143
+ draw_state._sa_cm_state = cm_state
144
+ srcs = collect_input_sources(draw_state, cm_state, class_to_show)
145
+ # Keep-alive + revive: code hosts are EVICTABLE - the idle sweep pops them
146
+ # off the draw_state once their consumers close (RenderHost.sweep). A
147
+ # swept host still passes writes: the tree stays dirty but nothing runs
148
+ # its chain_out, so the write silently never fires (set_anywhere writing
149
+ # into a dirty orphan was the first field failure). notify_on_change is
150
+ # the sanctioned per-frame pulse: stamps liveness AND re-registers a
151
+ # swept host that the upstream straining. Same thing the input
152
+ # tab does at its end with its own draw_state.
153
+ for _h in (cm_state.render_func_dict, getattr(cm_state, "decoration_dict", None), cm_state.class_dict,
154
+ cm_state.mode_dict,
155
+ *[dh for (_sh, dh) in (cm_state.call_site_hosts or [])]):
156
+ if _h is not None:
157
+ _h.notify_on_change(draw_state)
158
+ return srcs
159
+
160
+
161
+ _UNSET = object()
162
+
163
+
164
+ # Source layers that OUTRANK the draw_state/auto-state layer in the wrapper's
165
+ # kwargs gauntlet: their-set values (mode overrides, caller kwargs,
166
+ # child_kwargs, the comment splat, @window decoration kwargs). A write to a
167
+ # param driven by one of these must edit the source or an edit would be
168
+ # shadowed at runtime. Everything below (signature defaults, @render_func
169
+ # kwargs, @defaults, class vars, instance attrs, codec render_kwargs) merges
170
+ # BENEATH auto-state - a plain draw_state write both takes effect immediately
171
+ # and persists (auto_params), so it is the default write target.
172
+ _ABOVE_DRAW_STATE = {
173
+ SourcePriority.LIVE_COMMENT.value, SourcePriority.MODE.value,
174
+ SourcePriority.WINDOW_DECORATION.value, SourcePriority.CALLER.value,
175
+ SourcePriority.MODE_CHILD_KWARGS.value, SourcePriority.CHILD_KWARGS.value,
176
+ }
177
+
178
+
179
+ # ── source speed: fast vs slow writers ──────────────────────────────────────
180
+ # A source's SPEED is how a write becomes LIVE. Fast sources apply in place -
181
+ # a draw_state field, a live instance attr, the codec's in-memory
182
+ # render_kwargs - one setattr/item-set and the next frame reads it. Slow
183
+ # sources are code-backed: the write lands in a parse dict and only becomes
184
+ # live through the debounced chain_out → save + recompile/hotswap trip. That
185
+ # trip is fine per click, but a DRAG writes every frame, and one trip per
186
+ # frame craters the frame rate (120 to ~30fps measured on the voxel camera
187
+ # driving a `# [cam_brightness=...]` comment). So while a left/right/middle
188
+ # mouse button is held - or a scroll is in flight (no release event, so
189
+ # "recent tick within the same interval") - slow writes DEFER: the value
190
+ # lands in the in-flight display cache (_sa_pending - anywhere_value/
191
+ # locate_* reads already prefer it) plus a per-ds deferred dict, and the
192
+ # real set_anywhere runs ONCE, when input goes quiet, via
193
+ # flush_deferred_writes (called per frame by draw_main).
194
+ _FAST_PRIORITIES = {
195
+ SourcePriority.DRAW_STATE.value, SourcePriority.INSTANCE_ATTR.value,
196
+ SourcePriority.CODEC.value,
197
+ }
198
+
199
+
200
+ def source_is_slow(kind):
201
+ """True when a write to a source of this kind round-trips through
202
+ save/recompile rather than applying in place."""
203
+ return _source_priority(kind)[0] not in _FAST_PRIORITIES
204
+
205
+
206
+ # ── source precision: low-precision writers ─────────────────────────────────
207
+ # Some sources store floats as literal TEXT in code, where full float64
208
+ # precision is noise (`# [cam_x=0.30000000000000004]`). Kinds marked here get
209
+ # their float writes rounded to LOW_PRECISION_DECIMALS before landing in the
210
+ # source; the FULL-precision value parks on the draw_state's _sa_precise
211
+ # overlay, which anywhere_value serves for as long as the live value is still
212
+ # a rounding of it - the view sees the precise float (a voxel camera is
213
+ # accumulating sub-4dp drag deltas) while the code keeps a readable one. A
214
+ # write that ONLY moves digits beyond the cap skips the save/recompile trip
215
+ # entirely. The overlay is in-memory: an app reload sees the rounded source
216
+ # value (accepted trade-off). Mark a new source by adding its kind here.
217
+ LOW_PRECISION_DECIMALS = 4
218
+ _LOW_PRECISION_KINDS = {"code comment", "class var"}
219
+
220
+
221
+ def source_is_low_precision(kind):
222
+ """True when float writes to a source of this kind are rounded to
223
+ LOW_PRECISION_DECIMALS decimal places (see the block comment above)."""
224
+ return kind in _LOW_PRECISION_KINDS
225
+
226
+
227
+ def _round_low_precision(value):
228
+ """`value` rounded to LOW_PRECISION_DECIMALS — elementwise for float
229
+ sequences; anything without a float passes through untouched."""
230
+ if isinstance(value, float):
231
+ return round(value, LOW_PRECISION_DECIMALS)
232
+ if (isinstance(value, (tuple, list))
233
+ and any(isinstance(v, float) for v in value)):
234
+ seq = tuple if isinstance(value, tuple) else list
235
+ return seq(_round_low_precision(v) for v in value)
236
+ return value
237
+
238
+
239
+ def _is_rounding_of(live, full):
240
+ """True when `live` is `full` rounded at LOW_PRECISION_DECIMALS or
241
+ coarser — i.e. the source still holds OUR write (possibly re-capped by
242
+ the source's float formatter), not an external edit."""
243
+ try:
244
+ if live is full or bool(live == full):
245
+ return True
246
+ except Exception:
247
+ return False
248
+ if (isinstance(full, float) and isinstance(live, (int, float))
249
+ and not isinstance(live, bool)):
250
+ return any(live == round(full, dp)
251
+ for dp in range(LOW_PRECISION_DECIMALS + 1))
252
+ if (isinstance(full, (tuple, list)) and isinstance(live, (tuple, list))
253
+ and len(full) == len(live)):
254
+ return all(_is_rounding_of(lv, fv) for lv, fv in zip(live, full))
255
+ return False
256
+
257
+
258
+ def _stamp_precise(attr_name, value, draw_state):
259
+ """Park the full-precision value a low-precision source write rounded
260
+ away; anywhere_value serves it over the rounded live value."""
261
+ precise = getattr(draw_state, "_sa_precise", None)
262
+ if precise is None:
263
+ precise = {}
264
+ draw_state._sa_precise = precise
265
+ precise[attr_name] = value
266
+
267
+
268
+ def _precise_or_live(attr_name, draw_state, live):
269
+ """The full-precision overlay for `attr_name` while the live value is
270
+ still a rounding of it; once the source moves elsewhere (an external
271
+ edit, another driver) the overlay drops and live reads resume."""
272
+ precise = getattr(draw_state, "_sa_precise", None)
273
+ if precise is None or attr_name not in precise:
274
+ return live
275
+ full = precise[attr_name]
276
+ if _is_rounding_of(live, full):
277
+ return full
278
+ del precise[attr_name]
279
+ return live
280
+
281
+
282
+ _DRAG_BUTTONS = ("left_mouse", "right_mouse", "middle_mouse")
283
+
284
+
285
+ def _drag_active():
286
+ """A mouse button is currently held — the window during which slow-source
287
+ writes defer. Level state from the input handler (not per-frame events),
288
+ so it can't miss between drag events."""
289
+ h = getattr(Core.melty, "event_handler", None)
290
+ return h is not None and any(h.is_down(b) for b in _DRAG_BUTTONS)
291
+
292
+
293
+ # Scroll has no release: a gesture is "over" once no tick has arrived for the
294
+ # quiet window. Wheel notches land ~100ms+ apart, so the window must span the
295
+ # inter-tick gap or every notch would flush its own save/recompile trip.
296
+ _SCROLL_QUIET_FRAMES = 24
297
+ # Frame of the last seen scroll event - module registry, survives hotswap.
298
+ _last_scroll_frame = globals().get("_last_scroll_frame", [-1_000_000])
299
+
300
+
301
+ def _note_scroll():
302
+ """Stamp the frame when a scroll event is in flight. events_by_type is
303
+ drained at frame start (begin_frame), so the flush call at the top of
304
+ draw_main sees this frame's ticks before any view's handler writes."""
305
+ if "scroll_y_changed" in (Core.melty.events_by_type or {}):
306
+ _last_scroll_frame[0] = Core.melty.frame_count
307
+
308
+
309
+ def _scroll_recent():
310
+ return Core.melty.frame_count - _last_scroll_frame[0] < _SCROLL_QUIET_FRAMES
311
+
312
+
313
+ def _input_busy():
314
+ """True while a gesture that should hold off slow writes is in flight —
315
+ a held drag button, or a scroll within its quiet window."""
316
+ return _drag_active() or _scroll_recent()
317
+
318
+
319
+ # Draw_states holding deferred writes, flushed on release. Module registry -
320
+ # survives hotswap (re-exec reuses the existing global).
321
+ _DEFERRED_DS = globals().get("_DEFERRED_DS", set())
322
+
323
+
324
+ def _stamp_pending(attr_name, value, draw_state):
325
+ """In-flight display cache (see anywhere_value): remember what was set and
326
+ what the live value was when the set was issued. Re-sets during a drag
327
+ refresh the UI value but KEEP the original live_at_set — live hasn't moved
328
+ yet, and that's the baseline whose change means "the trip landed"."""
329
+ live = (draw_state._kwargs or {}).get(attr_name, _UNSET)
330
+ if attr_name == "view_func":
331
+ live = getattr(draw_state, "_wrapper", None) or draw_state._view_func
332
+ pending = getattr(draw_state, "_sa_pending", None)
333
+ if pending is None:
334
+ pending = {}
335
+ draw_state._sa_pending = pending
336
+ prior = pending.get(attr_name)
337
+ # _UNSET normalizes to None: anywhere_value reads live with a None
338
+ # default, and the baselines must compare equal until the trip lands.
339
+ live_at_set = prior[1] if prior is not None else (
340
+ None if live is _UNSET else live)
341
+ pending[attr_name] = (value, live_at_set)
342
+ getattr(draw_state, "_sa_verify", {}).pop(attr_name, None)
343
+
344
+
345
+ # ── Live write: the in-memory twin of the recompile ─────────────────────────
346
+ # A write to a DECORATOR source (`@window(tint=...)`, `@glfw_window(tint=...)`,
347
+ # `@render_func(tint=...)`) only became visible once the mouse-up recompile
348
+ # re-ran the decorator and the reconcile installed its fresh kwargs. Every
349
+ # render reads those kwargs from their place - the same place the recompile
350
+ # replaces - so a drag can write the value straight there and the window
351
+ # follows the picker live; the recompile on release lands the same value
352
+ # and nothing moves. Each case: kind caption → apply(attr, value,
353
+ # draw_state, class_to_show) → True when a registration was hit.
354
+ def _apply_window_decoration(attr_name, value, draw_state, class_to_show):
355
+ """`Melty.annotated_window_classes[name] = (obj, kwargs)`: draw_main
356
+ copies `kwargs` into the window's call every frame. Matched on the
357
+ registered OBJECT (the view's wrapper or raw for a @window func, the
358
+ shown class for @window(cls)), never on the key — a `name=` registration
359
+ keys by that name. The dict is looked up per call: the recompile's
360
+ reconcile swaps in a fresh one."""
361
+ registry = getattr(Core.melty, "annotated_window_classes", None)
362
+ if not isinstance(registry, dict):
363
+ return False
364
+ view_func = (draw_state._kwargs or {}).get("_view_func_origin", getattr(draw_state, "_view_func", None))
365
+ owners = {id(o) for o in (view_func, _raw_of(view_func), class_to_show) if o is not None}
366
+ hit = False
367
+ for entry in registry.values():
368
+ if isinstance(entry, tuple) and len(entry) == 2 and id(entry[0]) in owners \
369
+ and isinstance(entry[1], dict):
370
+ entry[1][attr_name] = value
371
+ hit = True
372
+ return hit
373
+
374
+
375
+ def _apply_glfw_window_decoration(attr_name, value, draw_state, class_to_show):
376
+ """`app._ROOTS[i] = (fn, config)`: the root body reads
377
+ `config['view_kwargs']` every frame (app._root_body), and a re-run
378
+ decorator updates that same dict in place."""
379
+ import meltygui.core.runtime.app as app
380
+ view_func = (draw_state._kwargs or {}).get("_view_func_origin", getattr(draw_state, "_view_func", None))
381
+ owners = {id(o) for o in (view_func, _raw_of(view_func)) if o is not None}
382
+ hit = False
383
+ for fn, config in app._ROOTS:
384
+ if id(fn) in owners or id(_raw_of(fn)) in owners:
385
+ kwargs = config.get('view_kwargs')
386
+ if not isinstance(kwargs, dict):
387
+ kwargs = config['view_kwargs'] = {}
388
+ kwargs[attr_name] = value
389
+ hit = True
390
+ return hit
391
+
392
+
393
+ def _apply_render_func_decoration(attr_name, value, draw_state, class_to_show):
394
+ """`@render_func(tint=…)` lives in the wrapper's closure: the `o_kwargs`
395
+ dict (merged under every call's kwargs) and, for the tint, the
396
+ `_decoration_tint` cell the wrapper compares identities against. The
397
+ hotswap's _transfer_wrapper_state later overwrites both cells with the
398
+ freshly-decorated values — the same ones."""
399
+ wrapper = getattr(draw_state, "_view_func", None)
400
+ code = getattr(wrapper, "__code__", None)
401
+ cells = getattr(wrapper, "__closure__", None)
402
+ if code is None or cells is None:
403
+ return False
404
+ names = code.co_freevars
405
+ if "o_kwargs" not in names:
406
+ return False
407
+ o_kwargs = cells[names.index("o_kwargs")].cell_contents
408
+ if not isinstance(o_kwargs, dict):
409
+ return False
410
+ o_kwargs[attr_name] = value
411
+ if attr_name == "tint":
412
+ # The identity the wrapper's `_tint_as_decoration` compares against
413
+ # sits in that nested helper's own closure (a cell of the wrapper).
414
+ for cell in _decoration_tint_cells(wrapper):
415
+ cell.cell_contents = value
416
+ return True
417
+
418
+
419
+ def _decoration_tint_cells(wrapper):
420
+ """The `_decoration_tint` cells reachable from a render_func wrapper:
421
+ on the wrapper itself or on a nested function it closes over."""
422
+ found = []
423
+ seen = set()
424
+ stack = [wrapper]
425
+ while stack:
426
+ fn = stack.pop()
427
+ if id(fn) in seen:
428
+ continue
429
+ seen.add(id(fn))
430
+ code = getattr(fn, "__code__", None)
431
+ cells = getattr(fn, "__closure__", None)
432
+ if code is None or not cells:
433
+ continue
434
+ for name, cell in zip(code.co_freevars, cells):
435
+ try:
436
+ content = cell.cell_contents
437
+ except ValueError:
438
+ continue
439
+ if name == "_decoration_tint":
440
+ found.append(cell)
441
+ elif callable(content) and getattr(content, "__closure__", None):
442
+ stack.append(content)
443
+ return found
444
+
445
+
446
+ def _raw_of(func):
447
+ try:
448
+ import inspect
449
+ return inspect.unwrap(func) if func is not None else None
450
+ except Exception:
451
+ return None
452
+
453
+
454
+ _LIVE_APPLY = {
455
+ "window decoration": _apply_window_decoration,
456
+ "class decoration": _apply_window_decoration,
457
+ "glfw window decoration": _apply_glfw_window_decoration,
458
+ "decoration": _apply_render_func_decoration,
459
+ }
460
+
461
+
462
+ def live_apply(attr_name, value, draw_state, kind, class_to_show=None):
463
+ """Write `value` into the in-memory registration a source of `kind`
464
+ feeds the render loop from (_LIVE_APPLY), so the view shows it on the
465
+ next frame without waiting for the save + recompile. False for kinds
466
+ that only become live through a recompile (signature, @defaults,
467
+ callers, mode) — those keep the display cache alone."""
468
+ apply = _LIVE_APPLY.get(kind)
469
+ if apply is None:
470
+ return False
471
+ try:
472
+ hit = apply(attr_name, value, draw_state, class_to_show)
473
+ except Exception as e:
474
+ print(f"[live_apply] {kind} {attr_name}: {e}")
475
+ return False
476
+ if hit:
477
+ from meltygui.core.windowing.glfw_utils import request_render
478
+ request_render()
479
+ return hit
480
+
481
+
482
+ def _defer_write(attr_name, value, draw_state, class_to_show, source=None, kind=None):
483
+ """Park a slow-source write for the duration of the drag: the display
484
+ cache serves reads immediately; flush_deferred_writes runs the real
485
+ set_anywhere on release."""
486
+ deferred = getattr(draw_state, "_sa_deferred", None)
487
+ if deferred is None:
488
+ deferred = {}
489
+ draw_state._sa_deferred = deferred
490
+ deferred[attr_name] = (value, class_to_show, source, kind)
491
+ _DEFERRED_DS.add(draw_state)
492
+ _stamp_pending(attr_name, value, draw_state)
493
+ live_apply(attr_name, value, draw_state, kind, class_to_show)
494
+
495
+
496
+ def flush_deferred_writes():
497
+ """Per-frame (draw_main): once no drag button is held, run each parked
498
+ write through the normal set_anywhere — one slow trip per gesture, not
499
+ one per event. A no-op set-check when nothing is parked."""
500
+ _note_scroll()
501
+ if not _DEFERRED_DS:
502
+ return
503
+ if _input_busy():
504
+ # Rendering is event-driven: after the last scroll tick no further
505
+ # input arrives, so keep frames flowing until the quiet window expires
506
+ # and the deferred writes actually flush.
507
+ from meltygui.core.windowing.glfw_utils import request_render
508
+ request_render()
509
+ return
510
+ for ds in list(_DEFERRED_DS):
511
+ _DEFERRED_DS.discard(ds)
512
+ deferred = getattr(ds, "_sa_deferred", None) or {}
513
+ items = list(deferred.items())
514
+ deferred.clear()
515
+ for attr_name, (value, class_to_show, source, _kind) in items:
516
+ set_anywhere(attr_name, value, ds, class_to_show=class_to_show,
517
+ allow_any=True, ds_fallback=True, source=source)
518
+
519
+
520
+ def _pref_matches(pref, kind):
521
+ """True when a source row's kind caption satisfies a preferred_source
522
+ flag — given either as a kind caption string ("code comment") or a
523
+ SourcePriority member (matched through the same priority table)."""
524
+ if isinstance(pref, SourcePriority):
525
+ return _source_priority(kind)[0] == pref.value
526
+ return kind == pref
527
+
528
+
529
+ def _preferred_source_for(draw_state):
530
+ """The `preferred_source` flag riding this view's resolved kwargs, or an
531
+ enclosing window's — a spawning view (live view) stamps it on the value
532
+ windows it opens, and edits from anywhere in that subtree (params panel
533
+ rows, context menus) should land at the nominated source. Walks _parent
534
+ (self-loop root) then hops parent_window, same as other ancestor walks."""
535
+ node, hops = draw_state, 0
536
+ while node is not None and hops < 32:
537
+ pref = (getattr(node, "_kwargs", None) or {}).get("preferred_source")
538
+ if pref is not None:
539
+ return pref
540
+ parent = getattr(node, "_parent", None)
541
+ nxt = parent if parent is not None and parent is not node else None
542
+ if nxt is None:
543
+ pw = getattr(node, "parent_window", None)
544
+ nxt = pw if pw is not None and pw is not node else None
545
+ node = nxt
546
+ hops += 1
547
+ return None
548
+
549
+
550
+ def get_value_for_source(attr_name, input_source, draw_state, class_to_show=None):
551
+ """(input_source, value) for `attr_name` as set by a specific
552
+ SourcePriority source on this view, or (input_source, None) when that
553
+ source doesn't set it. The same registry the input tab shows."""
554
+ srcs = _sources_for(draw_state, class_to_show)
555
+ for sname, sdict in srcs["sources"].items():
556
+ kind = srcs["kinds"].get(sname)
557
+ if _source_priority(kind)[0] == input_source.value and attr_name in sdict:
558
+ return input_source, sdict[attr_name]
559
+ return input_source, None
560
+
561
+
562
+ def _unset_value(v):
563
+ """True when a stored value can't DRIVE a param: None (declared-unset) or
564
+ a fully transparent color (an alpha-0 4-tuple — the codec opt-out
565
+ convention; it renders nothing, so it must not claim the pick)."""
566
+ if v is None:
567
+ return True
568
+ return (isinstance(v, (tuple, list)) and len(v) >= 4
569
+ and isinstance(v[3], (int, float)) and not v[3])
570
+
571
+
572
+ def _setting_source(srcs, attr_name):
573
+ """The highest-priority (SourcePriority order) WRITABLE source that
574
+ actually SETS `attr_name`, or None when no source does. Split out from
575
+ _driving_source so a caller can tell "some source holds this value" from
576
+ "nothing does, the pick is only the stamp-fallback"."""
577
+ sources, kinds = srcs["sources"], srcs["kinds"]
578
+ writable = set(srcs["writable"])
579
+ # `is not None`: a parse'd `param=None` (signature defaults, cleared
580
+ # kwargs) is DECLARED-UNSET - it must not claim driving over a source
581
+ # holding a real value (the signature's child_kwargs=None poisoned
582
+ # from_anywhere for every real setter below it).
583
+ candidates = [sname for sname in sources
584
+ if sname in writable
585
+ and not _unset_value(sources[sname].get(attr_name))]
586
+ if attr_name == "view_func" and srcs.get("view_func") is not None:
587
+ import inspect
588
+ from meltygui.core.input.view_selection import resolve_view_func
589
+ live = inspect.unwrap(resolve_view_func(srcs["view_func"]))
590
+ candidates = [s for s in candidates if not (getattr(sources[s], "direct", False)
591
+ or getattr(sources[s], "implicit_view_func", False))
592
+ or inspect.unwrap(resolve_view_func(sources[s][attr_name])) is live]
593
+ if not candidates:
594
+ return None
595
+ return min(candidates, key=lambda s: _source_priority(kinds.get(s)))
596
+
597
+
598
+ def _driving_source(srcs, attr_name):
599
+ """The source name actually driving `attr_name`: the highest-priority
600
+ WRITABLE source that currently sets it, else the signature source as the
601
+ stamp-fallback, else None. Shared by get_source_for / from_anywhere /
602
+ set_anywhere so they can never disagree."""
603
+ target = _setting_source(srcs, attr_name)
604
+ if target is not None:
605
+ return target
606
+ kinds, writable = srcs["kinds"], set(srcs["writable"])
607
+ if attr_name == "view_func":
608
+ return next((s for s in srcs["sources"]
609
+ if kinds.get(s) == "draw state" and s in writable), None)
610
+ return next((s for s in srcs["sources"]
611
+ if kinds.get(s) == "signature" and s in writable), None)
612
+
613
+
614
+ def get_source_for(attr_name, draw_state, class_to_show=None):
615
+ """Name of the source driving `attr_name` on this view (display label)."""
616
+ return _driving_source(_sources_for(draw_state, class_to_show), attr_name)
617
+
618
+
619
+ # Call sites in these framework locations are BAD default write targets - a
620
+ # kwarg stamped into view_collection's dispatch call, the render loop
621
+ # (end_frame / update_melty_windows / the studio's draw), or any core_view
622
+ # plumbing would restyle every view app-wide. Caller rows located here are
623
+ # skipped by default_write_source's pick - they stay visible and manually
624
+ # pickable in the info tab's dropdown.
625
+ _FRAMEWORK_CALLER_DIRS = ("/meltygui/rendering/", "/meltygui/views/",
626
+ "/meltygui/code/", "/meltygui/state/", "/meltygui/utils/",
627
+ "/meltygui/editor/")
628
+ _FRAMEWORK_CALLER_FILES = ("melty.py", "app.py", "surface.py", "background.py")
629
+
630
+
631
+ def _is_framework_caller(location):
632
+ """True when a caller row's (file, line) sits inside the meltygui framework
633
+ — or is unknown, which must never be defaulted into either."""
634
+ if not location or not location[0]:
635
+ return True
636
+ p = str(location[0]).replace("\\", "/")
637
+ if "/meltygui/" in p:
638
+ return True
639
+ # Component packages built on the toolkit register their own folders.
640
+ from meltygui.core.runtime.extensions import framework_folders
641
+ if any(str(folder).replace("\\", "/") in p for folder in framework_folders()):
642
+ return True
643
+ if any(d in p for d in _FRAMEWORK_CALLER_DIRS):
644
+ return True
645
+ return ("/meltygui/" in p
646
+ and p.rsplit("/", 1)[-1] in _FRAMEWORK_CALLER_FILES)
647
+
648
+
649
+ def window_source_writable(srcs, target):
650
+ """Automatic window gestures may always edit comments/local state.
651
+
652
+ Source-code changes require the GUI-editor toggle; framework dispatch
653
+ call sites are never an automatic target, even with that toggle enabled.
654
+ """
655
+ from meltygui.core.runtime.toggles import Toggles
656
+ kind = srcs['kinds'].get(target)
657
+ if kind in ('code comment', 'draw state'):
658
+ return True
659
+ if not Toggles.dangerous_edit_mode:
660
+ return False
661
+ if isinstance(kind, str) and kind.startswith('caller'):
662
+ return not _is_framework_caller(srcs['locations'].get(target))
663
+ return target is not None
664
+
665
+
666
+ def window_position_movable(draw_state, kwargs):
667
+ if kwargs.get('window_pos') is None:
668
+ return True
669
+ # Live windows nominate their comment even before it has an entry.
670
+ if kwargs.get('preferred_source') == 'code comment':
671
+ return True
672
+ srcs = _sources_for(draw_state)
673
+ return window_source_writable(srcs, _setting_source(srcs, 'window_pos'))
674
+
675
+
676
+ def default_write_source(attr_name, draw_state, class_to_show=None, srcs=None):
677
+ """The source name a NEW write of `attr_name` should default to — the
678
+ info tab's picker preselection.
679
+
680
+ 1. A source already setting the attr wins (the normal driving pick).
681
+ 2. Otherwise, use the source setting view_func when available.
682
+ 3. With no view_func source, use the view's OTHER params as a cue: the writable source
683
+ already defining the most of them is where this view is being
684
+ configured, so a new param belongs there too (SourcePriority as the
685
+ tie-breaker). The signature is excluded — it defines EVERY param by
686
+ construction and would always win — and so is the draw_state (it's
687
+ the fallback, not a configuration site).
688
+ 4. No cue at all: the highest-priority writable code source, else the
689
+ draw_state."""
690
+ if srcs is None:
691
+ srcs = _sources_for(draw_state, class_to_show)
692
+ writable = set(srcs["writable"])
693
+
694
+ def _eligible(sname):
695
+ kind = srcs["kinds"].get(sname)
696
+ if sname not in writable or kind in ("signature", "draw state"):
697
+ return False
698
+ if isinstance(kind, str) and kind.startswith("caller"):
699
+ # Caller row names are the frame's function name (with an
700
+ # optional " ^N" dedup suffix). A dunder name (__call__ - a
701
+ # wrapper/dispatch protocol) is machinery regardless of where
702
+ # it lives, never a place to stamp a view kwarg.
703
+ fn = sname.split(" ^", 1)[0]
704
+ if fn.startswith("__") and fn.endswith("__"):
705
+ return False
706
+ return not _is_framework_caller(srcs["locations"].get(sname))
707
+ return True
708
+
709
+ target = _setting_source(srcs, attr_name)
710
+ if target is not None and _eligible(target):
711
+ return target
712
+ target = _setting_source(srcs, "view_func")
713
+ if target is not None:
714
+ return target
715
+ others = set(view_param_names(draw_state))
716
+ others.discard(attr_name)
717
+
718
+ candidates = [s for s in srcs["sources"] if _eligible(s)]
719
+ best, best_key = None, None
720
+ for sname in candidates:
721
+ sdict = srcs["sources"][sname]
722
+ try:
723
+ count = sum(1 for k in sdict if k in others)
724
+ except Exception:
725
+ count = 0
726
+ if not count:
727
+ continue
728
+ key = (-count, _source_priority(srcs["kinds"].get(sname)))
729
+ if best_key is None or key < best_key:
730
+ best, best_key = sname, key
731
+ if best is not None:
732
+ return best
733
+ if candidates:
734
+ return min(candidates,
735
+ key=lambda s: _source_priority(srcs["kinds"].get(s)))
736
+ return "draw_state"
737
+
738
+
739
+ def from_anywhere(attr_name, draw_state, class_to_show=None, default=None):
740
+ """Read `attr_name` from whichever source is driving it — the read mirror
741
+ of set_anywhere, resolved through the same registry and priority pick.
742
+ `default` when no source sets the attr (the driving pick may be the
743
+ signature FALLBACK, which doesn't set it yet — a + affordance case)."""
744
+ srcs = _sources_for(draw_state, class_to_show)
745
+ target = _driving_source(srcs, attr_name)
746
+ if target is None:
747
+ return default
748
+ return srcs["sources"][target].get(attr_name, default)
749
+
750
+
751
+ # Parameters the set-anywhere round trip supports. The trip is multi-frame
752
+ # (write → debounced chain_out → save → hotswap → new o_kwargs), so supported
753
+ # params also get the in-flight display cache below; grow this list as params
754
+ # are verified end-to-end. This gates DIRECT set_anywhere usage: both generic
755
+ # accessors - `draw_state.locate_<param>` and the ParamProxy - pass
756
+ # allow_any=True, but the point of an arbitrary-name accessor is that any
757
+ # param on the view is settable.
758
+ SET_ANYWHERE_PARAMS = ("tint", "view_func")
759
+
760
+
761
+ def anywhere_value(attr_name, draw_state, default=None, live_kwargs=None):
762
+ """The value a set-anywhere editor should DISPLAY for `attr_name`:
763
+ draw_state._kwargs — the framework-resolved truth — except while a
764
+ set_anywhere round trip is in flight, when it's the pending UI value
765
+ (cached on the draw_state by set_anywhere), so the widget doesn't snap
766
+ back to the stale value for the frames the write→save→hotswap takes.
767
+
768
+ Completion is "the live value MOVED off what it was when the set was
769
+ issued" — never equality with the set value, which round-trips through
770
+ source formatting (float reformat) and might never compare equal. Moving
771
+ to anything (our set landing, or someone else's edit) clears the entry
772
+ and live reads resume."""
773
+ _anywhere_recompile_tick(draw_state)
774
+ live = (draw_state._kwargs if live_kwargs is None else live_kwargs) or {}
775
+ live = live.get(attr_name)
776
+ if attr_name == "view_func":
777
+ live = getattr(draw_state, "_wrapper", None) or draw_state._view_func
778
+ if _unset_value(live):
779
+ live = None # alpha-0 = the codec opt-out; fall through
780
+ if live is None:
781
+ # The style cascade's own last fallback: the draw_state field
782
+ # (ds.tint) - the DRAW_STATE source. Without this, ds-tinted windows
783
+ # read as "no value" here while visibly wearing one.
784
+ live = getattr(draw_state, attr_name, None)
785
+ if live is None:
786
+ live = default
787
+ _anywhere_verify_tick(attr_name, draw_state, live)
788
+ pending = getattr(draw_state, "_sa_pending", None)
789
+ entry = pending.get(attr_name) if pending else None
790
+ if entry is None:
791
+ return _precise_or_live(attr_name, draw_state, live)
792
+ ui_value, live_at_set = entry
793
+ try:
794
+ moved = not (live is live_at_set or bool(live == live_at_set))
795
+ except Exception:
796
+ moved = True
797
+ if moved:
798
+ del pending[attr_name]
799
+ # The trip LANDED (live moved off the at-set baseline). Queue the
800
+ # value cross-check for 2 frames out - comparing any sooner fires
801
+ # on every valid set (the old write = check's failure mode).
802
+ verify = getattr(draw_state, "_sa_verify", None)
803
+ if verify is None:
804
+ verify = {}
805
+ draw_state._sa_verify = verify
806
+ verify[attr_name] = (ui_value, Core.melty.frame_count)
807
+ return _precise_or_live(attr_name, draw_state, live)
808
+ return ui_value
809
+
810
+
811
+ def _anywhere_agrees(a, b):
812
+ """Tolerant post-trip comparison: a set value round-trips through source
813
+ formatting (floats re-rendered at 2 decimals, a 3-tuple may come back
814
+ 4-long), so exact equality would flag working trips. Numeric sequences
815
+ compare elementwise over the common prefix at 0.01; incomparables pass
816
+ (no basis to warn)."""
817
+ if a is b:
818
+ return True
819
+ try:
820
+ if bool(a == b):
821
+ return True
822
+ except Exception:
823
+ return True
824
+ if (isinstance(a, (tuple, list)) and isinstance(b, (tuple, list))
825
+ and a and b):
826
+ try:
827
+ return all(abs(float(x) - float(y)) <= 0.01
828
+ for x, y in zip(a, b))
829
+ except (TypeError, ValueError):
830
+ return True
831
+ if isinstance(a, (int, float)) and isinstance(b, (int, float)):
832
+ return abs(float(a) - float(b)) <= 0.01
833
+ return True
834
+
835
+
836
+ def _anywhere_verify_tick(attr_name, draw_state, live):
837
+ """Deferred set_anywhere cross-check: 2 frames after a trip lands,
838
+ compare the live value against what was set. A real divergence here
839
+ means the SourcePriority pick wrote to a source that ISN'T driving this
840
+ view — the one thing the alert exists to catch."""
841
+ verify = getattr(draw_state, "_sa_verify", None)
842
+ entry = verify.get(attr_name) if verify else None
843
+ if entry is None:
844
+ return
845
+ set_value, landed_frame = entry
846
+ if Core.melty.frame_count - landed_frame < 2:
847
+ return
848
+ del verify[attr_name]
849
+ if not _anywhere_agrees(live, set_value):
850
+ from meltygui.core.diagnostics.notifications import notify
851
+ notify(f"set_anywhere: '{attr_name}' settled at {live!r}, not the "
852
+ f"{set_value!r} that was set — SourcePriority may not match "
853
+ f"meltygui's routing for this view", tag="set_anywhere")
854
+
855
+
856
+ def _owning_code_host(cm_state, kind):
857
+ """(str_host, source_obj) whose buffer a write of this kind lands in — the
858
+ pair the writer-side hotswap drives. None for kinds that apply LIVE with
859
+ no compile (comment splat, instance attr) or aren't wired yet (callers)."""
860
+ if kind in ("decoration", "window decoration", "glfw window decoration") and getattr(cm_state, "decoration_str", None) is not None:
861
+ return cm_state.decoration_str, cm_state.decoration_key
862
+ if kind in ("signature", "decoration", "window decoration", "glfw window decoration"):
863
+ return cm_state.render_func_str, (cm_state.host_key or (None, None))[0]
864
+ if kind in ("class var", "class default", "class decoration"):
865
+ return cm_state.class_str, (cm_state.host_key or (None, None))[1]
866
+ if isinstance(kind, str) and kind.startswith("caller"):
867
+ # One host pair per walked frame (_collect_input_sources), indexed by
868
+ # the kind's depth suffix. The str host's input is the CallSite, which
869
+ # recompile_source hotswaps via _recompile_caller (the ENCLOSING
870
+ # function, with the edited statement spliced-in).
871
+ depth = int(kind.split("+", 1)[1]) if "+" in kind else 0
872
+ hosts = cm_state.call_site_hosts or []
873
+ if depth < len(hosts) and hosts[depth][0] is not None:
874
+ _sh = hosts[depth][0]
875
+ return _sh, getattr(_sh, "input_value", None)
876
+ return None, None
877
+ if kind in ("mode", "mode child kwargs"):
878
+ # "mode child kwargs" rows are registered off the PARENT's mode host,
879
+ # but code_hosts_for caches per-mode reference, so the target's own
880
+ # mode host (same enum class) is the same host pair. A leaf whose
881
+ # current_mode enum differs from the parent's would stamp the wrong
882
+ # host and the recompile wait times out harmlessly.
883
+ return cm_state.mode_str, cm_state.mode_key
884
+ return None, None
885
+
886
+
887
+ def _anywhere_recompile_tick(draw_state):
888
+ """Writer-side hotswap driver, run every frame the anywhere UI reads a
889
+ value (anywhere_value). set_anywhere stamps what it's waiting for; the
890
+ tick starts the recompile the moment the owning host's buffer moves off
891
+ the pre-write snapshot (the debounced chain_out landing), then keeps the
892
+ runner polled until the hotswap lands. This lives ENTIRELY on the writer
893
+ side — code_file_io has no edit-driven recompile (the cache hosts also
894
+ back visible editor panes, where any `edited` trigger fires per
895
+ keystroke)."""
896
+ pend = getattr(draw_state, "_sa_recompile", None)
897
+ if not pend:
898
+ return
899
+ # Once per frame: anywhere_value calls this per PARAM (the params panel /
900
+ # info tab read every param each refresh), and a second run_recompile
901
+ # of the same name in one frame triggers run_in_background's duplicate-
902
+ # unique detection (sa_recompile<u> : Forcing re-render" spam).
903
+ if pend.get("tick_frame") == Core.melty.frame_count:
904
+ return
905
+ pend["tick_frame"] = Core.melty.frame_count
906
+ from meltygui.code.new_converters import host_code_state
907
+ from meltygui.code.new_converters import run_recompile
908
+ # Keep the owning host alive through the wait - it may be an idle-swept
909
+ # cache host that still needs to load and run its chain.
910
+ pend["host"].notify_on_change(draw_state)
911
+ cs = host_code_state(pend["host"])
912
+ if cs is None or cs.address is None:
913
+ return
914
+ if pend["buf"] is None:
915
+ # Stamped before the host had a code_state: adopt the first buffer we
916
+ # see as the pre-write baseline and wait for it to move.
917
+ pend["buf"] = cs.text_cache
918
+ return
919
+ start = False
920
+ if not pend["started"]:
921
+ if cs.text_cache is pend["buf"]:
922
+ return # chain_out hasn't landed yet
923
+ pend["started"] = True
924
+ pend["start_frame"] = Core.melty.frame_count
925
+ start = True
926
+ run_recompile(pend["source"], cs, draw_state, start=start,
927
+ name=f"sa_recompile{draw_state.unique}")
928
+ landed = (cs._recompiled_on_frame is not None
929
+ and cs._recompiled_on_frame >= pend["start_frame"])
930
+ # Timeout only counts AFTER the compile started - comparing an unstarted
931
+ # pend's start_frame=0 against the session frame count cleared every
932
+ # stamp on its first tick (the runner silently never ran).
933
+ if landed or (pend["started"]
934
+ and Core.melty.frame_count - pend["start_frame"] > 600):
935
+ draw_state._sa_recompile = None
936
+ # Switching draw_any's renderer can retire this DrawState. If it is
937
+ # reused later, its old baseline may still equal the live renderer;
938
+ # don't resurrect a selection whose recompile already completed.
939
+ pending = getattr(draw_state, "_sa_pending", None)
940
+ if pending:
941
+ pending.pop("view_func", None)
942
+
943
+
944
+ def set_anywhere(attr_name, value, draw_state, class_to_show=None, allow_any=False,
945
+ ds_fallback=False, source=None):
946
+ """Set `attr_name` at whichever input source is actually driving it —
947
+ code, comment, decoration, mode entry — using the same registry the input
948
+ tab edits. The write is a plain item-set on the source's bubbling parse
949
+ dict, so the owning host goes dirty and saves through its normal chain.
950
+
951
+ The DRIVING source is the highest-priority (SourcePriority order) writable
952
+ source that currently sets the attr; when none sets it, the value stamps
953
+ alongside view_func, or into the signature defaults when no source sets
954
+ view_func (the + button's fallback).
955
+ Returns the source name written to, or None when nothing writable exists.
956
+
957
+ Sanity cross-check, not bulletproof: if the driving source's pre-write
958
+ value disagrees with the live draw_state._kwargs value, our hardcoded
959
+ priority table probably mis-ranked this view's sources — notify, don't
960
+ throw, and write anyway (the user asked for the set).
961
+
962
+ `allow_any` skips the SET_ANYWHERE_PARAMS gate — the generic accessors
963
+ pass it (the whole point of `locate_<param>` is that any signature param
964
+ is writable), while direct calls keep the whitelist.
965
+
966
+ `ds_fallback` changes what happens when NO source sets the param: instead
967
+ of stamping the signature default (the + affordance's behavior), the value
968
+ follows the source setting view_func, falling back to draw_state when
969
+ there is none. This also applies to auto-state params driven only by
970
+ default layers; higher-priority sources keep their existing writes.
971
+
972
+ `source` names an EXPLICIT target (a registered source name, or the
973
+ literal "draw_state") — the info tab's per-param picker passes it. It
974
+ overrides preferred_source and the driving pick entirely; the write can
975
+ also CREATE the entry at that source (a new caller kwarg, a new class
976
+ var), which is how the picker's + affordance stamps a param nothing
977
+ sets yet."""
978
+ from meltygui.core.diagnostics.notifications import notify
979
+ if not allow_any and attr_name not in SET_ANYWHERE_PARAMS:
980
+ notify(f"set_anywhere: '{attr_name}' not in SET_ANYWHERE_PARAMS",
981
+ tag="set_anywhere")
982
+ return None
983
+ if attr_name == "view_func":
984
+ from meltygui.core.input.view_selection import resolve_view_func
985
+ try:
986
+ value = resolve_view_func(value)
987
+ except ValueError as error:
988
+ notify(str(error), tag="set_anywhere")
989
+ return None
990
+ # Mid-drag repeat write to an already-deferred attr: skip the registry
991
+ # walk entirely - the drag's FIRST write resolved the target (slow) and
992
+ # parked it; later frames just stamp the parked value. This is what
993
+ # makes drag frames ~ instantaneous.
994
+ _deferred = getattr(draw_state, "_sa_deferred", None)
995
+ if _deferred and attr_name in _deferred and _input_busy():
996
+ _prev = _deferred[attr_name]
997
+ deferred_source = source if source is not None else _prev[2]
998
+ _kind = _prev[3]
999
+ _deferred[attr_name] = (value, class_to_show, deferred_source, _kind)
1000
+ _stamp_pending(attr_name, value, draw_state)
1001
+ live_apply(attr_name, value, draw_state, _kind, class_to_show)
1002
+ _last = getattr(draw_state, "_sa_last_source", None)
1003
+ return _last.get(attr_name) if _last else None
1004
+ srcs = _sources_for(draw_state, class_to_show)
1005
+ sources = srcs["sources"]
1006
+ # A spawning view can NOMINATE where edits land: live view stamps
1007
+ # preferred_source="code comment" on the value windows it spawns, so a
1008
+ # panel/menu edit targets the site's `# [<key>=...]` comment even when the
1009
+ # comment doesn't set the param yet - the write CREATES the entry there
1010
+ # (the registered row is a _LazyOverrideEntry when no comment exists,
1011
+ # and its first write materializes one). Falls through to the normal
1012
+ # pick when no writable source of that kind is registered (tree unparsed).
1013
+ target = None
1014
+ if source is not None:
1015
+ # Explicit target from the picker: honor it or fail loudly - silently
1016
+ # falling back to the automatic pick would write somewhere the user
1017
+ # didn't choose.
1018
+ _writable = set(srcs["writable"])
1019
+ if source in sources and source in _writable:
1020
+ target = source
1021
+ elif source == "draw_state":
1022
+ # The ds row only registers once a whitelisted attr diverged, so
1023
+ # the picker offers the literal name even when unregistered.
1024
+ if attr_name == "view_func":
1025
+ draw_state.auto_params[attr_name] = value
1026
+ draw_state.invalidate_up(max_depth=6)
1027
+ else:
1028
+ setattr(draw_state, attr_name, value)
1029
+ _last_ds = getattr(draw_state, "_sa_last_source", None)
1030
+ if _last_ds is None:
1031
+ _last_ds = {}
1032
+ draw_state._sa_last_source = _last_ds
1033
+ _last_ds[attr_name] = "draw state"
1034
+ return "draw state"
1035
+ else:
1036
+ notify(f"set_anywhere: picked source {source!r} isn't writable "
1037
+ f"for '{attr_name}'", tag="set_anywhere")
1038
+ return None
1039
+ pref = _preferred_source_for(draw_state) if target is None else None
1040
+ if pref is not None:
1041
+ _writable = set(srcs["writable"])
1042
+ target = next((s for s in sources
1043
+ if s in _writable
1044
+ and _pref_matches(pref, srcs["kinds"].get(s))), None)
1045
+ if target is None and ds_fallback:
1046
+ # Preserve sources above auto-state and existing reserved param
1047
+ # sources (such as tint). Otherwise save alongside view_func, with
1048
+ # the old draw_state fallback for calls without a renderer source.
1049
+ _setting = _setting_source(srcs, attr_name)
1050
+ _kind = srcs["kinds"].get(_setting) if _setting is not None else None
1051
+ from meltygui.core.core_render import _draw_state_reserved_names
1052
+ # None (DrawState not constructible yet) makes reserved set unknown;
1053
+ # keep the legacy pick for the call rather than throwing.
1054
+ _mirrored = attr_name not in (_draw_state_reserved_names() or ())
1055
+ if (_setting is None
1056
+ or (_mirrored
1057
+ and _source_priority(_kind)[0] not in _ABOVE_DRAW_STATE)):
1058
+ target = _setting_source(srcs, "view_func")
1059
+ if target is None:
1060
+ if attr_name == "view_func":
1061
+ draw_state.auto_params[attr_name] = value
1062
+ draw_state.invalidate_up(max_depth=6)
1063
+ else:
1064
+ setattr(draw_state, attr_name, value)
1065
+ # Stamp provenance like every other target - without it a ds
1066
+ # write is invisible ("where did my line_height=2 go?"): the
1067
+ # value lives only in auto_params.
1068
+ _last_ds = getattr(draw_state, "_sa_last_source", None)
1069
+ if _last_ds is None:
1070
+ _last_ds = {}
1071
+ draw_state._sa_last_source = _last_ds
1072
+ _last_ds[attr_name] = "draw state"
1073
+ return "draw state"
1074
+ if target is None and _setting_source(srcs, attr_name) is None:
1075
+ target = _setting_source(srcs, "view_func")
1076
+ if target is None:
1077
+ target = _driving_source(srcs, attr_name)
1078
+ if target is None:
1079
+ notify(f"set_anywhere: no writable source for '{attr_name}'",
1080
+ tag="set_anywhere")
1081
+ return None
1082
+ if attr_name == "view_func" and srcs["kinds"].get(target) == "signature" and attr_name not in sources[target]:
1083
+ notify("Choose a caller, defaults, comment, or draw-state source for the renderer", tag="set_anywhere")
1084
+ return None
1085
+
1086
+ if source is None and ds_fallback and attr_name in ('closed', 'window_pos'):
1087
+ # Parameter panels inherit their inspected window's comment for
1088
+ # parameter edits, but their own geometry/visibility belongs to the
1089
+ # panel. Never close or move the ancestor through that inherited row.
1090
+ owner = srcs.get('comment_owners', {}).get(target, draw_state)
1091
+ if owner is not draw_state:
1092
+ setattr(draw_state, attr_name, value)
1093
+ last = getattr(draw_state, '_sa_last_source', None)
1094
+ if last is None:
1095
+ last = {}
1096
+ draw_state._sa_last_source = last
1097
+ last[attr_name] = 'draw state'
1098
+ return 'draw state'
1099
+ if not window_source_writable(srcs, target):
1100
+ return None
1101
+
1102
+ # Last-written source, by attr - lazily maintained (stamped here on every
1103
+ # set, and by the popover's lazy resolve on first open): cheap provenance
1104
+ # for display without a per-frame collection.
1105
+ _last = getattr(draw_state, "_sa_last_source", None)
1106
+ if _last is None:
1107
+ _last = {}
1108
+ draw_state._sa_last_source = _last
1109
+ _last[attr_name] = target
1110
+
1111
+ # SLOW target + gesture in flight (drag or scroll): park the write
1112
+ # instead of running the save/recompile cycle per event (see the
1113
+ # full-speed block above).
1114
+ if _input_busy() and source_is_slow(srcs["kinds"].get(target)):
1115
+ _defer_write(attr_name, value, draw_state, class_to_show, source=source,
1116
+ kind=srcs["kinds"].get(target))
1117
+ return target
1118
+
1119
+ # (No write-time sanity cross-check here: mid-trip the live value
1120
+ # LEGITIMATELY disagrees with the source, so comparing now cries wolf on
1121
+ # every working set. Verification is deferred - see anywhere_value: 2
1122
+ # frames after the round trip lands, live vs what we set.)
1123
+ _t_kind = srcs["kinds"].get(target)
1124
+ write_value = value
1125
+ if source_is_low_precision(_t_kind):
1126
+ write_value = _round_low_precision(value)
1127
+ try:
1128
+ _rounded_away = not bool(write_value == value)
1129
+ except Exception:
1130
+ _rounded_away = True
1131
+ if _rounded_away:
1132
+ _stamp_precise(attr_name, value, draw_state)
1133
+ # A write that only moves digits BELOW the limit is a no-op at the
1134
+ # source: the overlay already serves the precise value, so skip
1135
+ # the save/recompile trip (and clear any parked pending entry -
1136
+ # no trip means the live baseline will never move to clear it).
1137
+ try:
1138
+ if bool(sources[target].get(attr_name) == write_value):
1139
+ _pending = getattr(draw_state, "_sa_pending", None)
1140
+ if _pending:
1141
+ _pending.pop(attr_name, None)
1142
+ return target
1143
+ except Exception:
1144
+ pass
1145
+ if attr_name == "view_func":
1146
+ from meltygui.core.input.view_selection import resolve_view_func
1147
+ from meltygui.core.input.view_selection import view_reference_code
1148
+ value = resolve_view_func(value)
1149
+ if _t_kind == "code comment":
1150
+ write_value = f"RenderFuncs.{value.__name__}" if value is not None else None
1151
+ elif not getattr(sources[target], "direct", False) and source_is_slow(_t_kind):
1152
+ location = srcs["locations"].get(target)
1153
+ write_value = view_reference_code(value, location[0] if location else None)
1154
+ else:
1155
+ write_value = value
1156
+ sources[target][attr_name] = write_value
1157
+ # The in-memory twin of the recompile the write arms below: the window
1158
+ # shows the value now, the hotswap lands the same live on the source's
1159
+ # save (see _LIVE_APPLY).
1160
+ live_apply(attr_name, value, draw_state, _t_kind, class_to_show)
1161
+ # A write to a BELOW-draw_state layer (signature, @defaults, class var,
1162
+ # codec, var) would be otherwise shadowed by a diverged auto_param
1163
+ # riding kwargs. The ds layer is framework session state, not user code -
1164
+ # so the code write CLAIMS the param: drop the stale auto_param and let
1165
+ # the new source value drive (the field case: a stale
1166
+ # auto_params['hide_internal'] kept overriding a fresh signature edit).
1167
+ if _t_kind != "draw state" and _source_priority(_t_kind)[0] not in _ABOVE_DRAW_STATE:
1168
+ _ap = getattr(draw_state, "auto_params", None)
1169
+ if isinstance(_ap, dict):
1170
+ _ap.pop(attr_name, None)
1171
+ # In-flight display cache - see _stamp_pending. The FULL-precision value:
1172
+ # the UI keeps serving it through the trip, then the overlay takes over.
1173
+ _stamp_pending(attr_name, value, draw_state)
1174
+ # Deferred writer-side hotswap: code-backed sources only become LIVE via
1175
+ # recompile, and the source text only exists after the host's debounced
1176
+ # chain_out. Snapshot the current buffer identity; the per-frame tick
1177
+ # (anywhere_value → _anywhere_recompile_tick) starts the recompile when
1178
+ # the buffer moves and polls the runner until the hotswap lands.
1179
+ _arm_recompile(draw_state, sources, target, _t_kind)
1180
+ return target
1181
+
1182
+
1183
+ def _arm_recompile(draw_state, sources, target, kind):
1184
+ """Stamp the deferred writer-side hotswap for a code-backed edit at
1185
+ `target` (kind caption `kind`) — shared by set_anywhere and
1186
+ clear_anywhere. The per-frame tick (_anywhere_recompile_tick) starts the
1187
+ recompile when the owning host's buffer moves off the snapshot."""
1188
+ if kind in ("child kwargs", "mode child kwargs", "attr default"):
1189
+ # These rows are parse rows of an ANCESTOR's code (stacked
1190
+ # @defaults) - which ancestor is not guessable from the ds graph (a
1191
+ # Lora item's parent is a plain dict view), but the written row KNOWS
1192
+ # its owner: its bubbling root IS the owning render host, whose
1193
+ # upstream render host's input is the real source object. Stamp the
1194
+ # deferred hotswap against exactly that; without it the trip never
1195
+ # lands and the pending cache shows the un-landed value forever.
1196
+ from meltygui.core.conversion.render_host import RenderHost
1197
+ _row = sources.get(target)
1198
+ _broot = (getattr(_row, "_bubble_root", None)
1199
+ or getattr(getattr(_row, "_dp", None), "_bubble_root", None))
1200
+ if isinstance(_broot, RenderHost):
1201
+ _sh = _broot.input_value if isinstance(_broot.input_value, RenderHost) else None
1202
+ _src_obj = getattr(_sh, "input_value", None) if _sh is not None else None
1203
+ if _sh is not None and _src_obj is not None:
1204
+ from meltygui.code.new_converters import host_code_state
1205
+ _cs = host_code_state(_sh)
1206
+ draw_state._sa_recompile = {
1207
+ "host": _sh, "source": _src_obj, "started": False,
1208
+ "start_frame": 0,
1209
+ "buf": _cs.text_cache if _cs is not None else None}
1210
+ return
1211
+ cm_state = getattr(draw_state, "_sa_cm_state", None)
1212
+ if cm_state is not None:
1213
+ _rc_host, _rc_source = _owning_code_host(cm_state, kind)
1214
+ if _rc_host is not None and _rc_source is not None:
1215
+ from meltygui.code.new_converters import host_code_state
1216
+ _cs = host_code_state(_rc_host)
1217
+ draw_state._sa_recompile = {
1218
+ "host": _rc_host, "source": _rc_source, "started": False,
1219
+ "start_frame": 0,
1220
+ "buf": _cs.text_cache if _cs is not None else None}
1221
+
1222
+
1223
+ def clear_anywhere(attr_name, draw_state, source, class_to_show=None):
1224
+ """Delete `attr_name`'s entry AT `source` — the info tab's trash button.
1225
+ A code source loses its parse entry (bubbling __delitem__ dirties the
1226
+ host → normal chain_out/save) and the module hotswaps through the same
1227
+ deferred trip as a set_anywhere write. "draw_state" drops the diverged
1228
+ auto_param (and nulls a whitelisted ds attr), so lower-priority layers
1229
+ resume driving. Returns the source cleared, or None when it held
1230
+ nothing."""
1231
+ from meltygui.core.diagnostics.notifications import notify
1232
+ srcs = _sources_for(draw_state, class_to_show)
1233
+ # In-flight caches for this attr are stale either way a clear goes.
1234
+ for _slot in ("_sa_pending", "_sa_precise", "_sa_deferred"):
1235
+ _d = getattr(draw_state, _slot, None)
1236
+ if isinstance(_d, dict):
1237
+ _d.pop(attr_name, None)
1238
+ kind = srcs["kinds"].get(source)
1239
+ if source == "draw_state" or kind == "draw state":
1240
+ cleared = False
1241
+ _ap = getattr(draw_state, "auto_params", None)
1242
+ if isinstance(_ap, dict) and attr_name in _ap:
1243
+ del _ap[attr_name]
1244
+ cleared = True
1245
+ from meltygui.core.core_render import OBJ_ATTR_PARAMS
1246
+ if (attr_name in OBJ_ATTR_PARAMS
1247
+ and getattr(draw_state, attr_name, None) is not None):
1248
+ setattr(draw_state, attr_name, None)
1249
+ cleared = True
1250
+ return "draw_state" if cleared else None
1251
+ sdict = srcs["sources"].get(source)
1252
+ if not isinstance(sdict, dict) or attr_name not in sdict:
1253
+ notify(f"clear_anywhere: {source!r} doesn't set '{attr_name}'",
1254
+ tag="set_anywhere")
1255
+ return None
1256
+ if kind == "instance attr" and hasattr(sdict, "_obj"):
1257
+ # Snapshot adapter over the live object - clear the OBJECT, not just
1258
+ # the snapshot (a snapshot del would resurrect next collect).
1259
+ try:
1260
+ delattr(sdict._obj, attr_name)
1261
+ except AttributeError:
1262
+ setattr(sdict._obj, attr_name, None)
1263
+ dict.__delitem__(sdict, attr_name)
1264
+ draw_state.invalidate_up(max_depth=6)
1265
+ return source
1266
+ if kind == "codec":
1267
+ # _CodecSource writes fan out to per-file meta / class render_kwargs;
1268
+ # a snapshot del wouldn't reach them. Not wired yet.
1269
+ notify(f"clear_anywhere: clearing at the codec isn't supported yet",
1270
+ tag="set_anywhere")
1271
+ return None
1272
+ if attr_name == "view_func" and getattr(sdict, "direct", False):
1273
+ notify("A direct call needs a render function; choose another renderer", tag="set_anywhere")
1274
+ return None
1275
+ del sdict[attr_name]
1276
+ _arm_recompile(draw_state, srcs["sources"], source, kind)
1277
+ return source
1278
+
1279
+
1280
+ def _permute_slots(sdict, order):
1281
+ """Refill the slots of `sdict`'s keys that appear in `order` with those
1282
+ same keys sorted by `order`; every other key (dunder bookkeeping, params
1283
+ the panel doesn't show, comment keys) keeps its slot. Slot permutation,
1284
+ the same rule every CST writer applies on save (_reorder_params /
1285
+ _reorder_call_kwargs / _reorder_class_body / _reformat_override_comment),
1286
+ so the dict's new order is exactly what the source will read. In place
1287
+ via clear/update, which a bubbling parse dict bubbles to its host as a
1288
+ change. False when fewer than two keys are involved or nothing moves."""
1289
+ present = [k for k in dict.keys(sdict) if k in order]
1290
+ if len(present) < 2:
1291
+ return False
1292
+ wanted = sorted(present, key=order.__getitem__)
1293
+ if wanted == present:
1294
+ return False
1295
+ refill = iter(wanted)
1296
+ items = []
1297
+ for k, v in dict.items(sdict):
1298
+ if k in order:
1299
+ nk = next(refill)
1300
+ items.append((nk, dict.__getitem__(sdict, nk)))
1301
+ else:
1302
+ items.append((k, v))
1303
+ sdict.clear()
1304
+ sdict.update(items)
1305
+ return True
1306
+
1307
+
1308
+ def reorder_anywhere(keys, draw_state, class_to_show=None):
1309
+ """Write a new ORDER of the view's params — `keys`, the complete key order
1310
+ the params panel was dragged into — to every code-backed source that
1311
+ stores two or more of them: the signature's parameter list, a caller's
1312
+ or decorator's kwargs, a class body, a mode entry's kwargs, an override
1313
+ comment. Each is an ordered store in its own right, so each one follows
1314
+ the panel's relative order (slot permutation: a source holding a SUBSET
1315
+ of the params neither gains nor loses keys) and they all agree after the
1316
+ drag — the signature among them, which is where the panel's own order
1317
+ derives from. Only parse-node dicts qualify: the adapter rows (instance
1318
+ attr, draw_state, codec) are snapshots over live objects with no
1319
+ persisted order. Returns the source names written, in priority order."""
1320
+ from meltygui.core.conversion.bubbling import _BubblingDictMixin
1321
+ srcs = _sources_for(draw_state, class_to_show)
1322
+ order = {k: i for i, k in enumerate(keys)}
1323
+ writable = set(srcs["writable"])
1324
+ written = []
1325
+ for sname, sdict in srcs["sources"].items():
1326
+ if sname not in writable or not isinstance(sdict, _BubblingDictMixin):
1327
+ continue
1328
+ if _permute_slots(sdict, order):
1329
+ written.append(sname)
1330
+ # Deferred writer-side hotswap, like set_anywhere. One stamp per
1331
+ # draw_state, so arm lowest-priority first and let the signature (the
1332
+ # first registered, the one the panel's order reads back from) win.
1333
+ for sname in reversed(written):
1334
+ _arm_recompile(draw_state, srcs["sources"], sname, srcs["kinds"].get(sname))
1335
+ return written
1336
+
1337
+
1338
+ def _pending_param_order(draw_state):
1339
+ """The view function's parameter order as the PENDING source has it: the
1340
+ signature parse held by this view's code host (`_sa_cm_state`, there once
1341
+ anything has collected the view's sources). A reorder rewrites that parse
1342
+ at once while the live function only follows after the save + hotswap
1343
+ trip, so ordering the proxy by it shows the drag's result immediately
1344
+ instead of snapping back for the trip's duration. None when no host is
1345
+ in reach — the live signature order stands."""
1346
+ cm_state = getattr(draw_state, "_sa_cm_state", None)
1347
+ host = getattr(cm_state, "render_func_dict", None)
1348
+ if host is None:
1349
+ return None
1350
+ try:
1351
+ params = host.deep.parameters()
1352
+ except Exception:
1353
+ return None
1354
+ if not isinstance(params, dict):
1355
+ return None
1356
+ return [k for k in dict.keys(params)
1357
+ if isinstance(k, str) and not k.startswith("__")]
1358
+
1359
+
1360
+ def view_param_names(draw_state):
1361
+ """The render view's own input parameters, in signature order — what
1362
+ `locate_params` iterates.
1363
+
1364
+ Signature params minus the ones that aren't inputs at all: the wrapper's
1365
+ injected/plumbing names and event params (core_render's own
1366
+ _AUTO_PARAM_EXCLUDE / _is_event_param_name — the same predicates
1367
+ auto-state uses, so the two lists can't drift), plus injected-state params
1368
+ (`gl_state: GLState = None` — a class annotation with a None default, owned
1369
+ by set_default's misc path). DrawState-reserved names (width, tint, ...)
1370
+ are deliberately KEPT: auto-state skips them because they have legacy
1371
+ manual handling, but they're still real inputs of the view.
1372
+
1373
+ Also deliberately NOT extended with render_func_kwarg_names(): those
1374
+ framework kwargs are shared by every view and would bury its actual
1375
+ params."""
1376
+ return _func_param_names(getattr(draw_state, "_view_func", None))
1377
+
1378
+
1379
+ def _func_param_names(func):
1380
+ """Input param names of a render func (unwrapped), with the wrapper's
1381
+ injected/plumbing/event/state params filtered — the engine behind
1382
+ view_param_names and header_param_names."""
1383
+ import inspect
1384
+ if func is None:
1385
+ return []
1386
+ try:
1387
+ params = inspect.signature(inspect.unwrap(func)).parameters
1388
+ except (TypeError, ValueError):
1389
+ return []
1390
+ from meltygui.core.core_render import _AUTO_PARAM_EXCLUDE
1391
+ from meltygui.core.core_render import _is_event_param_name
1392
+ out = []
1393
+ for name, p in params.items():
1394
+ if name in _AUTO_PARAM_EXCLUDE or _is_event_param_name(name):
1395
+ continue
1396
+ if name.startswith("_"):
1397
+ continue # placeholder/private (`_`), not a real input
1398
+ if p.kind in (inspect.Parameter.VAR_POSITIONAL,
1399
+ inspect.Parameter.VAR_KEYWORD):
1400
+ continue
1401
+ ann = p.annotation
1402
+ if (ann is not inspect.Parameter.empty and inspect.isclass(ann)
1403
+ and p.default is None):
1404
+ continue # injected state (GLState / CodeState / ...)
1405
+ out.append(name)
1406
+ return out
1407
+
1408
+
1409
+ def signature_default_for(attr_name, draw_state):
1410
+ """The param's DECLARED default: the view function's signature, else the
1411
+ header function's (locate_all_params spans both). None when neither
1412
+ declares one — the + affordance falls back to this when the resolved
1413
+ value is None (a header param nothing sets resolves to None; stamping
1414
+ that None would create an entry that still reads as unset)."""
1415
+ import inspect
1416
+ for fn in (getattr(draw_state, "_view_func", None),
1417
+ (getattr(draw_state, "_kwargs", None) or {}).get("with_header")):
1418
+ if not callable(fn):
1419
+ continue
1420
+ try:
1421
+ p = inspect.signature(inspect.unwrap(fn)).parameters.get(attr_name)
1422
+ except (TypeError, ValueError):
1423
+ continue
1424
+ if p is not None and p.default is not inspect.Parameter.empty:
1425
+ return p.default
1426
+ return None
1427
+
1428
+
1429
+ # Header plumbing _AUTO_PARAM_EXCLUDE didn't cover - the header receives
1430
+ # these from the wrapper/parent per call, they're never user inputs.
1431
+ _HEADER_PARAM_EXCLUDE = {"meltygui", "parent_show_add_delete", "name_func"}
1432
+
1433
+
1434
+ def header_param_names(draw_state):
1435
+ """Params of the view's HEADER function — the resolved `with_header`
1436
+ kwarg (a render_func or plain callable). Empty when the view has no
1437
+ header. Same filtering as the view's own params."""
1438
+ hf = (getattr(draw_state, "_kwargs", None) or {}).get("with_header")
1439
+ if not callable(hf):
1440
+ return []
1441
+ return [n for n in _func_param_names(hf) if n not in _HEADER_PARAM_EXCLUDE]
1442
+
1443
+
1444
+ # Builtin bases a signature default may SPECIALIZE (TensorDim(int)). The
1445
+ # stored/parsed value round-trips as the plain base (comments literal_eval to
1446
+ # int, auto_params to the base), so the read edge re-wraps it in the
1447
+ # default's type; that type is what routes the value to its custom renderer.
1448
+ _SPECIALIZE_BASES = (int, float, str, tuple)
1449
+ _default_types_cache = {}
1450
+
1451
+
1452
+ def _signature_default_types(draw_state):
1453
+ """{param: type} for signature defaults whose type is a strict SUBCLASS
1454
+ of a builtin base — the params whose values should be re-specialized on
1455
+ read. Cached per (wrapper, unwrapped) function identity pair so a hotswap
1456
+ that changes the signature refreshes it."""
1457
+ import inspect
1458
+ func = getattr(draw_state, "_view_func", None)
1459
+ if func is None:
1460
+ return {}
1461
+ try:
1462
+ inner = inspect.unwrap(func)
1463
+ except Exception:
1464
+ return {}
1465
+ key = (id(func), id(inner))
1466
+ cached = _default_types_cache.get(key)
1467
+ if cached is not None:
1468
+ return cached
1469
+ out = {}
1470
+ try:
1471
+ for n, p in inspect.signature(inner).parameters.items():
1472
+ d = p.default
1473
+ if d is inspect.Parameter.empty or d is None or isinstance(d, bool):
1474
+ continue
1475
+ t = type(d)
1476
+ for b in _SPECIALIZE_BASES:
1477
+ if isinstance(d, b) and t is not b:
1478
+ out[n] = t
1479
+ break
1480
+ except (TypeError, ValueError):
1481
+ pass
1482
+ _default_types_cache[key] = out
1483
+ return out
1484
+
1485
+
1486
+ class ParamProxy(dict):
1487
+ """Live dict view over one render view's input parameters:
1488
+
1489
+ ds.locate_params["x_dim"] # resolved value (from _kwargs)
1490
+ ds.locate_params["x_dim"] = 0 # set_anywhere on the driving source
1491
+ for param, value in ds.locate_params: ...
1492
+ draw_collection(ds.locate_params) # renders like any dict
1493
+
1494
+ A REAL dict subclass, so every isinstance(x, dict) path in the framework
1495
+ (draw_collection's key routing, converters, serialization probes, `{**p}`,
1496
+ C-level fast paths that bypass overridden methods entirely) treats it as
1497
+ the dict it looks like. The inherited storage holds a SNAPSHOT of the
1498
+ resolved values, refreshed by `refresh()` whenever the draw_state hands
1499
+ the proxy out; the overridden accessors read live on top of it.
1500
+
1501
+ Reads and writes keep the `locate_<param>` asymmetry: reading is the
1502
+ framework-resolved value, only writing walks the source registry. Writes
1503
+ skip the SET_ANYWHERE_PARAMS whitelist and fall back to the draw_state
1504
+ when no source in code sets the param (allow_any / ds_fallback).
1505
+
1506
+ Iterating yields (param, value) PAIRS — the loop this exists for. That's
1507
+ the one deviation from dict, and it's confined to Python-level `for x in
1508
+ proxy`: `keys()`, `dict(proxy)`, `{**proxy}` and every C-level consumer go
1509
+ through the storage and see plain keys."""
1510
+
1511
+ # No instance __dict__: draw_collection prefers getattr(input_value, key)
1512
+ # over collection[key] for anything that has one, which would hand a param
1513
+ # named like a dict method (`items`, `values`, ...) a bound method instead
1514
+ # of its value. With __slots__ the proxy is storage-only and every read
1515
+ # resolves through __getitem__.
1516
+ __slots__ = ("_ds", "_names_fn")
1517
+
1518
+ def __init__(self, draw_state, names_fn=None):
1519
+ super().__init__()
1520
+ self._ds = draw_state
1521
+ # names_fn: which params this proxy spans - view_param_names by
1522
+ # default; the grouped locate_all_params supplies the header-only
1523
+ # supplier for its 'header' sub-dict. Same read/write semantics
1524
+ # either way (the header draws against the same draw_state/kwargs).
1525
+ self._names_fn = names_fn
1526
+ # Warm the view's code hosts now (one registry walk; the hosts parse
1527
+ # in the background): a reorder only reaches sources that are parsed
1528
+ # when the drop lands, and without this the first drag on a fresh
1529
+ # panel would be the call that first creates the signature host and
1530
+ # the comment would move while the header (the panel's own order)
1531
+ # stayed put. Guarded: a proxy can be minted for a view whose sources
1532
+ # haven't been collected (no view func yet), it's a plain snapshot.
1533
+ try:
1534
+ _sources_for(draw_state)
1535
+ except Exception:
1536
+ pass
1537
+ self.refresh()
1538
+
1539
+ def _names(self):
1540
+ names = (self._names_fn or view_param_names)(self._ds)
1541
+ # Live-signature names go in the PENDING signature's order when a code
1542
+ # host holds one (see _pending_param_order); ones the host doesn't
1543
+ # know (header params) keep their relative order at the end.
1544
+ pending = _pending_param_order(self._ds)
1545
+ if pending:
1546
+ pos = {k: i for i, k in enumerate(pending)}
1547
+ names.sort(key=lambda n: pos.get(n, len(pos)))
1548
+ return names
1549
+
1550
+ def reorder_keys(self, keys):
1551
+ """drag_drop.Reorder's collection hook: `keys` is this proxy's
1552
+ complete new key order. The order lives in the view's code sources,
1553
+ so it is written there (reorder_anywhere) and the snapshot follows
1554
+ on refresh — the pending signature parse already reads back in the
1555
+ new order. True when any source moved."""
1556
+ written = reorder_anywhere(list(keys), self._ds)
1557
+ if written:
1558
+ self.refresh()
1559
+ return bool(written)
1560
+
1561
+ def _specialize(self, name, value):
1562
+ """Re-wrap a plain parsed value in the signature default's subtype
1563
+ (int 1 → TensorDim(1)) so type routing picks the custom renderer.
1564
+ Values the subtype can't take (a dim given by NAME) pass through."""
1565
+ dt = _signature_default_types(self._ds).get(name)
1566
+ if dt is None or value is None or type(value) is dt:
1567
+ return value
1568
+ try:
1569
+ return dt(value)
1570
+ except (TypeError, ValueError):
1571
+ return value
1572
+
1573
+ def refresh(self):
1574
+ """Re-snapshot the inherited storage from the live values. Cheap (a
1575
+ _kwargs read per param) and the reason a handed-out proxy is never
1576
+ stale, including for consumers that read the storage directly."""
1577
+ ds = self._ds
1578
+ live = {k: self._specialize(k, anywhere_value(k, ds))
1579
+ for k in self._names()}
1580
+ dict.clear(self)
1581
+ dict.update(self, live) # bypasses __setitem__; a snapshot, not a set
1582
+ return self
1583
+
1584
+ def __getitem__(self, name):
1585
+ if not dict.__contains__(self, name):
1586
+ raise KeyError(name)
1587
+ return self._specialize(name, anywhere_value(name, self._ds))
1588
+
1589
+ def __setitem__(self, name, value):
1590
+ set_anywhere(name, value, self._ds, allow_any=True, ds_fallback=True)
1591
+ # Take the snapshot in step so in-frame reads (a collection row
1592
+ # re-reading what it just wrote) don't show the pre-write value.
1593
+ dict.__setitem__(self, name, value)
1594
+
1595
+ def get(self, name, default=None):
1596
+ if not dict.__contains__(self, name):
1597
+ return default
1598
+ return self._specialize(name, anywhere_value(name, self._ds,
1599
+ default=default))
1600
+
1601
+ def items(self):
1602
+ return [(k, self._specialize(k, anywhere_value(k, self._ds)))
1603
+ for k in dict.keys(self)]
1604
+
1605
+ def values(self):
1606
+ return [v for _k, v in self.items()]
1607
+
1608
+ def __iter__(self):
1609
+ return iter(self.items())
1610
+
1611
+ def __repr__(self):
1612
+ return f"ParamProxy({dict(self.items())!r})"
1613
+
1614
+
1615
+ def _header_only_param_names(draw_state):
1616
+ """Header params MINUS ones the view itself declares — those belong to
1617
+ the 'params' group (view-first dedup, same rule the flat proxy had)."""
1618
+ own = set(view_param_names(draw_state))
1619
+ return [n for n in header_param_names(draw_state) if n not in own]
1620
+
1621
+
1622
+ class GroupedParamProxy(dict):
1623
+ """locate_all_params' shape: two nested live dicts —
1624
+
1625
+ {'params': <ParamProxy over the view's own params>,
1626
+ 'header': <ParamProxy over the header-only params>}
1627
+
1628
+ Each leaf keeps ParamProxy semantics (reads resolve via anywhere_value,
1629
+ item-writes route through set_anywhere); the grouping just separates the
1630
+ two origins so they render/route as distinct nested dicts. refresh()
1631
+ re-snapshots both — the DrawState property calls it per access, same
1632
+ stable-identity rules as locate_params."""
1633
+ __slots__ = ()
1634
+
1635
+ def __init__(self, draw_state):
1636
+ super().__init__()
1637
+ dict.__setitem__(self, "params", ParamProxy(draw_state))
1638
+ dict.__setitem__(self, "header",
1639
+ ParamProxy(draw_state,
1640
+ names_fn=_header_only_param_names))
1641
+
1642
+ def refresh(self):
1643
+ for v in dict.values(self):
1644
+ v.refresh()
1645
+ return self
1646
+
1647
+ def __setitem__(self, key, value):
1648
+ # draw_collection writes the changed CHILD dict into its parent
1649
+ # (`grouped['params'] = edited`) - accepting that would swap the live
1650
+ # sub-proxy for a plain dict snapshot and disconnect routing. The
1651
+ # sub-proxies are canonical and already received the leaf writes in
1652
+ # place, so a group-slot write is a no-op; unknown keys are refused.
1653
+ if key not in self:
1654
+ raise KeyError(f"GroupedParamProxy has fixed groups, not {key!r}")
1655
+
1656
+
1657
+ # ── draw_state.locate_* ─────────────────────────────────────────────────────
1658
+ # The accessors themselves live on the DrawState CLASS, in
1659
+ # model/core_model/draw_state.py, so they work on every draw_state whether or
1660
+ # not this module has been imported yet; they call back into anywhere_value /
1661
+ # set_anywhere / ParamProxy here via a lazy descriptor. `locate_<param>` is
1662
+ # generic over the param: reads route through DrawState.__getattr__ (miss-only,
1663
+ # so ordinary reads pay nothing) and writes through DrawState._locate_set,
1664
+ # which @live's existing __setattr__ dispatches on the LOCATE_PARAMS. See the
1665
+ # comment block there for why the two paths are wired differently.