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,1803 @@
1
+ """Editor-side live_view widgets: marker token + anchored value window.
2
+
3
+ The type-keyed token_views overlay in draw_text calls `draw_live_view_overlay`
4
+ for every CallParse node; it bails unless the call is live_view, then resolves
5
+ the SAME (store_obj, key_path) the capture side publishes under
6
+ (live_view.site_for_line — one resolution code path, no drift) and draws a
7
+ box outline around the symbol being visualized. Clicking the box toggles the
8
+ value window: a closable nested window (latched into Melty.root_draw_states,
9
+ so it persists and re-draws every frame even while the editor tile is
10
+ blit-cached) whose swoosh connector anchors back to the marker. The window
11
+ re-reads the store each render and registers itself as a watcher, so the
12
+ publishing thread invalidates exactly this window per publish — never the
13
+ editor tile, never per frame.
14
+
15
+ Only explicit live_view() call tokens auto-open their window the first time a
16
+ marker renders while a value exists — you typed the call, so the value shows
17
+ without a click. SIMPLE builtin values (int/float/str/bool/tuple of ≤4
18
+ scalars/enum members, never None) skip the window entirely and render as an
19
+ inline pill drawn directly over the symbol in the editor's font
20
+ (_inline_value_text + the inline block in draw_live_view_marker) — for
21
+ explicit live_view() tokens AND snapshot markers alike. An inline marker has
22
+ no value window at all (no auto-open, preview, or double-click; the gutter
23
+ shows an inert info glyph instead of the magnifier). Snapshot/param markers (every captured assignment from an
24
+ instrumented run) start closed and open on box click, so a run doesn't bury
25
+ the code under one window per local. Sites the dict conversion can't surface
26
+ (while/with/match bodies — line-keyed fallback stores) have no CallParse token
27
+ to anchor and get no marker yet.
28
+ """
29
+
30
+ import bisect
31
+ import collections
32
+ import colorsys
33
+ import enum
34
+ import inspect
35
+ import re
36
+ import sys
37
+ import time
38
+ import weakref
39
+
40
+ import meltygui_imgui as imgui
41
+ from meltygui.hdr_color import pack_color
42
+ from meltygui_imgui.core import _DrawList
43
+
44
+ from meltygui.state.new_core_model import Anchor
45
+ from meltygui.state.new_core_model import Pin
46
+ from meltygui.core.styling.fonts import Font
47
+ from meltygui.core.rendering.modes import Modes
48
+ from meltygui.core.core_render import render_func
49
+ from meltygui.code.live_view import live_values_for
50
+ from meltygui.code.live_view import label_for
51
+ from meltygui.code.live_view import site_for_line
52
+ from meltygui.code.live_view import watch
53
+ from meltygui.code.live_view import install_builtin
54
+ from meltygui.code.live_view import auto_dim_names_for
55
+ from meltygui.code.live_view import RerunHint
56
+ from meltygui.core.rendering.core_decoration import Core
57
+ from meltygui.core.rendering.window_decoration import window
58
+ from meltygui.core.cache.tile_cache import add_shadow
59
+ import meltygui.editor.live_usage as live_usage
60
+
61
+ # The seamless path: any app can call live_view() with no import (like
62
+ # breakpoint()). Installed when the editor side loads - i.e. every studio
63
+ # session - so "type the call, hotswap, run" needs no source changes beyond
64
+ # the call itself.
65
+ install_builtin()
66
+
67
+
68
+ def install_token_views(token_views):
69
+ """Merge the live_view overlays into a token_views dict (called by
70
+ text_editor right after DEFAULT_TOKEN_VIEWS is defined). Order matters:
71
+ CallParse IS-A GeneralParse and the walk takes the first matching type, so
72
+ the call-token entry must precede the root snapshot entry."""
73
+ from meltygui.code.libcst_conversion import CallParse
74
+ from meltygui.code.libcst_conversion import GeneralParse
75
+ token_views[CallParse] = {"renderer": draw_live_view_overlay,
76
+ "char_width": None}
77
+ token_views[GeneralParse] = {"renderer": draw_snapshot_overlay,
78
+ "char_width": None}
79
+
80
+
81
+
82
+ def _store_name(obj):
83
+ """Stable display/name key for a store object (function qualname or
84
+ module name) — window/marker names key on this + the cst key path,
85
+ never on draw_state ids (not stable across sessions/editors)."""
86
+ return getattr(obj, "__qualname__", None) or getattr(obj, "__name__", "?")
87
+
88
+
89
+ _NO_VALUE = object() # "window has received no value yet" sentinel
90
+
91
+
92
+ def _display_key(key):
93
+ """Human title for one key-path element: a `line:N#name` key (frame
94
+ snapshots / twin_snap stamps) reads as its token name, not the line
95
+ number. Display only — stable IDs keep the raw key."""
96
+ key = str(key)
97
+ if key.startswith("line:") and "#" in key:
98
+ return key.split("#", 1)[1]
99
+ return key
100
+
101
+
102
+ def _split_line_key(seg):
103
+ """(label, line) for a `line:N#name` segment, (None, None) otherwise."""
104
+ seg = str(seg)
105
+ if not seg.startswith("line:"):
106
+ return None, None
107
+ head, _, label = seg.partition("#")
108
+ try:
109
+ return label, int(head[5:])
110
+ except ValueError:
111
+ return label, 0
112
+
113
+
114
+
115
+ def _stable_key_name(key_path, all_keys=None):
116
+ """Identity form of a store key path for VIEW NAMES — the LINE NUMBER is
117
+ STRIPPED: `line:12#r` reads as `r`. The name is hashed into the render
118
+ unique ID, so a raw line-keyed name gives the same code line a NEW view
119
+ identity every time a rerun/edit re-stamps its line — leaking the old
120
+ marker/window draw_states and colliding IDs across runs. The label IS the
121
+ code line's stable id; repeat sites of the same label stay sibling-
122
+ distinct via their ordinal among same-label keys in `all_keys` (the
123
+ store's key set), ranked by stamped line — the ORDER survives the line
124
+ shifts the raw numbers don't. Bare `line:N` keys rank under the empty
125
+ label the same way.
126
+
127
+ A plain STATEMENT key with the same name at the same position —
128
+ `('x',)` beside `('line:N#x',)`, a dict-visible assignment plus a
129
+ line-keyed with/while-body site of the same local — claims ordinal 0
130
+ and shifts every line-keyed ordinal up: the two used to collapse to
131
+ ONE name, and colliding view names shared their marker/window
132
+ draw_states (the draw_text garbled-overlay bug)."""
133
+ parts = []
134
+ for i, seg in enumerate(key_path):
135
+ label, line = _split_line_key(seg)
136
+ if label is None:
137
+ parts.append(str(seg))
138
+ continue
139
+ ordinal = 0
140
+ if all_keys is not None:
141
+ lines = []
142
+ statement_twin = False
143
+ for k in all_keys:
144
+ if len(k) > i:
145
+ lb, ln = _split_line_key(k[i])
146
+ if lb == label and ln is not None:
147
+ lines.append(ln)
148
+ elif lb is None and str(k[i]) == label:
149
+ statement_twin = True
150
+ if line in lines:
151
+ ordinal = sorted(lines).index(line)
152
+ if statement_twin:
153
+ ordinal += 1
154
+ parts.append(label if ordinal == 0 else f"{label}~{ordinal}")
155
+ return "/".join(parts)
156
+
157
+
158
+ def _stable_key_names(all_keys):
159
+ """{key_path: line-free name} for a WHOLE store snapshot in one pass.
160
+ The per-key _stable_key_name ordinal scan is O(store), which made naming
161
+ O(store²) per overlay pass on frame-snapshot stores (thousands of keys —
162
+ the draw_text slowdown). Grouping ordinals per (position, label) once
163
+ produces identical names at O(n log n); callers cache the map on their
164
+ existing invalidation signals (the anchor index / a per-store memo), so
165
+ steady-state naming is a dict hit."""
166
+ groups = {}
167
+ statement_twins = set() # (position, literal name) of non-line segs
168
+ for k in all_keys:
169
+ for i, seg in enumerate(k):
170
+ label, line = _split_line_key(seg)
171
+ if label is not None:
172
+ groups.setdefault((i, label), []).append(line)
173
+ else:
174
+ statement_twins.add((i, str(seg)))
175
+ ranks = {}
176
+ for (i, label), lines in groups.items():
177
+ # A statement key with this literal name at this position claims
178
+ # ordinal 0 (see _stable_key_name's docstring) - every line-keyed
179
+ # sibling shifts up so no two key paths share a name.
180
+ base = 1 if (i, label) in statement_twins else 0
181
+ for o, ln in enumerate(sorted(lines)):
182
+ ranks.setdefault((i, label, ln), o + base) # dups keep first rank
183
+ out = {}
184
+ for k in all_keys:
185
+ parts = []
186
+ for i, seg in enumerate(k):
187
+ label, line = _split_line_key(seg)
188
+ if label is None:
189
+ parts.append(str(seg))
190
+ else:
191
+ o = ranks.get((i, label, line), 0)
192
+ parts.append(label if o == 0 else f"{label}~{o}")
193
+ out[k] = "/".join(parts)
194
+ return out
195
+
196
+
197
+ # One name map per store - see _store_key_names.
198
+ _NAME_MAPS = weakref.WeakKeyDictionary()
199
+
200
+
201
+ def _store_key_names(store_obj):
202
+ """The store's {key_path: view name} map, memoized per KEY-SET
203
+ generation (`__live_keys_gen__`, bumped by live_view on first publish /
204
+ re-key / prune, with len as a backstop). Ordinal names depend on the
205
+ WHOLE key set, so every consumer — snapshot overlay, call-token
206
+ overlay, idle paths — must read the SAME map: consumers holding
207
+ differently-stale private memos minted one name for two different keys
208
+ (duplicate marker/window IDs, the draw_text garbled overlays)."""
209
+ try:
210
+ d = vars(store_obj)
211
+ except TypeError:
212
+ return {}
213
+ store = d.get("__live_values__") or {}
214
+ gen = d.get("__live_keys_gen__", 0)
215
+ try:
216
+ ent = _NAME_MAPS.get(store_obj)
217
+ if ent is None or ent[0] != gen or ent[1] != len(store):
218
+ ent = (gen, len(store), _stable_key_names(list(store.keys())))
219
+ _NAME_MAPS[store_obj] = ent
220
+ return ent[2]
221
+ except TypeError:
222
+ return _stable_key_names(list(store.keys()))
223
+
224
+
225
+ def _inline_value_text(value, max_chars=20):
226
+ """Format a simple builtin value for the marker's INLINE label, or None
227
+ when the value isn't simple enough (those keep the popover window).
228
+ Simple: bool, int, float, str, enum members, and tuples of up to 4 such
229
+ scalars. `max_chars` caps a string's printed length (ellipsis past it) —
230
+ the label floats over code, so it must stay short. Colors (see
231
+ _inline_swatch_rgba) keep their text; the pill adds the swatch."""
232
+ if value is None:
233
+ return None # None is NOT on the supported list - no label
234
+ if isinstance(value, RerunHint):
235
+ # The parked 'Rerun to visualize ...' placeholder is an affordance,
236
+ # not a captured value - it keeps the popover, not a label.
237
+ return None
238
+ # bool before int (bool IS-A int), Enum before int (IntEnum members).
239
+ if isinstance(value, bool):
240
+ return "True" if value else "False"
241
+ if isinstance(value, enum.Enum):
242
+ return value.name
243
+ if isinstance(value, int):
244
+ return str(value)
245
+ if isinstance(value, float):
246
+ return f"{value:.4g}"
247
+ if isinstance(value, str):
248
+ # repr ONLY a max_chars prefix: a frame snapshot's locals include
249
+ # whole file texts (megabytes), and repr(value) on one is ~1 ms
250
+ # per call — 90 markers a frame put the stack-trace pane at 110 ms
251
+ # (cProfile 09-01: 2.13 s of 2.38 s in builtins.repr). Quotes and
252
+ # quotes only lengthen a repr, so the prefix's repr already runs
253
+ # past max_chars whenever the full one would, and the label cut
254
+ # from it is the same text.
255
+ if len(value) > max_chars:
256
+ return repr(value[:max_chars])[:max_chars] + "…"
257
+ text = repr(value)
258
+ if len(text) > max_chars:
259
+ text = text[:max_chars] + "…"
260
+ return text
261
+ if isinstance(value, tuple) and len(value) <= 4:
262
+ parts = []
263
+ for item in value:
264
+ part = (None if isinstance(item, tuple)
265
+ else _inline_value_text(item, max_chars))
266
+ if part is None:
267
+ return None
268
+ parts.append(part)
269
+ tail = "," if len(value) == 1 else ""
270
+ return "(" + ", ".join(parts) + tail + ")"
271
+ return None
272
+
273
+
274
+ _HEX_COLOR_RE = re.compile(r"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")
275
+ # Two blank cells the pill text reserves for the swatch - the width math and
276
+ # the left-gap cells below need no special case for it.
277
+ _SWATCH_HOLE = " "
278
+
279
+
280
+ def _inline_swatch_rgba(value):
281
+ """(r, g, b, a) in 0..1 when a captured value READS as a color — a 3/4
282
+ tuple of numbers all within 0..1 (or all ints within 0..255 with one
283
+ past 1, scaled down), or a hex color string (`'#8888c6'`, `'#fff'`,
284
+ RGBA `'#8888c680'`) — else None. Same shapes the editor's color3 /
285
+ colorhex token widgets swatch."""
286
+ if isinstance(value, str):
287
+ if len(value) > 9 or not _HEX_COLOR_RE.match(value):
288
+ return None
289
+ hex_digits = value[1:]
290
+ if len(hex_digits) == 3:
291
+ hex_digits = "".join(c + c for c in hex_digits)
292
+ channels = [int(hex_digits[i:i + 2], 16) / 255.0
293
+ for i in range(0, len(hex_digits), 2)]
294
+ return (channels[0], channels[1], channels[2],
295
+ channels[3] if len(channels) == 4 else 1.0)
296
+ if not isinstance(value, tuple) or len(value) not in (3, 4):
297
+ return None
298
+ for item in value:
299
+ if isinstance(item, bool) or not isinstance(item, (int, float)):
300
+ return None
301
+ if not 0 <= item <= 255:
302
+ return None
303
+ if all(item <= 1 for item in value):
304
+ channels = tuple(float(c) for c in value)
305
+ elif all(isinstance(item, int) for item in value):
306
+ channels = tuple(c / 255.0 for c in value)
307
+ else:
308
+ return None
309
+ return channels if len(channels) == 4 else channels + (1.0,)
310
+
311
+
312
+ def _paint_value_pill(inline_text, span_x, text_y, span_width=None,
313
+ allow_overflow=False, fill=False, tint=None,
314
+ swatch=None):
315
+ """The shared inline-value pill on the window draw list, in the CURRENT
316
+ (editor) font, over the code span [span_x, span_x + span_width).
317
+
318
+ Placement rules: a value narrower than the span RIGHT-ALIGNS on it, so
319
+ the span's FIRST characters peek through beside it; a wider value
320
+ either grows right past the span (`allow_overflow` — the span is the
321
+ last code on its line, nothing there to cover) or is elided with an
322
+ ellipsis to the span's width. `fill=True` stretches the card over the
323
+ whole span regardless of the value's width (a live_view() call token is
324
+ instrumentation, not code worth peeking at). span_width=None is the
325
+ simple left-anchored unlimited pill (no span geometry known).
326
+
327
+ `tint` is the rgb the pill wears — the background tint under the token
328
+ (see _pill_tint: token → enclosing def/class block → file), so the
329
+ value reads as part of the scope it was captured in; None keeps the
330
+ default forest green. Fill and text derive from it with the SAME
331
+ factors as the green, so an untinted pill looks exactly as before.
332
+
333
+ `swatch` = ((r, g, b, a), hole_index): a color chip painted into the
334
+ _SWATCH_HOLE the text carries at character `hole_index` — the pill's
335
+ layout treats the hole as text, so nothing else changes. The chip is
336
+ split like the editor's color widgets: left half opaque, right half at
337
+ the real alpha over the card."""
338
+ # [tint=(0.36, 0.85, 0.46)] pad_x = 3.0
339
+ pad_x = 3.0
340
+ # [tint=(0.30, 0.52, 0.20)] default_tint = (0.30, 0.52, 0.20)
341
+ default_tint = (0.30, 0.52, 0.20)
342
+ # Card and text are the tint re-saturated at fixed brightness (value),
343
+ # so every pill reads as the same kind of thing whatever hue it wears:
344
+ # a deep, vivid card with equally vivid text - deliberately contrasting the
345
+ # muted code text around it. Raise *_value to lighten, *_saturation
346
+ # toward 1.0 to make the hue purer.
347
+ fill_saturation = 0.92
348
+ fill_value = 0.14
349
+ text_saturation = 0.78
350
+ text_value = 0.88
351
+ # The label's own face: a step below the editor's (JetBrains Mono 18.5)
352
+ # so the value never passes for code. None while the face is still
353
+ # baking (first get() queues it) - the current font stands in.
354
+ label_font = Font.JETBRAINS_MONO_16
355
+ pad_y = 1.0
356
+ shadow_offset = 2.0
357
+ corner_radius = 4.0
358
+ line_text_height = imgui.get_text_line_height() # in editor face
359
+ _font_mgr = Core.melty.font_mgr
360
+ _font_handle = _font_mgr.get(label_font) if _font_mgr is not None else None
361
+ if _font_handle is not None:
362
+ imgui.push_font(_font_handle)
363
+ try:
364
+ _paint_value_pill_body(inline_text, span_x, text_y, span_width,
365
+ allow_overflow, fill, tint, swatch, pad_x,
366
+ pad_y, shadow_offset, corner_radius,
367
+ default_tint, fill_saturation, fill_value,
368
+ text_saturation, text_value, line_text_height)
369
+ finally:
370
+ if _font_handle is not None:
371
+ imgui.pop_font()
372
+
373
+
374
+ def _pill_rgb(base, saturation, value):
375
+ """`base` re-saturated at a fixed brightness — the pill's card / text
376
+ colour for a tint. Hue is all that survives of the base."""
377
+ hue, sat, _val = colorsys.rgb_to_hsv(base[0], base[1], base[2])
378
+ # A grey base has no hue to keep - let it stay grey at the target value.
379
+ return colorsys.hsv_to_rgb(hue, saturation if sat > 0.05 else sat, value)
380
+
381
+
382
+ def _paint_value_pill_body(inline_text, span_x, text_y, span_width,
383
+ allow_overflow, fill, tint, swatch, pad_x, pad_y,
384
+ shadow_offset, corner_radius, default_tint,
385
+ fill_saturation, fill_value, text_saturation,
386
+ text_value, line_text_height):
387
+ """_paint_value_pill's layout + paint, run with the label font pushed
388
+ (every measurement here is in that face). The pill's vertical centre
389
+ stays on the code line: text_y is the LINE's text top, and the smaller
390
+ face is centred within the line's glyph height."""
391
+ text_size = imgui.calc_text_size(inline_text)
392
+ text_y += max(0.0, (line_text_height - text_size.y) * 0.5)
393
+ if (span_width is not None and text_size.x > span_width
394
+ and not allow_overflow):
395
+ while inline_text and imgui.calc_text_size(
396
+ inline_text + "…").x > span_width:
397
+ inline_text = inline_text[:-1]
398
+ if not inline_text:
399
+ return # not even one character fits - draw nothing
400
+ inline_text += "…"
401
+ text_size = imgui.calc_text_size(inline_text)
402
+ if span_width is not None and text_size.x <= span_width:
403
+ text_x = span_x + span_width - text_size.x # right-aligned
404
+ else:
405
+ text_x = span_x
406
+ card_left = span_x if (fill or text_x == span_x) else text_x
407
+ card_right = text_x + text_size.x
408
+ if fill and span_width is not None:
409
+ card_right = max(card_right, span_x + span_width)
410
+ box_x = card_left - pad_x
411
+ box_y = text_y - pad_y
412
+ box_width = (card_right - card_left) + 2 * pad_x
413
+ box_height = text_size.y + 2 * pad_y
414
+ add_shadow((box_x, box_y, box_width, box_height),
415
+ offset=shadow_offset, corner_radius=corner_radius)
416
+ base = tint if tint is not None else default_tint
417
+ fill_rgb = _pill_rgb(base, fill_saturation, fill_value)
418
+ text_rgb = _pill_rgb(base, text_saturation, text_value)
419
+ draw_list: _DrawList = imgui.get_window_draw_list()
420
+ draw_list.add_rect_filled(
421
+ box_x, box_y, box_x + box_width, box_y + box_height,
422
+ pack_color(fill_rgb[0], fill_rgb[1], fill_rgb[2], 0.97),
423
+ rounding=corner_radius)
424
+ draw_list.add_text(text_x, text_y,
425
+ pack_color(text_rgb[0], text_rgb[1],
426
+ text_rgb[2], 0.97),
427
+ inline_text)
428
+ if swatch is not None and len(inline_text) >= swatch[1] + len(_SWATCH_HOLE):
429
+ rgba, hole_index = swatch
430
+ hole_x = text_x + imgui.calc_text_size(inline_text[:hole_index]).x
431
+ hole_width = imgui.calc_text_size(_SWATCH_HOLE).x
432
+ side = max(4.0, min(hole_width - 2.0, text_size.y - 2.0))
433
+ chip_x = hole_x + (hole_width - side) * 0.5
434
+ chip_y = text_y + (text_size.y - side) * 0.5
435
+ chip_mid = chip_x + side * 0.5
436
+ draw_list.add_rect_filled(
437
+ chip_x, chip_y, chip_mid, chip_y + side,
438
+ pack_color(rgba[0], rgba[1], rgba[2], 1.0),
439
+ rounding=2.0, flags=imgui.DRAW_ROUND_CORNERS_LEFT)
440
+ draw_list.add_rect_filled(
441
+ chip_mid, chip_y, chip_x + side, chip_y + side,
442
+ pack_color(rgba[0], rgba[1], rgba[2], rgba[3]),
443
+ rounding=2.0, flags=imgui.DRAW_ROUND_CORNERS_RIGHT)
444
+
445
+
446
+ _FILE_TINT_FN = None
447
+
448
+
449
+ def _pill_tint(editor_ds, line0, symbol=None, token_tint=None):
450
+ """RGB a value pill at DISPLAY line `line0` wears: the background tint
451
+ under its token. Highest first: the token's own tint (its `# [tint=…]`
452
+ comment → `token_tint`; else a tinted definition named `symbol`, one
453
+ dict read off the editor's cached def-tint name map), the innermost
454
+ tinted class/def block containing the line (draw_text stamps its
455
+ fold-remapped block list as `_lv_tint_blocks`), the file's FileMeta
456
+ tint, else None (the pill's default green). Cheap by construction: the
457
+ block scan is memoized per line against the block tuple's identity
458
+ (rebuilt only when the def-tint pass rebuilds), everything else is a
459
+ handful of dict reads — this runs once per visible pill per repaint."""
460
+ global _FILE_TINT_FN
461
+ if token_tint is not None:
462
+ return tuple(token_tint[:3])
463
+ if editor_ds is None:
464
+ return None
465
+ _d = editor_ds.__dict__
466
+ if symbol is not None:
467
+ _dt = _d.get("_def_tints")
468
+ if _dt is not None and len(_dt) == 4:
469
+ _t = _dt[3].get(symbol)
470
+ if _t is not None:
471
+ return _t
472
+ blocks = _d.get("_lv_tint_blocks") or ()
473
+ memo = _d.get("_lv_pill_tint_memo")
474
+ if memo is None or memo[0] is not blocks:
475
+ memo = (blocks, {})
476
+ object.__setattr__(editor_ds, "_lv_pill_tint_memo", memo)
477
+ cache = memo[1]
478
+ got = cache.get(line0, _NO_VALUE)
479
+ if got is _NO_VALUE:
480
+ got, best = None, -1
481
+ for _l0, _i0, _e0, _t0 in blocks:
482
+ # Innermost = the containing block that starts LAST.
483
+ if _l0 <= line0 <= _e0 and _l0 > best:
484
+ best, got = _l0, tuple(_t0[:3])
485
+ cache[line0] = got
486
+ if got is not None:
487
+ return got
488
+ # File tint: the same FileMeta color the editor tab wears. The path
489
+ # comes from the editor's jump_to Address, else the editor's file_key.
490
+ _jt = _d.get("jump_to")
491
+ path = getattr(_jt, "path", None) if _jt is not None else None
492
+ if path is None:
493
+ path = _d.get("_file_meta")
494
+ if not isinstance(path, str):
495
+ return None
496
+ if _FILE_TINT_FN is None:
497
+ from meltygui.editor.text_editor import _uj_file_tint
498
+ _FILE_TINT_FN = _uj_file_tint
499
+ return _FILE_TINT_FN(path)
500
+
501
+
502
+ # Assignment operators the inline binding pill replaces the right side of:
503
+ # augmented forms first (so `+=` doesn't read as a bare `=`), walrus, then a
504
+ # bare `=` that is neither ==/<=/>=/!= nor the tail of an augmented form.
505
+ _ASSIGN_OP_RE = re.compile(
506
+ r"\*\*=|//=|>>=|<<=|[+\-*/%&|^@]=|:=|(?<![=<>!+\-*/%&|^@:])=(?!=)")
507
+
508
+
509
+ def _code_end_col(line_text):
510
+ """Column where the line's CODE ends: before an inline # comment
511
+ (quote-aware scan, so a '#' inside a string literal doesn't count) and
512
+ before trailing whitespace."""
513
+ quote = None
514
+ i = 0
515
+ n = len(line_text)
516
+ while i < n:
517
+ ch = line_text[i]
518
+ if quote is not None:
519
+ if ch == "\\":
520
+ i += 2
521
+ continue
522
+ if ch == quote:
523
+ quote = None
524
+ elif ch in "'\"":
525
+ quote = ch
526
+ elif ch == "#":
527
+ return len(line_text[:i].rstrip())
528
+ i += 1
529
+ return len(line_text.rstrip())
530
+
531
+
532
+ def _rhs_span(line_text, from_col):
533
+ """(start_col, end_col) of the assignment's right-hand side on
534
+ `line_text`, searching for the assignment operator from `from_col` (the
535
+ boxed symbol's end) — so `seq_len = input_ids.shape[1]` pills over the
536
+ RHS and reads `seq_len = 301`. None when the line has no assignment
537
+ after the symbol (bare live_view() calls, expression statements) or the
538
+ RHS continues on the next line with nothing on this one."""
539
+ match = _ASSIGN_OP_RE.search(line_text, from_col or 0)
540
+ if match is None:
541
+ return None
542
+ start = match.end()
543
+ while start < len(line_text) and line_text[start] == " ":
544
+ start += 1
545
+ end = _code_end_col(line_text)
546
+ if end <= start:
547
+ return None
548
+ return start, end
549
+
550
+
551
+ def _stacked_list_value(value, ds):
552
+ """Display-side stacking: a LIST of ≥2 same-shape tensors/ndarrays
553
+ renders as ONE stacked tensor (leading dim = list index) — covering a
554
+ raw captured list (`hiddens = output.hidden_states`), an accumulator
555
+ that fell back to its list path, and stores built by older code. Dtype/
556
+ device drift is coerced to the first element's. Anything else (ragged,
557
+ mixed kinds, non-tensor lists) passes through untouched. Memoized on
558
+ the marker's draw_state — keyed on the list's identity, length, and
559
+ first/last element identity, so a rollover overwrite or an append
560
+ rebuilds while steady-state re-renders are a tuple compare."""
561
+ if not (isinstance(value, list) and len(value) >= 2):
562
+ return value
563
+ kind = type(value[0]).__name__
564
+ if kind not in ("Tensor", "ndarray"):
565
+ return value
566
+ first = value[0]
567
+ if not all(type(v).__name__ == kind
568
+ and tuple(v.shape) == tuple(first.shape) for v in value):
569
+ return value
570
+ key = (id(value), len(value), id(value[0]), id(value[-1]))
571
+ cached = getattr(ds, "_lv_stack_cache", None)
572
+ if cached is not None and cached[0] == key:
573
+ return cached[1]
574
+ try:
575
+ if kind == "Tensor":
576
+ import torch
577
+ stacked = torch.stack([v.detach().to(first.device, first.dtype)
578
+ for v in value])
579
+ else:
580
+ import numpy as np
581
+ stacked = np.stack(value).astype(first.dtype, copy=False)
582
+ except Exception:
583
+ return value
584
+ ds._lv_stack_cache = (key, stacked)
585
+ return stacked
586
+
587
+
588
+ def _merged_dim_names(auto_dims, user_dims):
589
+ """Combined dim names for an accumulated loop-site value: the auto loop
590
+ names (leading stacked dims) followed by the site's own `# [dim_names=…]`
591
+ (the per-iteration value's dims) — ('l_idx', 'head', 'query', 'key').
592
+ Round-trip stable: a user list that ALREADY starts with the auto names
593
+ (e.g. a merged list written back into the comment by a panel edit) is
594
+ returned as-is instead of gaining a second copy of the loop dims."""
595
+ user = ([user_dims] if isinstance(user_dims, str)
596
+ else [str(d) for d in (user_dims or ())])
597
+ auto = [str(d) for d in auto_dims]
598
+ if user[:len(auto)] == auto:
599
+ return user
600
+ return auto + user
601
+
602
+
603
+ def _padded_dim_names(user_dims, ndim):
604
+ """dim_names padded to the value's dim count: a list with too few names
605
+ (or none at all) gains positional `dim<i>` entries for the unnamed
606
+ trailing axes, so every axis still gets a picker tab and an edge label.
607
+ `i` is the actual axis index — matching the `dim{i}` fallbacks the voxel
608
+ view already uses for out-of-range axes. Returns None when the names
609
+ already cover ndim (no change needed)."""
610
+ names = ([user_dims] if isinstance(user_dims, str)
611
+ else [str(d) for d in (user_dims or ())])
612
+ if not ndim or len(names) >= ndim:
613
+ return None
614
+ return names + [f"dim{i}" for i in range(len(names), ndim)]
615
+
616
+
617
+ def _build_owner_index(scope_node):
618
+ """{surfaced key → owning dict} for a scope, the same breadth-first
619
+ walk _override_owner does, done ONCE: a marker's first render used to
620
+ BFS the whole scope per marker (5.5k markers × a 5.6k-line def = 15 s
621
+ on a diff expand, 09-01). First node in BFS order wins, as before."""
622
+ from meltygui.code.libcst_conversion import _is_block_key
623
+ index = {}
624
+ queue = [scope_node]
625
+ for node in queue:
626
+ if not isinstance(node, dict):
627
+ continue
628
+ ovs = node.get("__overrides__")
629
+ for k in node.keys():
630
+ if isinstance(k, str) and k not in index:
631
+ index[k] = node
632
+ if isinstance(ovs, dict):
633
+ for ok in ovs.keys():
634
+ if (isinstance(ok, str) and len(ok) > 4 and ok.startswith("__")
635
+ and ok.endswith("__") and ok[2:-2] not in index):
636
+ index[ok[2:-2]] = node
637
+ for k, v in node.items():
638
+ if isinstance(v, dict) and _is_block_key(k):
639
+ queue.append(v)
640
+ return index
641
+
642
+
643
+ def _owner_index(host_ds, scope_node, def_key, src):
644
+ """The owner index of a def scope, memoized on `host_ds` (the editor)
645
+ per source version: `src` is the parse's source string (identity = the
646
+ content-free change signal), `def_key` = (def name, def line)."""
647
+ d = host_ds.__dict__
648
+ memo = d.get("_lv_owner_indexes")
649
+ if memo is None or memo[0] is not src:
650
+ memo = (src, {})
651
+ object.__setattr__(host_ds, "_lv_owner_indexes", memo)
652
+ index = memo[1].get(def_key)
653
+ if index is None:
654
+ index = memo[1][def_key] = _build_owner_index(scope_node)
655
+ return index
656
+
657
+
658
+ def _override_owner(scope_node, lookup_key, index_host=None, def_key=None,
659
+ src=None):
660
+ """The dict that OWNS statement `lookup_key` — the node whose
661
+ __overrides__ carries the site's `# [...]` comment — searched
662
+ breadth-first through surfaced BLOCK children (for/if/try branches, via
663
+ libcst_conversion's _is_block_key), so a loop-body site links its
664
+ comment exactly like a top-level one: the marker reads comment kwargs
665
+ from it, and live_root points set_anywhere's lazy entry at the same
666
+ level the save patcher writes back. Never descends into non-block dicts
667
+ (nested defs, CallParse args — their names live in other scopes).
668
+ Returns scope_node itself when the key isn't surfaced anywhere (sites
669
+ in with/while bodies — the conversion has no node to hang a comment
670
+ on)."""
671
+ if index_host is not None and def_key is not None and src is not None:
672
+ return _owner_index(index_host, scope_node, def_key, src).get(
673
+ lookup_key, scope_node)
674
+ from meltygui.code.libcst_conversion import _is_block_key
675
+ queue = [scope_node]
676
+ for node in queue:
677
+ if not isinstance(node, dict):
678
+ continue
679
+ ovs = node.get("__overrides__")
680
+ if (lookup_key in node
681
+ or (isinstance(ovs, dict) and f"__{lookup_key}__" in ovs)):
682
+ return node
683
+ for k, v in node.items():
684
+ if isinstance(v, dict) and _is_block_key(k):
685
+ queue.append(v)
686
+ return scope_node
687
+
688
+
689
+ def _is_funcdef_node(node):
690
+ """A def's parse from EITHER parser: a FunctionParse (core_syntax stamps no
691
+ __cst__) or a libcst FunctionDef-backed dict. isinstance, never the class
692
+ NAME: the code host's held tree is reclassed in place to
693
+ `Bubbling_FunctionParse` (bubbling.py), which is what the overlay walk
694
+ hands this function — a name check returned False for every def and the
695
+ snapshot overlay never drew a single live view (08-25)."""
696
+ if not isinstance(node, dict):
697
+ return False
698
+ from meltygui.code.libcst_conversion import FunctionParse
699
+ if isinstance(node, FunctionParse):
700
+ return True
701
+ return type(node.get("__cst__")).__name__ == "FunctionDef"
702
+
703
+
704
+ def _def_name(node):
705
+ """The def's name: `def_name` (both parsers stamp it) or the libcst node's."""
706
+ name = getattr(node, "def_name", None)
707
+ if name is not None:
708
+ return name
709
+ cst_node = node.get("__cst__") if isinstance(node, dict) else None
710
+ return getattr(getattr(cst_node, "name", None), "value", None)
711
+
712
+
713
+ def _is_def_parse(node, def_name):
714
+ return _is_funcdef_node(node) and _def_name(node) == def_name
715
+
716
+
717
+ def _find_def_node(tree, def_name, near_line):
718
+ """The def parse named `def_name` in `tree` (a module or span parse).
719
+ Module-level defs are a direct key lookup; otherwise (methods, nested
720
+ defs) a plain dict walk, and when the name repeats (same-named methods
721
+ on two classes) the one whose span starts nearest `near_line`. The
722
+ caller memoizes per tree."""
723
+ direct = tree.get(def_name) if isinstance(tree, dict) else None
724
+ if _is_def_parse(direct, def_name):
725
+ return direct
726
+ best, best_d = None, None
727
+ stack, seen = [tree], set()
728
+ while stack:
729
+ node = stack.pop()
730
+ if not isinstance(node, dict) or id(node) in seen:
731
+ continue
732
+ seen.add(id(node))
733
+ if _is_def_parse(node, def_name):
734
+ sp = getattr(node, "span", None)
735
+ d = abs((getattr(sp, "start_line", 0) or 0) - (near_line or 0))
736
+ if best is None or d < best_d:
737
+ best, best_d = node, d
738
+ for v in node.values():
739
+ if isinstance(v, dict):
740
+ stack.append(v)
741
+ return best
742
+
743
+
744
+ def _host_held_tree(node):
745
+ """The tree the owning code host CURRENTLY holds — the one its chain_out
746
+ serializes — reached from ANY generation of the site's dict: every
747
+ parse dict keeps `_bubble_root` (the dict RenderHost) after a reparse
748
+ orphans it, and the host's `_held()` is the live tree. None when the
749
+ node isn't host-backed (tests, plain parses)."""
750
+ root = getattr(node, "_bubble_root", None)
751
+ held = getattr(root, "_held", None)
752
+ if callable(held):
753
+ try:
754
+ cur = held()
755
+ except Exception:
756
+ return None
757
+ if isinstance(cur, dict):
758
+ return cur
759
+ return None
760
+
761
+
762
+ def current_live_root(ds):
763
+ """The dict that owns a live site's `# [...]` comment, resolved against
764
+ the tree the code host CURRENTLY holds — for the marker's own comment
765
+ splat, set_anywhere's `# [<key>]` source row, and the replayed window's
766
+ comment re-splat in Melty.draw.
767
+
768
+ Why not the stamped `live_root` (or the editor's code_dict kwarg): every
769
+ reparse (incremental merges included) REPLACES the def dict, its block
770
+ dicts and their __overrides__. The marker re-stamps only when it renders
771
+ (an open window whose def is scrolled off-viewport is pruned whole by
772
+ the overlay walk), and the editor's code_dict is whatever the cached
773
+ tabs body last captured from the host — both lag the host's held tree
774
+ by an arbitrary number of frames. A panel edit then wrote into an
775
+ orphan: the replay re-splatted the value from it (the UI moved), the
776
+ host serialized its OWN tree without it, and the next fresh render
777
+ handed the window the real tree — the value snapped back and
778
+ anywhere_value's cross-check fired ("settled at 1, not the TensorDim(3)
779
+ that was set"), most often when one write's round trip overlapped the
780
+ next write. Resolving through the host puts the write where the save
781
+ reads, whatever the editor happens to be showing.
782
+
783
+ Anchor: the stamped dict's `_bubble_root` → host → `_held()`; the
784
+ editor's code_dict/code_tree is the fallback when the site isn't
785
+ host-backed, then the stamp itself. Memoized per tree on the ds (tree
786
+ identity + its `source` str — the content-free change signal) and kept
787
+ across renders, so the def lookup runs once per reparse per ds."""
788
+ d = ds.__dict__
789
+ stamped = d.get("live_root")
790
+ loc = d.get("_lv_locator")
791
+ if loc is None:
792
+ return stamped
793
+ editor_ds, def_name, def_line = loc
794
+ tree = _host_held_tree(stamped)
795
+ if tree is None and editor_ds is not None:
796
+ # draw_text params (auto-state markers): the spanned node tree
797
+ # rides code_dict on the non-host route (code_tree rides the error
798
+ # dict case) and code_tree on all chain routes.
799
+ ed = editor_ds.__dict__
800
+ tree = ed.get("code_dict")
801
+ if not isinstance(tree, dict):
802
+ tree = ed.get("code_tree")
803
+ if not isinstance(tree, dict):
804
+ return stamped
805
+ src = getattr(tree, "source", None)
806
+ cache = d.get("_lv_owner_cache")
807
+ if (cache is not None and cache[0] is tree
808
+ and (src is None or cache[1] is src)):
809
+ return cache[2]
810
+ # Def lookup + owner BFS memoized on the EDITOR ds per (tree, source):
811
+ # per marker ds they ran once each — 5.5k first renders on a diff
812
+ # expand walked the def 11k times (09-01).
813
+ if editor_ds is not None:
814
+ _dmemo = editor_ds.__dict__.get("_lv_def_nodes")
815
+ if _dmemo is None or _dmemo[0] is not tree or _dmemo[1] is not src:
816
+ _dmemo = (tree, src, {})
817
+ object.__setattr__(editor_ds, "_lv_def_nodes", _dmemo)
818
+ node = _dmemo[2].get((def_name, def_line), _NO_VALUE)
819
+ if node is _NO_VALUE:
820
+ node = _dmemo[2][(def_name, def_line)] = _find_def_node(
821
+ tree, def_name, def_line)
822
+ else:
823
+ node = _find_def_node(tree, def_name, def_line)
824
+ scope = node.get("locals") if isinstance(node, dict) else None
825
+ key = d.get("live_key")
826
+ owner = stamped
827
+ if isinstance(scope, dict) and isinstance(key, str):
828
+ owner = _override_owner(scope, key, index_host=editor_ds,
829
+ def_key=(def_name, def_line),
830
+ src=src if editor_ds is not None else None)
831
+ if owner is not stamped:
832
+ # Keep the stamp fresh for anything still reading it raw.
833
+ object.__setattr__(ds, "live_root", owner)
834
+ object.__setattr__(ds, "_lv_owner_cache", (tree, src, owner))
835
+ return owner
836
+
837
+
838
+ # First-spawn size estimates for the display clamps below: the real size
839
+ # only exists after the window's first render (the spawn must pass it).
840
+ # Height: voxel/value windows both land in this size. Width: matches
841
+ # LIVE_WINDOW's initial width in view.py, which IS the first-spawn width.
842
+ _SPAWN_EST_HEIGHT = 380.0
843
+ _SPAWN_EST_WIDTH = 400.0
844
+
845
+
846
+ def _left_of_window_pos(anchor_left, marker_x, marker_y=None, win_h=None,
847
+ win_w=None, gap=50.0, screen_margin=10.0):
848
+ """Parent-relative window_pos that opens a spawned live-value window just
849
+ to the LEFT of the draw text, vertically level with the marker — instead
850
+ of on top of the code the marker sits in.
851
+
852
+ `anchor_left` is the absolute x of the draw text's left edge (the editor
853
+ draw_state's abs_left — NOT the enclosing window's edge, which can sit
854
+ far left of the text in multi-pane layouts). The value window's RIGHT
855
+ edge sits `gap` left of it, so its left edge needs `win_w` (the live
856
+ width on reopen, the LIVE_WINDOW initial width on first spawn).
857
+
858
+ window_pos is relative to the spawned window's parent (the same editor
859
+ window), and a marker renders at the cursor (abs_left == marker_x when
860
+ window_pos is 0), so the parent-origin x offset is exactly marker_x:
861
+ subtract it from the target absolute left edge to land there. Keeping
862
+ window_pos parent-relative means the value window then tracks the
863
+ editor window as it moves. Returns None when there's nothing to anchor
864
+ to (caller falls back to the default on-cursor placement).
865
+
866
+ `marker_y` enables the display-bottom clamp: a marker near the screen
867
+ bottom would otherwise spawn its window mostly below the display (the
868
+ pinned-anchor bound in _pinned_base_y clamps to the EDITOR window's box,
869
+ which can itself reach the display bottom, and window_pos is
870
+ deliberately outside that bound). The y offset lifts the window just
871
+ enough that `win_h` (the live height on reopen, an estimate on first
872
+ spawn) fits above the display bottom, floored so the top never leaves
873
+ the screen.
874
+
875
+ The x offset gets the same treatment against the display's LEFT edge:
876
+ a draw text flush against it would spawn the value window entirely
877
+ off-screen. Floored so the left edge stays on screen — when the display
878
+ can't fit both, keeping the left edge visible wins."""
879
+ if anchor_left is None:
880
+ return None
881
+ disp = Core.melty.display_size
882
+ est_w = win_w or _SPAWN_EST_WIDTH
883
+ x_off = anchor_left - gap - est_w - marker_x
884
+ if disp:
885
+ x_off = min(x_off, disp[0] - screen_margin - est_w - marker_x)
886
+ x_off = max(x_off, screen_margin - marker_x) # left edge on screen
887
+ y_off = 0.0
888
+ if marker_y is not None and disp:
889
+ est = win_h or _SPAWN_EST_HEIGHT
890
+ y_off = min(0.0, disp[1] - screen_margin - est - marker_y)
891
+ y_off = max(y_off, -marker_y) # keep the title bar on screen
892
+ return (x_off, y_off)
893
+
894
+
895
+ def _token_in_selection(line, start_col, end_col, sel_lo, sel_hi):
896
+ """True when the editor selection is EXACTLY the token on buffer `line`
897
+ spanning [start_col, end_col) ((line, col) tuples, None when the editor
898
+ has no selection / isn't focused). Exact on purpose: only selecting the
899
+ symbol and JUST the symbol (a double-click select) previews its value
900
+ window — a sweep that happens to contain instrumented tokens must not
901
+ pop windows over the text being selected."""
902
+ if line is None or sel_lo is None or sel_hi is None:
903
+ return False
904
+ return sel_lo == (line, start_col) and sel_hi == (line, end_col)
905
+
906
+
907
+ def _line_in_selection(line, sel_lo, sel_hi):
908
+ """True when buffer `line` (1-based) intersects the editor selection's
909
+ line range — inline value pills hide there so the text being selected
910
+ stays readable."""
911
+ return (line is not None and sel_lo is not None and sel_hi is not None
912
+ and sel_lo[0] <= line <= sel_hi[0])
913
+
914
+
915
+ def _draw_marker_at(editor_ds, pos, cursor_inside, token_span, token,
916
+ **marker_kwargs):
917
+ """Draw a live-view marker at screen `pos` — unless its token lies inside
918
+ the editor selection, in which case it is QUEUED on the editor and drawn
919
+ by `flush_selected_markers` after the overlay walk. Only ONE selected
920
+ token previews its value window at a time (the last one selected), and
921
+ which one can only be decided once every selected token of the frame is
922
+ known — deferring the calls keeps that decision lag-free (no frame where
923
+ two previews show) and keeps all marker state inside the marker body.
924
+ `token_span` = (buffer line, start_col, end_col) in the selection's
925
+ coordinates; `token` is the marker's positional input_value."""
926
+ if cursor_inside and editor_ds is not None:
927
+ fc = Core.melty.frame_count
928
+ if getattr(editor_ds, "_lv_sel_frame", None) != fc:
929
+ editor_ds._lv_sel_frame = fc
930
+ editor_ds._lv_sel_pending = []
931
+ editor_ds._lv_sel_pending.append(
932
+ (token_span, pos, token, marker_kwargs))
933
+ return
934
+ imgui.set_cursor_screen_pos(pos)
935
+ draw_live_view_marker(token, cursor_inside=cursor_inside,
936
+ editor_ds=editor_ds, **marker_kwargs)
937
+
938
+
939
+ def _last_selected(pending, sel_caret):
940
+ """Index into `pending` of the LAST-selected token: the one nearest the
941
+ caret end of the selection. The caret is the moving end of every
942
+ selection gesture (shift+arrows, drag, shift+click), so the token it
943
+ sits nearest is the one the selection most recently grew over — and
944
+ when the selection shrinks back off a token, the remaining nearest one
945
+ takes over. Stateless, so it needs no memory of entry order. "Nearest" is
946
+ in DOCUMENT order: on the caret's line the closer edge, on a line above
947
+ the later token, on a line below the earlier one (a column gap only
948
+ means something on the caret's own line). A missing caret falls to the
949
+ last token in document order."""
950
+ if sel_caret is None:
951
+ return max(range(len(pending)),
952
+ key=lambda i: pending[i][0][:2])
953
+ cl, cc = sel_caret
954
+
955
+ def _key(i):
956
+ line, c0, c1 = pending[i][0]
957
+ if line == cl:
958
+ return (0, min(abs(c0 - cc), abs(c1 - cc)))
959
+ if line < cl:
960
+ return (cl - line, -c1) # above the caret: later = nearer
961
+ return (line - cl, c0) # below the caret: earlier = nearer
962
+ return min(range(len(pending)), key=_key)
963
+
964
+
965
+ def flush_selected_markers(draw_state=None, sel_caret=None, **kwargs):
966
+ """text_editor calls this once after the overlay walk (inside its
967
+ cursor-neutral bracket): draw the tokens `_draw_marker_at`
968
+ queued for this frame, passing cursor_inside=True to the elected one only
969
+ — the others render exactly as if the caret had left them, closing any
970
+ preview they were showing. The queue is dropped here whatever happens:
971
+ its entries carry the captured values (tensors), so a lingering queue
972
+ would pin a run's generation."""
973
+ pending = getattr(draw_state, "_lv_sel_pending", None) if draw_state else None
974
+ if draw_state is not None:
975
+ draw_state._lv_sel_pending = None
976
+ if not pending:
977
+ return
978
+ winner = _last_selected(pending, sel_caret)
979
+ for i, (_span, pos, token, mk) in enumerate(pending):
980
+ imgui.set_cursor_screen_pos(pos)
981
+ draw_live_view_marker(token, cursor_inside=(i == winner),
982
+ editor_ds=draw_state, **mk)
983
+
984
+
985
+ def _marker_idle_skip(editor_ds, name, x, y, w, h, captured, cursor_inside,
986
+ store_obj, key_path, buffer_line, auto_open):
987
+ """True when marker `name` is provably a NO-OP this frame, letting the
988
+ overlay skip its ~90µs @render_func call entirely. An idle marker (not
989
+ hovered, no caret inside, no open or pending value window, no hover/
990
+ cursor edge left to clear) draws nothing — its only per-frame work is the
991
+ gutter registration and the first-publish watch, both replicated here
992
+ raw. The marker's draw_state comes from editor_ds._lv_marker_ds (stamped
993
+ by the body), so the FIRST render of each marker always takes the full
994
+ path to create it; steady state is a dict hit + a rect test."""
995
+ reg = getattr(editor_ds, "_lv_marker_ds", None) if editor_ds else None
996
+ mds = reg.get(name) if reg else None
997
+ if mds is None:
998
+ return False
999
+ io = imgui.get_io()
1000
+ if x <= io.mouse_pos.x < x + w and y <= io.mouse_pos.y < y + h:
1001
+ return False
1002
+ if (cursor_inside or getattr(mds, "_lv_hovered", False)
1003
+ or getattr(mds, "_lv_cursor_in", False)):
1004
+ return False # interaction, or cursor edge → body must observe
1005
+ wds = getattr(mds, "_lv_window_ds", None)
1006
+ if getattr(mds, "_lv_open", False) or (wds is not None and not wds.closed):
1007
+ return False # open window streams values through the body
1008
+ if captured and auto_open and getattr(mds, "_lv_open", None) is None:
1009
+ return False # first value seen → body must auto-open
1010
+ # Idle - replicate the body's cheap registrations and bail.
1011
+ # Dismissed-latch reset (the body does this while focused with the caret
1012
+ # outside the symbol - skipping every such frame would leave the cursor
1013
+ # preview permanently suppressed after one X-close).
1014
+ if (editor_ds is not None and Core.melty.text_focused_ds is editor_ds
1015
+ and getattr(mds, "_lv_cursor_dismissed", False)):
1016
+ mds._lv_cursor_dismissed = False
1017
+ if editor_ds is not None and buffer_line is not None:
1018
+ if getattr(editor_ds, "_lv_gutter_frame", None) != Core.melty.frame_count:
1019
+ editor_ds._lv_gutter_frame = Core.melty.frame_count
1020
+ editor_ds._lv_gutter_markers = {}
1021
+ editor_ds._lv_gutter_markers.setdefault(buffer_line, []).append(mds)
1022
+ watch(store_obj, key_path, mds, first_only=True)
1023
+ return True
1024
+
1025
+
1026
+ from meltygui.view.code_view import draw_live_view_overlay
1027
+
1028
+
1029
+ def _ds_in_window(ds, win_ds, max_hops=64):
1030
+ """True when `ds` sits inside `win_ds`'s subtree. Walks BOTH up-links —
1031
+ the render-tree `_parent` chain and `parent_window` — because a deferred
1032
+ satellite (e.g. the voxel controls panel) parents to its window via
1033
+ parent_window while its `_parent` chain, stamped at queue time, isn't
1034
+ guaranteed to pass through the window after a root_draw_states
1035
+ re-dispatch. Identity-set + hop cap bound the walk (chains can
1036
+ self-parent at their root)."""
1037
+ seen, stack = set(), [ds]
1038
+ while stack and len(seen) < max_hops:
1039
+ node = stack.pop()
1040
+ if node is None or id(node) in seen:
1041
+ continue
1042
+ if node is win_ds:
1043
+ return True
1044
+ seen.add(id(node))
1045
+ if node._parent is not node:
1046
+ stack.append(node._parent)
1047
+ stack.append(getattr(node, "parent_window", None))
1048
+ return False
1049
+
1050
+
1051
+ def _mouse_in_window_tree(win_ds, mx, my):
1052
+ """True when (mx, my) is inside `win_ds`'s rect or any open window
1053
+ parented into its subtree — the value window's satellites (the voxel
1054
+ controls panel, a context menu) are separate root windows positioned
1055
+ OUTSIDE the window's own rect, so a press on them must count as
1056
+ engaging with the window."""
1057
+ def _hit(w):
1058
+ try:
1059
+ x, y = w.abs_left, w.abs_top
1060
+ return (x <= mx < x + (w.width or 0)
1061
+ and y <= my < y + (w.height or 0))
1062
+ except Exception:
1063
+ return False
1064
+ if _hit(win_ds):
1065
+ return True
1066
+ for lst in Core.melty.root_draw_states.values():
1067
+ for w in lst:
1068
+ if (w is not win_ds and not w.closed and _hit(w)
1069
+ and _ds_in_window(w, win_ds)):
1070
+ return True
1071
+ return False
1072
+
1073
+
1074
+ # auto_state=False: every named param would otherwise be MIRRORED onto the
1075
+ # draw_state (draw_state.value / .store_obj + the _auto_baseline copy) - so a
1076
+ # marker that stops rendering (culled off-viewport, key pruned) would keep
1077
+ # pinning its last tensor AND its last store-owning function (whose
1078
+ # __live_values__ may hold gigabytes of a superseded run) through those
1079
+ # mirrors. The marker writes none of its params, so it needs no auto-state.
1080
+ from meltygui.view.code_view import draw_live_view_marker
1081
+
1082
+
1083
+ _VALUE_ATTRS = ("_raw_input_value", "_input_value", "_original_input_ref")
1084
+ _VALUE_KWARGS = ("input_value", "value")
1085
+
1086
+
1087
+ def _sever_value_pins(window_ds):
1088
+ """Under `window_ds`, cut every GLState-cached TORCH reference to the
1089
+ displayed value while keeping the cache entry's metadata: the
1090
+ cuda_march path's `cuda_view` (a CudaVolumeView over a strided view of
1091
+ the source) has its `.view` nulled and its key cleared, so the previous
1092
+ generation can die, yet draw_voxels' hold-last-frame path can still
1093
+ read source_shape/mapping off it to keep the slice sliders up. The FBO
1094
+ / last image / GL-path textures are display-GPU objects and stay."""
1095
+ from meltygui.core.graphics.gl_state import GLState
1096
+ for state in GLState.states_under(window_ds):
1097
+ cv = state.peek("cuda_view")
1098
+ if cv is not None:
1099
+ try:
1100
+ cv.view = None
1101
+ cv._vol_key = None # never a cache hit again
1102
+ except Exception:
1103
+ state.drop("cuda_view")
1104
+
1105
+
1106
+ def release_live_value(ds, gl=True, keep_image=False):
1107
+ """Drop every reference a live-value VIEW holds to its captured value, so
1108
+ a tensor the store no longer serves can actually die.
1109
+
1110
+ Draw_states persist (that's the framework contract — a closed window
1111
+ keeps its size/position/params and lazily re-renders), and the wrapper
1112
+ stamps the last-rendered value onto them (`_kwargs['input_value']`,
1113
+ `_raw_input_value`, ...). For the live lab that is the leak: each run
1114
+ publishes NEW tensors (the per-layer accumulators are hundreds of MB),
1115
+ so a marker whose key was pruned (a renamed/removed assignment — every
1116
+ few keystrokes while typing) or a value window the user X-closed kept
1117
+ its last tensor — AND its GLState volume texture — pinned for the rest
1118
+ of the session, one generation per orphan. VRAM climbed with every
1119
+ edit; torch.cuda.empty_cache() can't help while the refs live.
1120
+
1121
+ Called when a key is pruned (live_view._prune_keys, for the marker and
1122
+ its window) and by the marker whenever its window is closed. The marker
1123
+ re-supplies the value on the next show (draw_any(value, draw_state=win)),
1124
+ so nothing is lost: only the STALE copy goes. GL resources under the
1125
+ window are released through the same path a deleted window takes
1126
+ (GLState.on_window_deleted — queued deletes, drained on the GL thread);
1127
+ the window lazily re-uploads when reopened. Safe from any thread (attr
1128
+ writes, queued GL deletes) and idempotent. `keep_image=True` (the
1129
+ fresh-run release) only severs the cached torch refs (_sever_value_pins)
1130
+ and leaves the FBO / last image, so the window holds its last frame
1131
+ until the new value arrives."""
1132
+ if ds is None:
1133
+ return
1134
+ targets = [ds]
1135
+ try:
1136
+ targets.extend(ds.descendants(max_depth=8))
1137
+ except Exception:
1138
+ pass
1139
+ from meltygui.core.core_render import release_input_refs
1140
+ for d in targets:
1141
+ # The wrapper owns more refs than the obvious two: the offscreen
1142
+ # blit stamps `_input_value_cache` (mark_start_offscreen) and the
1143
+ # change detector keeps `_input_cache["external_state"]` - a closed
1144
+ # value window kept a 13 GB stack alive via exactly those.
1145
+ # release_input_refs is the wrapper's own complete list.
1146
+ try:
1147
+ release_input_refs(d)
1148
+ except Exception:
1149
+ pass
1150
+ for attr in _VALUE_ATTRS:
1151
+ if getattr(d, attr, None) is not None:
1152
+ try:
1153
+ setattr(d, attr, None)
1154
+ except Exception:
1155
+ pass
1156
+ kw = getattr(d, "_kwargs", None)
1157
+ if isinstance(kw, dict):
1158
+ for k in _VALUE_KWARGS:
1159
+ if kw.get(k) is not None:
1160
+ kw[k] = None
1161
+ if gl:
1162
+ try:
1163
+ from meltygui.core.graphics.gl_state import GLState
1164
+ if keep_image:
1165
+ _sever_value_pins(ds)
1166
+ else:
1167
+ GLState.on_window_deleted(ds)
1168
+ except Exception:
1169
+ pass
1170
+
1171
+
1172
+ def _drop_captured_value(store_obj, key_path):
1173
+ """Forget one captured value's DATA on X-close: the loop accumulator is
1174
+ dropped, the store entry is swapped for the rerun hint, and every
1175
+ marker/window pin + GL texture is severed (live_view.park_rerun_hint).
1176
+ The KEY stays — the widget must keep rendering as captured so it can be
1177
+ reopened; the next run with it open refills it."""
1178
+ if store_obj is None or key_path is None:
1179
+ return
1180
+ try:
1181
+ from meltygui.code.live_view import park_rerun_hint
1182
+ park_rerun_hint(store_obj, key_path)
1183
+ except Exception as e:
1184
+ print(f"live_view: park hint for {key_path} failed: {e!r}")
1185
+ # The tensor is unreferenced now; hand its blocks back to the system so
1186
+ # the VRAM actually drops (allocator cache → empty_cache), off-thread.
1187
+ try:
1188
+ from meltygui.core.runtime.gc_manager import release_cuda_cache_soon
1189
+ release_cuda_cache_soon(label="live view close")
1190
+ except Exception:
1191
+ pass
1192
+
1193
+
1194
+ def _auto_run_on_user_open(editor_ds, store_obj):
1195
+ """User opened a value window: with Auto Execute on for the def, the
1196
+ def widget recompiles + runs so the window fills (text_editor.
1197
+ fnrun_auto_run_on_open — coalesced there). Never raises."""
1198
+ try:
1199
+ from meltygui.editor.text_editor import fnrun_auto_run_on_open
1200
+ fnrun_auto_run_on_open(editor_ds, store_obj)
1201
+ except Exception as e:
1202
+ print(f"live_view: auto-run on open failed: {e!r}")
1203
+
1204
+
1205
+ def set_marker_open(marker_ds, open_):
1206
+ """Gutter-button entry point: latch a marker's value window open/closed
1207
+ from OUTSIDE the marker body (raw draw-list button, no render_func).
1208
+ Mirrors the double-click toggle: flipping open snaps an existing window
1209
+ back to the left of the editor window (it may have been dragged onto
1210
+ the code). The marker ds is invalidated so its body re-runs and
1211
+ creates/hides the window on the next editor render; the CALLER must
1212
+ invalidate the editor tile itself (the marker only renders inside the
1213
+ editor's overlay pass)."""
1214
+ open_ = bool(open_)
1215
+ if bool(getattr(marker_ds, "_lv_open", False)) == open_:
1216
+ return
1217
+ marker_ds._lv_open = open_
1218
+ from meltygui.core.windowing.window_visibility import marker_user_visibility
1219
+ marker_user_visibility(marker_ds, not open_)
1220
+ win_ds = getattr(marker_ds, "_lv_window_ds", None)
1221
+ if open_:
1222
+ _auto_run_on_user_open(getattr(marker_ds, "_lv_editor_ds", None),
1223
+ (getattr(marker_ds, "_kwargs", None) or {}).get("store_obj"))
1224
+ if (open_ and win_ds is not None
1225
+ and "window_pos" not in (win_ds._kwargs or {})):
1226
+ # Clear the window's stale closed flag NOW: this latch is set from
1227
+ # OUTSIDE the marker body (the gutter runs after a close left
1228
+ # closed=True), and the body's own "closed via the header X" check
1229
+ # (win_ds.closed and _lv_open) runs before it repaints the window -
1230
+ # without this reset it reads the previous close as a fresh X click
1231
+ # and cancels the reopen on the spot.
1232
+ win_ds.closed = False
1233
+ _ed = getattr(marker_ds, "_lv_editor_ds", None)
1234
+ pos = _left_of_window_pos(_ed.abs_left if _ed is not None else None,
1235
+ marker_ds.abs_left,
1236
+ marker_y=marker_ds.abs_top,
1237
+ win_h=win_ds.height,
1238
+ win_w=win_ds.width)
1239
+ if pos is not None:
1240
+ win_ds.window_pos = pos
1241
+ marker_ds.invalidate()
1242
+
1243
+
1244
+ from meltygui.view.code_view import draw_snapshot_overlay
1245
+
1246
+
1247
+ def _draw_usage_labels(draw_state, fn, node, span, source_lines, snap_vals,
1248
+ anchor_lines, anchor_keys, origin_x, origin_y,
1249
+ char_w, line_px, lmap, clip, col_shift=0,
1250
+ binding_pills=None):
1251
+ """Every later USAGE of a captured symbol reads `seq_len=384` — the
1252
+ value is INSERTED right after the symbol and the rest of the line
1253
+ shifts to make room, so name and value show together (the same
1254
+ grid-bending draw_text uses for inline color swatches).
1255
+
1256
+ Two halves, one frame apart: this pass STAMPS the wanted gaps on the
1257
+ editor ds (`_lv_trail_views`: def start → (frame, {(display line0,
1258
+ buffer col): cells}); a change bumps `_lv_trail_gen` + invalidates, and
1259
+ draw_text's _window folds them into _build_vcols as positional trails
1260
+ on its next layout, publishing each gap's start CELL back in
1261
+ `_lv_trail_cells`) — and PAINTS the value pill into every gap already
1262
+ laid out. Raw draw-list paint: no draw_states, no hit rects (clicks
1263
+ fall through; caret/click math stays exact through vcols), no captures
1264
+ — each pill reads the binding's single store entry at draw time (see
1265
+ live_usage's module docstring). The occurrence index is memoized per
1266
+ (source, def, store size); the per-repaint work is one cheap loop over
1267
+ the def's occurrences. Stale defs' stamps are pruned by draw_text after
1268
+ the overlay pass. No caret/selection suppression here: the code text
1269
+ stays fully visible and closing the gap under an active caret would
1270
+ shift the line mid-edit."""
1271
+ from meltygui.core.runtime.toggles import Toggles
1272
+ usages_on = bool(Toggles.TextEditor.live_inline_usages)
1273
+ if not snap_vals or (not usages_on and not binding_pills):
1274
+ return
1275
+ frame = Core.melty.frame_count
1276
+ if not usages_on:
1277
+ # Binding pills only - no occurrence index needed.
1278
+ return _stamp_and_paint(draw_state, fn, span, (), {}, snap_vals,
1279
+ origin_x, origin_y, char_w, line_px, lmap,
1280
+ clip, col_shift, frame, binding_pills)
1281
+ memo = draw_state.__dict__.get("_lv_usage_memo")
1282
+ if memo is None or memo[0] is not source_lines:
1283
+ memo = (source_lines, {})
1284
+ object.__setattr__(draw_state, "_lv_usage_memo", memo)
1285
+ entry = memo[1].get(span.start_line)
1286
+ _fresh_owner = (entry is not None and len(entry) >= 6
1287
+ and entry[0]() is fn)
1288
+ # Key-count changes DEBOUNCE (30 frames): a run adds hundreds of
1289
+ # keys over frames, and rebuilding (a whole-def tokenize) per repaint
1290
+ # during the burst is O(def) per frame. New bindings' pills appear afte
1291
+ # the burst settles; a source/def change rebuilds ASAP.
1292
+ if not _fresh_owner or (entry[1] != len(snap_vals)
1293
+ and frame - entry[5] > 30):
1294
+ labels = vars(fn).get("__live_labels__") or {}
1295
+ # Binding sites extracted from the store's reverse index: name →
1296
+ # (all binding lines, their store keys). The store IS the
1297
+ # binding registry - a usage can only ever show a captured value.
1298
+ bindings = {}
1299
+ for line, key in zip(anchor_lines, anchor_keys):
1300
+ name = live_usage.binding_name(key, labels)
1301
+ if name is None:
1302
+ continue
1303
+ binding_lines, binding_keys = bindings.setdefault(name, ([], []))
1304
+ binding_lines.append(line)
1305
+ binding_keys.append(key)
1306
+ # Nested defs: an occurrence inside a closure's body is that
1307
+ # scope's own name (or a closure read at a DIFFERENT time), never a
1308
+ # plain read of an outer binding - keep out.
1309
+ exclude = []
1310
+ _locals = node.get("locals") if isinstance(node, dict) else None
1311
+ if isinstance(_locals, dict):
1312
+ for child in _locals.values():
1313
+ if isinstance(child, dict) and _is_funcdef_node(child):
1314
+ child_span = getattr(child, "span", None)
1315
+ if child_span is not None:
1316
+ exclude.append((child_span.start_line + 1,
1317
+ child_span.end_line))
1318
+ def_text = "\n".join(source_lines[span.start_line - 1:span.end_line])
1319
+ occurrences = live_usage.build_usage_index(
1320
+ def_text, span.start_line,
1321
+ {n: b[0] for n, b in bindings.items()}, tuple(exclude))
1322
+ entry = (weakref.ref(fn), len(snap_vals), occurrences, bindings,
1323
+ [o[0] for o in occurrences], frame)
1324
+ memo[1][span.start_line] = entry
1325
+ occurrences, bindings = entry[2], entry[3]
1326
+ # VISIBLE BAND ONLY: draw_text renders 20k+ line files and a ne
1327
+ # snapshot binds every local, so the full occurrence list is huge;
1328
+ # per-frame work must stay O(visible). Same approach as the anchor
1329
+ # index: occurrences are line-sorted, bisect the band (display band
1330
+ # ±64 lines of slack for in-flight edit shifts, matching _build_key_index)
1331
+ # and only those need the _lmap / resolve / stamp loop.
1332
+ if clip is not None and line_px:
1333
+ occ_lines = entry[4]
1334
+ band_lo, band_hi = _parse_line_band(lmap, clip, origin_y, line_px)
1335
+ i0 = bisect.bisect_left(occ_lines, band_lo)
1336
+ i1 = bisect.bisect_right(occ_lines, band_hi)
1337
+ occurrences = occurrences[i0:i1]
1338
+ return _stamp_and_paint(draw_state, fn, span, occurrences, bindings,
1339
+ snap_vals, origin_x, origin_y, char_w, line_px,
1340
+ lmap, clip, col_shift, frame, binding_pills)
1341
+
1342
+
1343
+ def _parse_line_band(lmap, clip, origin_y, line_px, slack=64):
1344
+ """The visible band as PARSE lines (1-based, `_lv_key_index` /
1345
+ occurrence-list space): the clip's rows are DISPLAY lines, and with
1346
+ folds collapsed a display line sits at a larger buffer line — a band
1347
+ taken straight from the rows bisected the buffer-sorted anchor lists
1348
+ short of every marker past ~64 folded lines (the live views vanished
1349
+ towards the end of a file with its `# [` comments hidden, 09-02). The
1350
+ line map carries the fold layout (`_d2b`: buffer line per display
1351
+ line); rows past its end extend at the same rate. `slack` rows either
1352
+ side cover in-flight edit shifts (the bridge)."""
1353
+ lo = int((clip[1] - origin_y) / line_px)
1354
+ hi = int((clip[3] - origin_y) / line_px)
1355
+ d2b = getattr(lmap, "_d2b", None) if lmap is not None else None
1356
+ if d2b:
1357
+ n = len(d2b)
1358
+
1359
+ def _to_buf(dl):
1360
+ if dl < 0:
1361
+ return dl
1362
+ if dl < n:
1363
+ return d2b[dl]
1364
+ return d2b[-1] + (dl - n + 1)
1365
+ lo, hi = _to_buf(lo), _to_buf(hi)
1366
+ return lo - slack, hi + slack + 1
1367
+
1368
+
1369
+ def _stamp_and_paint(draw_state, fn, span, occurrences, bindings, snap_vals,
1370
+ origin_x, origin_y, char_w, line_px, lmap, clip,
1371
+ col_shift, frame, binding_pills):
1372
+ """The trailing-gap stamp + paint shared by usage labels and binding
1373
+ pills (see _draw_usage_labels)."""
1374
+ trails = draw_state.__dict__.setdefault("_lv_trail_views", {})
1375
+ _fvars = vars(fn)
1376
+ publish_seq = _fvars.get("__live_pub_seq__") or {}
1377
+ # governing_key is O(bindings above the line) - a name rebound dozens
1378
+ # of times in a big def (`x`, `_ti`) with ~100 visible occurrences
1379
+ # was a per-frame scan in the thousands. Its answer only changes when
1380
+ # the bindings index rebuilds or a publish re-orders the sequence
1381
+ # (`__live_pub_gen__` bumped by add_view beside __live_pub_seq__).
1382
+ _pub_gen = _fvars.get("__live_pub_gen__", 0)
1383
+ _gov_memo = draw_state.__dict__.get("_lv_gov_memo")
1384
+ if (_gov_memo is None or _gov_memo[0] is not bindings
1385
+ or _gov_memo[1] != _pub_gen):
1386
+ _gov_memo = (bindings, _pub_gen, {})
1387
+ object.__setattr__(draw_state, "_lv_gov_memo", _gov_memo)
1388
+ _gov = _gov_memo[2]
1389
+ # Occurrence loop memo (a no-clip pane case hands EVERY occurrence of
1390
+ # the def here, ~5k in draw_text, every frame): its two outputs only
1391
+ # change with the occurrence index, a publish, the fold layout (the
1392
+ # fresh per-frame index keys its layout as `_d2b`, see draw_text's
1393
+ # _tv_fold_lm) or the geometry. An empty layout (no `_d2b`) means a
1394
+ # merge in flight - no memo. Watches were registered on the pass that
1395
+ # built the entry; they persist.
1396
+ _lm_key = getattr(lmap, "_lv_key", None) if lmap is not None else None
1397
+ _ok_key = (clip is None and (lmap is None or _lm_key is not None)
1398
+ and id(occurrences)) or None
1399
+ # The binding-pill LIST rides in the key by identity: the snapshot
1400
+ # overlay's full-pass memo hands the SAME list object frame after
1401
+ # frame while no key changes state (and pins it in its memo), so a
1402
+ # hit here covers the pills merge below as well.
1403
+ _ok = (_ok_key, _pub_gen, _lm_key, origin_y, line_px, col_shift,
1404
+ id(binding_pills) if binding_pills else 0)
1405
+ _om = draw_state.__dict__.get("_lv_stamp_memo")
1406
+ _hit = bool(_ok_key) and _om is not None and _om[0] == _ok
1407
+ if _hit:
1408
+ sub = _om[1] # shared: never mutated past this point
1409
+ paints = _om[2] # line-sorted, pills merged
1410
+ occurrences = ()
1411
+ binding_pills = None
1412
+ else:
1413
+ sub = {} # (display line0, buffer boundary col) → gap cells
1414
+ paints = [] # visible pills, painted after the stamp below
1415
+ for line, col, name, _last in occurrences:
1416
+ _ml = lmap(line) if lmap else line
1417
+ if _ml is None:
1418
+ continue # inside the mid-edit region - skip a wash
1419
+ _gk = (line, name)
1420
+ key = _gov.get(_gk, _NO_VALUE)
1421
+ if key is _NO_VALUE:
1422
+ binding_lines, binding_keys = bindings[name]
1423
+ key = live_usage.governing_key(binding_lines, binding_keys, line,
1424
+ publish_seq)
1425
+ _gov[_gk] = key
1426
+ if key is None or key not in snap_vals:
1427
+ continue
1428
+ _uval = snap_vals.get(key)
1429
+ pill_text = _inline_value_text(_uval)
1430
+ if pill_text is None:
1431
+ continue
1432
+ # Reads as `seq_len=384`; a color value carries the swatch hole
1433
+ # right after the `=`.
1434
+ _urgba = _inline_swatch_rgba(_uval)
1435
+ swatch = (_urgba, 1) if _urgba is not None else None
1436
+ pill_text = "=" + (_SWATCH_HOLE if _urgba is not None else "") + pill_text
1437
+ # A publish to the governing key must repaint usage pills even when
1438
+ # its binding marker sits off-viewport (culled, so its own full
1439
+ # watch never registered). Idempotent WeakSet add.
1440
+ watch(fn, key, draw_state)
1441
+ cells = len(pill_text) + 1 # one breathing cell around the value
1442
+ boundary_col = col + len(name) + col_shift
1443
+ sub[(_ml - 1, boundary_col)] = cells
1444
+ pill_y = origin_y + (_ml - 1) * line_px
1445
+ if clip is not None and (pill_y + line_px < clip[1]
1446
+ or pill_y > clip[3]):
1447
+ continue
1448
+ paints.append((_ml - 1, boundary_col, pill_text, len(name), pill_y,
1449
+ name, swatch or ()))
1450
+ # Binding pills: the captured value of an assignment/param target,
1451
+ # inserted right after ITS symbol exactly like a usage label -
1452
+ # `edited=False, new_text='...' = draw_text(...)`. A usage gap already at
1453
+ # the same boundary wins (same captured value).
1454
+ if binding_pills:
1455
+ for line0, bcol, pill_text, sym_len, swatch in binding_pills:
1456
+ if (line0, bcol) in sub:
1457
+ continue
1458
+ sub[(line0, bcol)] = len(pill_text) + 1
1459
+ pill_y = origin_y + line0 * line_px
1460
+ if clip is not None and (pill_y + line_px < clip[1]
1461
+ or pill_y > clip[3]):
1462
+ continue
1463
+ paints.append((line0, bcol, pill_text, sym_len, pill_y, "",
1464
+ swatch or ()))
1465
+ if _ok_key and not _hit:
1466
+ # `_layout`, `occurrences` and the pill list ride along so the ids
1467
+ # in the key stay pinned. Paints are stored line-sorted so a hit
1468
+ # can bisect the laid-out window instead of walking every
1469
+ # occurrence.
1470
+ paints.sort()
1471
+ object.__setattr__(draw_state, "_lv_stamp_memo",
1472
+ (_ok, sub, paints, lmap, occurrences,
1473
+ binding_pills))
1474
+ previous = trails.get(span.start_line)
1475
+ trails[span.start_line] = (frame, sub)
1476
+ if previous is None or previous[1] != sub:
1477
+ # New/changed gaps - relayout next frame (draw_text's _window clears
1478
+ # its cache on the stamp).
1479
+ draw_state._lv_trail_gen = getattr(draw_state, "_lv_trail_gen", 0) + 1
1480
+ draw_state.invalidate()
1481
+ from meltygui.core.windowing.glfw_utils import request_render
1482
+ request_render()
1483
+ # Paint into the gaps the CURRENT layout reserved (stamped back by
1484
+ # draw_text's _window). A gap not laid out yet - first frame after a
1485
+ # value appeared - skips painting; it opens on the very next layout.
1486
+ gap_cells = draw_state.__dict__.get("_lv_trail_cells") or {}
1487
+ base_x = origin_x - col_shift * char_w # buffer cell 0 in px
1488
+ if not gap_cells:
1489
+ paints = ()
1490
+ elif _hit and len(paints) > 256:
1491
+ # Memo hit: `paints` is line-sorted - only the lines draw_text has
1492
+ # gaps open for (the tokenized window) can paint, so bisect to them.
1493
+ _glo = min(k[0] for k in gap_cells)
1494
+ _ghi = max(k[0] for k in gap_cells)
1495
+ paints = paints[bisect.bisect_left(paints, (_glo,)):
1496
+ bisect.bisect_right(paints, (_ghi + 1,))]
1497
+ for line0, boundary_col, pill_text, name_len, pill_y, name, swatch in paints:
1498
+ gap_cell = gap_cells.get((line0, boundary_col))
1499
+ if gap_cell is None:
1500
+ continue
1501
+ gap_x = base_x + gap_cell * char_w
1502
+ # The pill wears the marker tint under its symbol (a tinted
1503
+ # definition of the name, else the enclosing function, else the
1504
+ # file) - one memoized lookup per pill.
1505
+ _paint_value_pill(pill_text, gap_x + 3.0, pill_y,
1506
+ tint=_pill_tint(draw_state, line0, name or None),
1507
+ swatch=swatch or None)
1508
+
1509
+
1510
+ def _symbol_cols(anchor, rel_line, key_path, source_lines, labels, memo=None):
1511
+ """(start_col, end_col) of the symbol a snapshot key boxes, or None to
1512
+ drop the key (a line-keyed anchor on a blank / out-of-range line).
1513
+ Resolved ONCE per source version into the anchor index: this is a
1514
+ regex per key, and running it per key per FRAME was 45 of the 70 ms a
1515
+ 5k-key def (draw_text in the context menu's Code pane) cost per
1516
+ selection frame (09-01)."""
1517
+ _rl, start_col, end_col = anchor
1518
+ if memo is not None:
1519
+ # Anchor index rebuilds (every reparse while typing): the answer
1520
+ # depends only on the line's text + the key, and nearly every line
1521
+ # is unchanged, so the regex runs once per (line text, key).
1522
+ text = (source_lines[rel_line - 1]
1523
+ if 1 <= rel_line <= len(source_lines) else None)
1524
+ _mk = (text, key_path[-1], end_col is None, start_col)
1525
+ _hit = memo.get(_mk, _NO_VALUE)
1526
+ if _hit is not _NO_VALUE:
1527
+ return _hit
1528
+ _res = _symbol_cols(anchor, rel_line, key_path, source_lines, labels)
1529
+ memo[_mk] = _res
1530
+ return _res
1531
+ if end_col is None:
1532
+ # No span (line-only keys) - box the LABELED SYMBOL on the line when
1533
+ # the store's key names one (frame-snapshot params and twin
1534
+ # while-body keys always carry their a's name; attribute keys
1535
+ # box just their FINAL segment - see _label_box_span), falling
1536
+ # back to the whole line's text. Full-line boxes stack into an
1537
+ # unreadable double-washed region when several line-keyed values
1538
+ # land on adjacent lines (a captured signature), and their click
1539
+ # latches swallow the lines.
1540
+ if not (1 <= rel_line <= len(source_lines)):
1541
+ return None
1542
+ text = source_lines[rel_line - 1]
1543
+ if not text.strip():
1544
+ return None
1545
+ span_cols = _label_box_span(labels.get(key_path), text)
1546
+ if span_cols is not None:
1547
+ return span_cols
1548
+ return len(text) - len(text.lstrip()), len(text.rstrip())
1549
+ # The leaf span covers the assignment (or maybe just its target,
1550
+ # depending on the key) - box the target itself so the highlight
1551
+ # (and its click latch) doesn't swallow the line: the first
1552
+ # word-boundary occurrence of the target name on the line (the
1553
+ # assignment target precedes any RHS use of the name).
1554
+ name = key_path[-1].split("#", 1)[0]
1555
+ text = (source_lines[rel_line - 1]
1556
+ if 1 <= rel_line <= len(source_lines) else "")
1557
+ m = re.search(rf"\b{re.escape(name)}\b", text)
1558
+ if m is not None:
1559
+ return m.start(), m.end()
1560
+ return start_col, start_col + max(1, len(name))
1561
+
1562
+
1563
+ def _node_owns_function(node, fn, span, line_offset=0):
1564
+ """True when the def node at `span` IS the definition of `fn` — the name
1565
+ matches and fn's first line (its top decorator, so it may sit a few
1566
+ lines ABOVE the span's def line) lies inside the span's line range. A
1567
+ closure's node fails this for the enclosing function it resolved to."""
1568
+ from meltygui.code.libcst_conversion import parse_def_name
1569
+ try:
1570
+ inner = inspect.unwrap(fn)
1571
+ except Exception:
1572
+ inner = fn
1573
+ if parse_def_name(node) != getattr(inner, "__name__", None):
1574
+ return False
1575
+ code = getattr(inner, "__code__", None)
1576
+ first = getattr(code, "co_firstlineno", None)
1577
+ if first is None:
1578
+ return True
1579
+ decorator_slack = 16
1580
+ start = span.start_line + line_offset
1581
+ end = getattr(span, "end_line", span.start_line) + line_offset
1582
+ return start - decorator_slack <= first <= end
1583
+
1584
+
1585
+ def _scope_function(filename, def_line):
1586
+ """The live function object for a def at an absolute file line — the same
1587
+ resolver capture uses, so the store read here is the store written to."""
1588
+ from meltygui.code.chain_converters import _enclosing_function
1589
+ try:
1590
+ return _enclosing_function(filename, def_line + 1)
1591
+ except Exception:
1592
+ return None
1593
+
1594
+
1595
+ def _label_box_span(label, text):
1596
+ """(start_col, end_col) of the symbol a line-keyed marker should box on
1597
+ `text`, or None (caller falls back to the whole line). A plain
1598
+ identifier boxes its first word-boundary occurrence. A DOTTED label
1599
+ (attribute keys: `draw_state.some_val`) boxes only its FINAL segment,
1600
+ matched right after its dotted prefix — so the base name's own marker
1601
+ (boxing `draw_state`) and the attribute's (boxing `some_val`) never
1602
+ overlap, and each toggles its own value window."""
1603
+ if not isinstance(label, str) or not label:
1604
+ return None
1605
+ if label.isidentifier():
1606
+ m = re.search(rf"\b{re.escape(label)}\b", text)
1607
+ return m.span() if m is not None else None
1608
+ if "." in label:
1609
+ prefix, last = label.rsplit(".", 1)
1610
+ if not last.isidentifier():
1611
+ return None
1612
+ m = re.search(rf"\b{re.escape(prefix)}\s*\.\s*({re.escape(last)})\b",
1613
+ text)
1614
+ return m.span(1) if m is not None else None
1615
+ return None
1616
+
1617
+
1618
+ def _snap_line_to_text(text, rel_line, source_lines, lo, hi):
1619
+ """Buffer line an exit-line wash should sit on: the stamped line if its
1620
+ current content still equals the stamped run-time `text` (stripped), else
1621
+ the NEAREST line in the def's [lo, hi] span with that content — so the
1622
+ return/error washes follow their statement through edits the same way
1623
+ line-keyed markers follow their symbols. No stamped text (old-shape
1624
+ stamp) or no match keeps the stamp unchanged."""
1625
+ want = text.strip() if isinstance(text, str) else ""
1626
+ if not want:
1627
+ return rel_line
1628
+ if (1 <= rel_line <= len(source_lines)
1629
+ and source_lines[rel_line - 1].strip() == want):
1630
+ return rel_line
1631
+ best = None
1632
+ for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
1633
+ if best is not None and abs(ln - rel_line) >= abs(best - rel_line):
1634
+ continue
1635
+ if source_lines[ln - 1].strip() == want:
1636
+ best = ln
1637
+ return best if best is not None else rel_line
1638
+
1639
+
1640
+ _IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
1641
+
1642
+
1643
+ def _label_line_index(source_lines, lo, hi):
1644
+ """{identifier: sorted [1-based lines]} for every identifier appearing in
1645
+ the def's [lo, hi] span — one linear pass, built once per source version
1646
+ (memoized by the caller in the _src-keyed snap memo). Turns the per-key
1647
+ label relocation from an O(def lines) regex scan into a lookup."""
1648
+ idx = {}
1649
+ for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
1650
+ for m in _IDENT_RE.finditer(source_lines[ln - 1]):
1651
+ idx.setdefault(m.group(), []).append(ln)
1652
+ return idx
1653
+
1654
+
1655
+ def _snap_line_to_label(label, rel_line, source_lines, lo, hi, lidx=None):
1656
+ """Buffer line a line-keyed marker should anchor on: the stamped line if
1657
+ it still contains `label` (the common case — one regex on one line), else
1658
+ the NEAREST line in the def's [lo, hi] span that does. Pure string ops on
1659
+ the already-split source — the symbol reference is recovered from the
1660
+ line stamp without any reparse, so line-keyed captures follow their
1661
+ symbols through edits instead of staying pinned where the last run left
1662
+ them. No match anywhere (symbol renamed/removed — the value is stale and
1663
+ the next run prunes it) keeps the stamp unchanged.
1664
+
1665
+ `lidx` (from _label_line_index) replaces the def-wide scan with a
1666
+ candidate lookup on the label's final identifier segment; candidates are
1667
+ tried nearest-first and verified with _label_box_span (dotted labels
1668
+ need their prefix checked)."""
1669
+ if not label:
1670
+ return rel_line
1671
+ if (1 <= rel_line <= len(source_lines)
1672
+ and _label_box_span(label, source_lines[rel_line - 1]) is not None):
1673
+ return rel_line
1674
+ if lidx is not None:
1675
+ cand = lidx.get(label.rsplit(".", 1)[-1].split("[", 1)[0])
1676
+ if not cand:
1677
+ return rel_line
1678
+ i = bisect.bisect_left(cand, rel_line)
1679
+ lo_i, hi_i = i - 1, i
1680
+ while lo_i >= 0 or hi_i < len(cand):
1681
+ _below = cand[lo_i] if lo_i >= 0 else None
1682
+ _above = cand[hi_i] if hi_i < len(cand) else None
1683
+ if _above is None or (_below is not None
1684
+ and rel_line - _below <= _above - rel_line):
1685
+ ln, lo_i = _below, lo_i - 1
1686
+ else:
1687
+ ln, hi_i = _above, hi_i + 1
1688
+ if _label_box_span(label, source_lines[ln - 1]) is not None:
1689
+ return ln
1690
+ return rel_line
1691
+ best = None
1692
+ for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
1693
+ if best is not None and abs(ln - rel_line) >= abs(best - rel_line):
1694
+ continue
1695
+ if _label_box_span(label, source_lines[ln - 1]) is not None:
1696
+ best = ln
1697
+ return best if best is not None else rel_line
1698
+
1699
+
1700
+ def _key_anchor(scope_node, key_path, line_offset):
1701
+ """(buffer-relative line, start col | None, end col | None) the symbol
1702
+ box for a snapshot key: descend the scope's locals by the key path to the
1703
+ leaf's span; a `line:N` tail IS the (absolute) anchor, with no column
1704
+ information (the caller boxes the whole line's text)."""
1705
+ tail = key_path[-1]
1706
+ if tail.startswith("line:"):
1707
+ # A '#name' suffix is the per-param qualifier frame snapshots append
1708
+ # so several params on one def signature line keep distinct keys.
1709
+ try:
1710
+ return int(tail[5:].split("#", 1)[0]) - line_offset, None, None
1711
+ except ValueError:
1712
+ return None
1713
+ node = scope_node.get("locals")
1714
+ if not isinstance(node, dict):
1715
+ node = scope_node
1716
+ for seg in key_path[:-1]:
1717
+ node = node.get(seg) if isinstance(node, dict) else None
1718
+ if node is None:
1719
+ return None
1720
+ spans = getattr(node, "_child_spans", None) or {}
1721
+ sp = spans.get(tail)
1722
+ if sp is None:
1723
+ child = node.get(tail) if isinstance(node, dict) else None
1724
+ sp = getattr(child, "span", None)
1725
+ if sp is None:
1726
+ return None
1727
+ end_col = getattr(sp, "end_col", None)
1728
+ return (sp.start_line,
1729
+ getattr(sp, "start_col", 0) if end_col is not None else None,
1730
+ end_col)
1731
+
1732
+
1733
+ # ── Run-once snapshot view: draw_function + instrumentation + source ──
1734
+
1735
+ _proxies = weakref.WeakKeyDictionary()
1736
+
1737
+
1738
+ def _run_proxy(fn):
1739
+ """A stable callable twin-runner for draw_function: same name/signature as
1740
+ `fn` (so the param UI builds identically), but the button runs the
1741
+ INSTRUMENTED twin. Cached per function object — identity survives hotswap,
1742
+ and run_instrumented re-twins per code version underneath."""
1743
+ proxy = _proxies.get(fn)
1744
+ if proxy is None:
1745
+ def proxy(**kw):
1746
+ from meltygui.code.live_instrument import run_instrumented
1747
+ return run_instrumented(fn, **kw)
1748
+ proxy.__name__ = fn.__name__
1749
+ proxy.__qualname__ = fn.__qualname__
1750
+ proxy.__signature__ = inspect.signature(fn)
1751
+ _proxies[fn] = proxy
1752
+ return proxy
1753
+
1754
+
1755
+
1756
+
1757
+
1758
+
1759
+
1760
+
1761
+ def request_run(lab_ds):
1762
+ """Ctrl+Enter = the Run button, nothing more: find the lab's
1763
+ draw_function runner draw_state and stamp a one-shot run request;
1764
+ draw_function pops it on its next render and calls the same _run() a
1765
+ button click does (same single-flight _run_busy guard, same twin path —
1766
+ the twin already compiles from the pending in-memory source, so running
1767
+ IS the latest code). Ctrl+Enter's DUAL dispatch calls this from both
1768
+ halves (the Ctrl+F rule: the behavior must live in BOTH places): the
1769
+ lab body's blocking on_action while the subtree renders, and draw_main's
1770
+ root ctrl_enter fallback when the lab is blit-cached (per-frame
1771
+ subscriptions lapse under a cached ancestor, so the root re-routes via
1772
+ BVH). At most one half fires per press — the body's blocking sub stops
1773
+ the chain before the root's."""
1774
+ from meltygui.core.windowing.glfw_utils import request_render
1775
+ for d in lab_ds.descendants(max_depth=8):
1776
+ if str(getattr(d, 'name', '')).endswith(" runner"):
1777
+ d.misc["_run_requested"] = True
1778
+ d.invalidate() # cached runner must re-render to consume
1779
+ request_render()
1780
+ return
1781
+
1782
+
1783
+ # use_cache=False: the body must run every frame so its Ctrl+Enter on_action
1784
+ # re-registers - subscriptions are per-frame, and a blit-cached body would
1785
+ # drop the action, letting draw_main's global handler win. The two column
1786
+ # children keep their own tile caches, so the shell itself is all this
1787
+ # dispatch about.
1788
+
1789
+
1790
+
1791
+ def is_volume(value):
1792
+ """True for values the voxel renderer should own: 3-D+ numeric tensors or
1793
+ ndarrays. 2-D and below keep their existing views; name-based checks so
1794
+ torch/numpy never import for scalar traffic."""
1795
+ cls = type(value).__name__
1796
+ try:
1797
+ if cls == "Tensor":
1798
+ return value.dim() >= 3
1799
+ if cls == "ndarray":
1800
+ return value.ndim >= 3 and value.dtype.kind in "fiu"
1801
+ except Exception:
1802
+ pass
1803
+ return False