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,2677 @@
1
+ """Code view functions and supporting definitions."""
2
+ from inspect import Parameter
3
+ from meltygui.code.fileref import Address
4
+ from meltygui.code.libcst_conversion import Comment
5
+ from meltygui.code.libcst_conversion import SymbolUsage
6
+ from meltygui.code.libcst_conversion import UsageRef
7
+ from meltygui.core.styling.fonts import Font
8
+ from meltygui.hdr_color import pack_color
9
+ from meltygui.core.melty import Melty
10
+ from meltygui.model.code_model import UsagePickerModel
11
+ from meltygui.core.rendering.modes import Modes
12
+ from meltygui.core.core_render import render_func
13
+ from meltygui.core.rendering.core_decoration import Core
14
+ from meltygui.core.rendering.render_funcs import RenderFuncs
15
+ from meltygui.state.code_state import SourcePreviewState
16
+ from meltygui.state.new_core_model import Anchor
17
+ from meltygui.state.new_core_model import DrawState
18
+ from meltygui.state.new_core_model import ExpandMode
19
+ from meltygui.state.new_core_model import Pin
20
+ from meltygui.state.new_core_model import TabState
21
+ from meltygui.core.runtime.toggles import Toggles
22
+ from meltygui.view.header_view import draw_footer
23
+ from meltygui.view.header_view import draw_header
24
+ from meltygui_imgui.core import _DrawList
25
+ from pathlib import Path
26
+ import bisect
27
+ import collections
28
+ import inspect
29
+ import meltygui_imgui as imgui
30
+ import sys
31
+ import threading
32
+ import time
33
+ import types
34
+ import weakref
35
+
36
+
37
+ @render_func(use_cache=True, selectable=False)
38
+ def run_button(input_value: any, with_kwargs=None, draw_state=None, clicked=False):
39
+ from meltygui.core.conversion.path_finder import Pending
40
+
41
+ is_render_func = hasattr(input_value, "__render_func__")
42
+ if not is_render_func:
43
+ imgui.text_colored(f"Value of type {type(input_value).__name__} needs @render_func",
44
+ 1.0, 0.5, 0.0)
45
+ return False, None
46
+ if with_kwargs is None:
47
+ with_kwargs = {}
48
+
49
+ if hasattr(input_value, "__header_defaults__"):
50
+ run_in_background = input_value.__header_defaults__.get("background", False)
51
+ else:
52
+ run_in_background = True
53
+
54
+ running = draw_state._running is input_value if run_in_background else False
55
+
56
+ fa_run_arrow = ""
57
+ from meltygui.view.control_view import button
58
+ if clicked or running or button(f"{fa_run_arrow} {input_value.__name__}##{draw_state.unique}",
59
+ height=30, draw=True, value=0.4, saturation=1.5,
60
+ name=f"{input_value.__name__}{draw_state.unique}_run")[0]:
61
+ with_kwargs['changed'] = True
62
+ changed, value = input_value(**with_kwargs)
63
+ if isinstance(value, Pending):
64
+ draw_state._running = input_value
65
+ return False, None
66
+
67
+ draw_state._running = False
68
+ return True, (changed, value)
69
+
70
+ return False, None
71
+
72
+
73
+ @render_func(use_cache=True, show_bg=False, selectable=False, disable_scroll=True,
74
+ shadow=False, indent_size=0, with_footer=None, fill_height=False)
75
+ def draw_with_view_funcs(input_value, view_funcs, route, routed, route_to_kwargs,
76
+ tab_state: TabState, unique, column_widths=None,
77
+ draw=False, draw_state=None, **kwargs):
78
+ """Tab strip + columns half of the old draw_modes.
79
+
80
+ Shows a tab per entry in `view_funcs` and renders the selected ones side by
81
+ side. The shared `routed` payload (built by convert_in_and_out) decides each
82
+ column's input: a view with a `route` entry is fed routed[route[view]] (e.g.
83
+ draw_collection <- the parsed dict); a view with no entry edits the raw text
84
+ (draw_text <- input_value). Every routed value also rides in as a kwarg, so a
85
+ view picks up the extras it wants (draw_text <- jump_to / error / code_tree)
86
+ WITHOUT convert_in_and_out hand-threading them — that's the routing.
87
+
88
+ Two edit channels flow back to convert_in_and_out:
89
+ * a RAW text edit is returned directly as (changed, value);
90
+ * a CONVERTED edit (a routed/structured view) is stashed on the shared
91
+ `routed` dict under 'converted_edit', because it must go back through
92
+ chain_out before it becomes text. It's written AFTER the loop so the
93
+ columns in this same frame never see it as an input kwarg."""
94
+ from meltygui.code.new_converters import UNSET
95
+
96
+ # Drop entries that didn't survive (de)serialization, then default to the
97
+ # first two views (text | structured) like the old draw_modes did.
98
+ tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t is not None]
99
+ if not tab_state.selected_tabs:
100
+ tab_state.selected_tabs = view_funcs[:2] or view_funcs[:1]
101
+
102
+ imgui.dummy(0, 5)
103
+ names = [getattr(vf, '__name__', str(vf)) for vf in view_funcs]
104
+ tab_changed, new_tabs = RenderFuncs.draw_tab_bar(input_value=tab_state.selected_tabs,
105
+ tab_height=30, show_bg=False, bg_offset=1,
106
+ name=f"tab_bar{unique}", names=names,
107
+ collection=view_funcs, as_toggles=False)
108
+ if tab_changed:
109
+ tab_state.selected_tabs = new_tabs
110
+
111
+ imgui.dummy(0, 2)
112
+ raw_changed, raw_value = False, input_value
113
+ converted_edit = UNSET
114
+ for idx, view_func in enumerate(tab_state.selected_tabs):
115
+ if column_widths is not None and len(column_widths) > idx:
116
+ column_width = column_widths[idx]
117
+ else:
118
+ column_width = None
119
+
120
+ # A view with a route entry consumes a routed (converted) value; one
121
+ # without edits the raw text. Falls back to the last-good routed value
122
+ # (already merged into `routed`), or UNSET if it never parsed.
123
+ uses_converted = route is not None and view_func in route
124
+ if uses_converted:
125
+ view_input = routed.get(route[view_func], UNSET)
126
+ else:
127
+ view_input = input_value
128
+
129
+ # route_to_kwargs_this = route_to_kwargs.get(view_func, {})
130
+ # for k in route_to_kwargs_this:
131
+ # arg_name = route_to_kwargs_this[k]
132
+ # if arg_name in routed:
133
+ # routed[k] = routed[arg_name]
134
+
135
+ # routed = {**routed, **{k: routed.get(k) for k in route_to_kwargs_this}}
136
+
137
+ # `draw=` (the external trigger flag) bypasses the view's cache for one
138
+ # redraw WITHOUT setting any sticky edit flags - never pass it as
139
+ # `changed=`, or a forced redraw comes back reported as an edit and, with
140
+ # auto_save, spins an endless reload -> redraw -> save.
141
+ if len(tab_state.selected_tabs) == 1:
142
+ column = None
143
+ m_changed, m_out = view_func(input_value=view_input, excluded=["__cst__", "__origin__"],
144
+ show_system=False, draw=draw, max_width=draw_state.content_width - 10,
145
+ disable_scroll=False, show_header=False,
146
+ column=idx, column_width=column_width,
147
+ show_add_delete=False, name=f"{view_func.__name__}##{unique}",
148
+ selectable=False, **routed)
149
+ if not m_changed:
150
+ continue
151
+ if draw_state is not None:
152
+ draw_state.invalidate_up(max_depth=3)
153
+ if uses_converted:
154
+ # Keep only the latest converted edit so a continuous drag collapses
155
+ # into one chain_out call when it settles.
156
+ converted_edit = m_out
157
+ else:
158
+ raw_changed, raw_value = True, m_out
159
+
160
+ if converted_edit is not UNSET:
161
+ routed['converted_edit'] = converted_edit
162
+ return raw_changed, raw_value
163
+
164
+
165
+ def draw_text_from_code_cache(input_value=None, root_input=None, error=None,
166
+ run_jedi=False, **kwargs):
167
+ """FILE_TREE's editor view: plain draw_text fed from the shared code-host
168
+ cache instead of an inline chain. code_hosts_for(path) owns the parse —
169
+ str_host watches the file, dict_host re-parses on change — and we pull its
170
+ held cst_dict each frame, so usage links and syntax-error highlighting
171
+ arrive as code_dict / code_tree exactly as in the NEW_CODE routes. Pull-
172
+ based by design: an edit here auto-saves to disk, the cache's str_host
173
+ reloads from the file, and the editor picks up the fresh parse a beat
174
+ later — the leaf and the cache only ever talk through the file."""
175
+ from meltygui.code.new_converters import ModesState
176
+ from meltygui.code.new_converters import _ensure_symbol_index
177
+ from meltygui.code.new_converters import _error_markers
178
+ from meltygui.code.new_converters import _host_label
179
+ from meltygui.code.new_converters import _host_relint_and_fixes
180
+ from meltygui.code.new_converters import code_hosts_for
181
+ from meltygui.core.diagnostics.perf_trace import once as _ponce
182
+ from meltygui.core.diagnostics.perf_trace import trace as _ptrace
183
+ from meltygui.core.diagnostics.perf_trace import trace_rl as _ptrace_rl
184
+ from meltygui.core.windowing.glfw_utils import request_render
185
+
186
+ code_dict, cache_error, dict_host = None, None, None
187
+ # Scope-up auto-select (contextual func tab): a file-ABSOLUTE line whose
188
+ # statement should be selected in this editor - consumed here (popped so it
189
+ # never leaks into draw_text as a stray kwarg) and applied one-shot below.
190
+ # select_seq is the arrow-press generation: it keys the one-shot, so every
191
+ # click re-selects even when the same line repeats in a persisted ds.
192
+ select_line = kwargs.pop("select_line", None)
193
+ select_seq = kwargs.pop("select_seq", 0)
194
+ _t_editor0 = time.monotonic()
195
+ if root_input is not None:
196
+ _str_host, dict_host = code_hosts_for(root_input)
197
+ code_dict = dict_host._held()
198
+ # First-parse arrival transition (once per host): the gap between
199
+ # "waiting" and "visible" is what the user sees as load time.
200
+ if isinstance(code_dict, dict):
201
+ if _ponce(("parse-visible", _host_label(dict_host))):
202
+ _ptrace("editor: first parse visible", host=_host_label(dict_host))
203
+ elif _ponce(("parse-wait", _host_label(dict_host))):
204
+ _ptrace("editor: waiting for first parse", host=_host_label(dict_host))
205
+ # Background auto-index: keep the held parse's symbol usages current
206
+ # without the manual Index click (no-op when already indexed).
207
+
208
+ _ensure_symbol_index(dict_host, _str_host, code_dict, kwargs.get("jump_to"))
209
+ # The chain's parse error lives on the wrapper's injected ModesState.
210
+ # Normalize it to the ParseError-dict shape _code_tree_errors reads
211
+ # ({'__error__','__line__'}) and pass it as code_tree: the raw exception
212
+ # could be a cst.ParserSyntaxError, whose line lives in `raw_line` - a
213
+ # name neither _code_tree_errors nor _exception_handler knows. code_dict
214
+ # keeps the last GOOD parse alongside, so the editor highlights the
215
+ # offending line without losing its structure.
216
+ wds = getattr(dict_host, "_wrapper_draw_state", None)
217
+ import_fixes = _host_relint_and_fixes(dict_host, _str_host, wds)
218
+ for v in (getattr(wds, "misc", None) or {}).values():
219
+ if isinstance(v, ModesState):
220
+ err = v.last_error
221
+ lint = getattr(v, "last_lint", None) or None
222
+ if err is not None or lint:
223
+ # ONE dict per underlying (exception, lint) pair, not one
224
+ # per frame: draw_text's parse-error staleness check
225
+ # compares code_tree by IDENTITY to detect "a fresh parse
226
+ # landed", so a dict rebuilt every frame would re-hide a
227
+ # stale highlight one frame after an edit, pinned to the
228
+ # old line. The memo keeps identity stable until the
229
+ # background reparse actually replaces last_error /
230
+ # last_lint (both swapped per completed parse, never
231
+ # mutated in place).
232
+ memo = getattr(dict_host, "_err_view_memo", None)
233
+ if memo is not None and memo[0] is err and memo[1] is lint:
234
+ cache_error = memo[2]
235
+ else:
236
+ # The parse/compile error, then the lint findings -
237
+ # all in __errors__, with the first mirrored into the
238
+ # single __error__/__line__ keys the editors use.
239
+ markers = _error_markers(err, lint)
240
+ cache_error = {"__error__": markers[0][1], "__line__": markers[0][0],
241
+ "__errors__": markers}
242
+ dict_host._err_view_memo = (err, lint, cache_error)
243
+ # The Index button's pulse rides to the cache's chain_in (one-shot:
244
+ # cleared again on the next un-pulsed frame). cst_module_to_dict only
245
+ # runs jedi when it ALSO has the resolved address (jump_to) - the leaf's
246
+ # code_file_io hands us the whole-file Address, so forward it alongside.
247
+ # _pending_external makes the host re-run chain_in (its start gate is
248
+ # external_change); invalidating alone would just replay the blit.
249
+ if run_jedi:
250
+ dict_host.child_kwargs["run_jedi"] = True
251
+ if kwargs.get("jump_to") is not None:
252
+ dict_host.child_kwargs["jump_to"] = kwargs["jump_to"]
253
+ dict_host._pending_external = True
254
+ for hds in (wds, getattr(dict_host, "_draw_state", None)):
255
+ if hds is not None:
256
+ hds.invalidate()
257
+ request_render()
258
+ elif (dict_host.child_kwargs.get("run_jedi")
259
+ and not dict_host._pending_external):
260
+ # One-shot clear - but only after the host actually consumed the
261
+ # pulse (_pending_external drops when its chain_in is handed the
262
+ # kwargs). Popping earlier loses a click whenever this editor
263
+ # re-renders between the pulse frame and the host's next draw.
264
+ dict_host.child_kwargs.pop("run_jedi", None)
265
+
266
+ held_values = list(_str_host.values())
267
+ from_host = len(held_values) > 0
268
+ # Short-circuit the first-parse wait: the code_dict pair materializes
269
+ # through the full cst→dict chain, and until it lands this editor
270
+ # rendered NOTHING - that gap IS the perceived load time of a code
271
+ # buffer ("editor: waiting for first parse" above). The codec-loaded
272
+ # buffer (input_value) is available the frame load_file lands, so draw
273
+ # it immediately: the colors come from the tokenizer and def-hints
274
+ # from the text mode roster, both text-based; code_dict extras
275
+ # (usage links) join when the parse arrives and dict_host's
276
+ # notify_on_change repaints this editor. Edits during this brief window
277
+ # are discarded - the host isn't there to receive them, the same
278
+ # contract as a read-only code-diff tab.
279
+ buffer_text = (held_values[0] if from_host
280
+ else input_value if isinstance(input_value, str) else None)
281
+ if buffer_text is not None:
282
+ _t_dt0 = time.monotonic()
283
+ changed, value, ds = RenderFuncs.draw_text(buffer_text, code_dict=code_dict,
284
+ code_tree=cache_error, error=error,
285
+ import_fixes=import_fixes,
286
+ return_extras=True,
287
+ **{"gutter_indent": True, **kwargs, "is_tree": False})
288
+ _t_dt1 = time.monotonic()
289
+ # Every frame's editor draws: mark as a LIVE user so the idle sweep
290
+ # keeps the host registered (and repaint it when a background parse
291
+ # lands). Not gated on `changed` - an open-but-unedited editor still
292
+ # owns the host, and the sweep would otherwise immediately-register it.
293
+ dict_host.notify_on_change(ds)
294
+ # Apply a pending start-up auto-select: map the file-absolute line
295
+ # into this span buffer via jump_to.start (the 0-based file line of
296
+ # buffer line #0) and select that line's code - same shape as the
297
+ # mouse-click line-select and draw_code_editor's jump_to_line
298
+ # consumption (caret placement + focus grant; the editor's own
299
+ # cursor-follow scroll brings it into view on the next body frame).
300
+ # One-shot per (arrow press, line) - stamped on ds.misc so re-renders
301
+ # must not keep clamping the selection while the user edits the
302
+ # editor, but a new press (select_seq bump) re-applies it.
303
+ if (select_line is not None and ds is not None
304
+ and isinstance(buffer_text, str)
305
+ and ds.misc.get("_applied_select_line") != (select_seq, select_line)):
306
+ ds.misc["_applied_select_line"] = (select_seq, select_line)
307
+ _start0 = getattr(kwargs.get("jump_to"), "start", 0) or 0
308
+ _lines = buffer_text.split("\n")
309
+ _li = max(0, min(int(select_line) - 1 - _start0, len(_lines) - 1))
310
+ _line_start = sum(len(l) + 1 for l in _lines[:_li])
311
+ _indent = len(_lines[_li]) - len(_lines[_li].lstrip())
312
+ _sel_len = len(_lines[_li]) - _indent
313
+ # Fold projection: these are FULL-buffer coords; the editor
314
+ # lays out fold-spliced display text. Expands any collapsed
315
+ # fold at the line, then shifts the selection start - the
316
+ # end rides the same line, so it shifts by the same delta.
317
+ from meltygui.editor.text_editor import fold_project_jump
318
+ _sel_s, _ = fold_project_jump(
319
+ ds, buffer_text, _line_start + _indent, _li)
320
+ ds.text_selection_start = _sel_s
321
+ ds.text_selection_end = _sel_s + _sel_len
322
+ ds.text_cursor_pos = ds.text_selection_end
323
+ Melty.text_focused_ds = ds
324
+ Melty._text_focus_grant_frame = Melty.frame_count
325
+ ds.invalidate()
326
+ request_render()
327
+ if changed and from_host:
328
+ _key0 = list(_str_host.keys())[0]
329
+ _held0 = _str_host[_key0]
330
+ if _held0 == value:
331
+ # Echo breaker: a changed=True with byte-identical text must
332
+ # not dirty the str host - that write is what feeds the
333
+ # startup parse storm (dirty → chain_out → queue_save →
334
+ # pending_gen bump → full reparse + usage recompute, per
335
+ # frame, for a no-op). Python == short-circuits by length
336
+ # and first differing char, so real edits pay ~nothing;
337
+ # the full-length compare only matters in the spurious
338
+ # case, where it replaces a multi-frame pipeline.
339
+ _ptrace("editor changed with IDENTICAL text — host write suppressed",
340
+ host=_host_label(dict_host))
341
+ else:
342
+ _t_w0 = time.monotonic()
343
+ _str_host[_key0] = value
344
+ _t_w1 = time.monotonic()
345
+ # TEMP perf: split the editor frame's tail - the draw_text
346
+ # CALL (body + render_func wrapper epilogue; body time is
347
+ # the top "draw_text took" line) vs the host WRITE
348
+ # (install_bubbling + notify_on_changed + invalidations).
349
+ if (_t_w1 - _t_dt0) * 1000.0 >= 30.0:
350
+ _ptrace("editor tail split",
351
+ call_ms=round((_t_dt1 - _t_dt0) * 1000.0, 1),
352
+ write_ms=round((_t_w1 - _t_w0) * 1000.0, 1))
353
+ # # Re-render this editor when a background parse lands: its cached
354
+ # # subtree is outside the host's own draw loop, so without registering it
355
+ # # the fresh cst_dict sits invisible until an unrelated invalidation.
356
+ # if dict_host is not None and ds is not None:
357
+ # dict_host.notify_on_change(ds)
358
+ _dt_editor = (time.monotonic() - _t_editor0) * 1000.0
359
+ if _dt_editor >= 20.0 and dict_host is not None:
360
+ # Render-thread stall inside the editor's loop (draw_text + pulls).
361
+ _ptrace_rl(("editor-slow", id(dict_host)),
362
+ f"editor frame took {_dt_editor:.0f}ms",
363
+ host=_host_label(dict_host))
364
+ return False, None
365
+
366
+
367
+ @render_func(use_cache=True, show_bg=False, selectable=False, disable_scroll=True,
368
+ shadow=False, indent_size=0, with_footer=None, bg_offset=0)
369
+ def draw_code_tabs_from_cache(input_value=None, root_input=None, tab_state: TabState = None,
370
+ unique=None, draw_state: DrawState = None, column_widths=None,
371
+ column_edges=None, draw=False, error=None,
372
+ run_jedi=False, **kwargs):
373
+ """NEW_CODE's text|structured tabs on the code-host-cache route — no inline
374
+ chain_in/chain_out (the legacy convert_in_and_out path this replaces).
375
+
376
+ • draw_text — delegates to draw_text_from_code_cache; a text edit returns
377
+ to the enclosing code_file_io, which saves the span (its normal path).
378
+ • draw_collection — renders the shared dict_host's HELD GeneralParse
379
+ (code_hosts_for). An edit mutates the bubbling-wrapped parse in place,
380
+ marking the host dirty; the host chain_outs to source and saves through
381
+ the cache's own str_host, so nothing returns upward from this column.
382
+ An edit ALSO drives the live source immediately (live_apply_edits:
383
+ class vars, function param defaults + constant locals, module
384
+ globals) — the same responsive preview the chain route's
385
+ general_parse_to_address gives, ahead of any recompile.
386
+
387
+ The two code_file_io instances (this window's and the cache's str_host)
388
+ only ever talk through the FILE: a dict edit saves via the cache and this
389
+ window's auto_load_edits picks it up; a text edit saves here and the
390
+ cache's file watch re-parses."""
391
+ from meltygui.code.chain_converters import live_apply_edits
392
+ from meltygui.code.new_converters import _ensure_symbol_index
393
+ from meltygui.code.new_converters import _host_code_tree_error
394
+ from meltygui.code.new_converters import _host_label
395
+ from meltygui.code.new_converters import _host_relint_and_fixes
396
+ from meltygui.code.new_converters import code_hosts_for
397
+ from meltygui.core.diagnostics.notifications import notify
398
+ from meltygui.core.diagnostics.perf_trace import once as _ponce
399
+ from meltygui.core.diagnostics.perf_trace import trace as _ptrace
400
+
401
+ _t_tabs0 = time.monotonic()
402
+ view_funcs = [RenderFuncs.draw_collection_as_tabs, RenderFuncs.draw_text]
403
+ # Drop entries that didn't survive (de)serialization, then default to two
404
+ # tabs (structured | text), matching draw_with_view_funcs.
405
+ tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t is not None]
406
+ if not tab_state.selected_tabs:
407
+ tab_state.selected_tabs = view_funcs[:2]
408
+
409
+ imgui.dummy(0, 5)
410
+ names = [getattr(vf, '__name__', str(vf)) for vf in view_funcs]
411
+ tab_changed, new_tabs = RenderFuncs.draw_tab_bar(input_value=tab_state.selected_tabs,
412
+ tab_height=30, show_bg=False, bg_offset=0,
413
+ name=f"tab_bar{unique}", names=names,
414
+ collection=view_funcs, as_toggles=False)
415
+ if tab_changed:
416
+ tab_state.selected_tabs = new_tabs
417
+
418
+ imgui.dummy(0, 2)
419
+ dict_host = None
420
+ if root_input is not None:
421
+ _str_host, dict_host = code_hosts_for(root_input)
422
+ # Repaint this subtree when the background parse lands - it reads the
423
+ # host's value from outside the host's own draw loop (same registration
424
+ # draw_text_from_code_cache makes for its error/code_dict pull).
425
+ dict_host.notify_on_change(draw_state)
426
+ # Auto-index here too (idempotent with the draw_text delegate's call):
427
+ # a structured-only window never runs draw_text_from_code_cache, and
428
+ # its usage links should stay live all the same.
429
+ _ensure_symbol_index(dict_host, _str_host, dict_host._held(),
430
+ kwargs.get("jump_to"))
431
+
432
+ # New columnLayout (shared edge system): the panes line up with other
433
+ # objects drawn on the root window - the divider between the
434
+ # structured and text panes is a draggable line in the same collision
435
+ # region as every other window edge. Lazy import (new_core_view sits
436
+ # between this module and columns.py). left_edge/right_edge: when this
437
+ # view renders inside another row's cell, the host passes the cell's edge
438
+ # dicts (through code_file_io's child_kwargs) and they become this row's
439
+ # far edges by reference - same adoption draw_columns gives nested
440
+ # Columns. Absent (the usual standalone window), ColumnLayout falls back
441
+ # to the window frame edges.
442
+ from meltygui.core.layout.column_core import ColumnLayout
443
+ from meltygui.core.layout.column_core import MIN_ROW_HEIGHT
444
+
445
+ cols = ColumnLayout(draw_state, len(tab_state.selected_tabs),
446
+ column_edges=column_edges, column_widths=column_widths,
447
+ left_edge=kwargs.get("left_edge"),
448
+ right_edge=kwargs.get("right_edge"))
449
+ # Pin each pane to the visible viewport (the legacy column_max_height
450
+ # path this replaces) - long sources scroll inside their pane.
451
+ avail_h = None
452
+ size_kwargs = {}
453
+ clip = cols.clip if cols.clip is not None else draw_state.abs_clip_rect
454
+ if clip is not None:
455
+ # Exactly match the frame band: the pane then ends one pixel
456
+ # above the band's bottom edge, so the black reads even all around.
457
+ avail_h = max(MIN_ROW_HEIGHT, clip[3] - cols.top)
458
+ # The pane content is inset by the fixed padding on every side.
459
+ size_kwargs = {"height": avail_h - 2 * cols.padding}
460
+
461
+ # Grab the parse + its normalized error off the MANAGED dict_host once, before
462
+ # the tab loop. Both tabs read them: the structured tab renders `gp` directly,
463
+ # the text tab injects code_dict/code_tree/error into its draw_text leaf via
464
+ # child_kwargs. Computing here also drops the old order dependency (the text tab
465
+ # read `gp` before the structured branch defined it).
466
+ gp = dict_host._held() if dict_host is not None else None
467
+ if isinstance(gp, dict):
468
+ if _ponce(("parse-visible", _host_label(dict_host))):
469
+ _ptrace("tabs: first parse visible", host=_host_label(dict_host))
470
+ elif dict_host is not None and _ponce(("parse-wait", _host_label(dict_host))):
471
+ _ptrace("tabs: waiting for first parse", host=_host_label(dict_host))
472
+ # Relint machinery + the import-suggestions pull run here too - this
473
+ # route's text pane wires draw_text directly (below), NOT through
474
+ # draw_text_from_code_cache, so without this the Alt-Enter quick-fix
475
+ # data never reached it (the live_view_forward NEW_CODE path).
476
+ import_fixes = _host_relint_and_fixes(
477
+ dict_host, _str_host, getattr(dict_host, "_wrapper_draw_state", None))
478
+ cache_error = _host_code_tree_error(dict_host)
479
+
480
+ raw_changed, raw_value = False, input_value
481
+ _pane_ms = {}
482
+ for idx, view_func in enumerate(tab_state.selected_tabs):
483
+ _t_pane0 = time.monotonic()
484
+ with cols.cell(idx, height=avail_h) as col_width:
485
+ if getattr(view_func, "__name__", "") == "draw_text":
486
+ # The host's parse + errors flow to the draw_text leaf through
487
+ # draw_collection's child_kwargs: code_dict → token views / symbol
488
+ # usages, code_tree → the syntax/lint error highlight, error → the
489
+ # recompile/runtime highlight (the same trio draw_text_from_code_cache
490
+ # hands draw_text, now via the parent _str_host).
491
+ m_changed, m_out = RenderFuncs.draw_collection(
492
+ input_value=_str_host,
493
+ child_kwargs={"error": error, "view_func": RenderFuncs.draw_text, "is_tree": False,
494
+ "code_dict": gp, "code_tree": cache_error, "child_kwargs": {"is_tree": False},
495
+ "import_fixes": import_fixes,
496
+ "run_jedi": run_jedi, "jump_to": kwargs.get("jump_to")},
497
+ show_header=False, show_name=False,
498
+ width=col_width, **size_kwargs,
499
+ name=f"draw_text##{unique}")
500
+ if m_changed:
501
+ notify("text changed", tag="save bug", tint=(1, 1, 0.5))
502
+ raw_changed, raw_value = True, m_out
503
+ # draw_state.invalidate_up(max_depth=2)
504
+ else:
505
+ if not isinstance(gp, dict):
506
+ # Placeholder frame: keep every ancestor's persisted
507
+ # content_height (see Melty.pending_placeholder_frame) -
508
+ # this one-line stand-in doesn't become the measure.
509
+ Melty.pending_placeholder_frame = Melty.frame_count
510
+ imgui.text_colored("Parsing…" if dict_host is not None
511
+ else "No parse for this source", 0.6, 0.6, 0.6, 1.0)
512
+ continue
513
+ # No draw= forwarding: code_file_io passes draw=True the frame
514
+ # after EVERY keystroke (its reconvert trigger), and the one-shot
515
+ # cache bypass rebuilt this whole structured pane per key
516
+ # (~13-23ms for Toggles' ~110 rows). The pane's content (gp)
517
+ # only changes when a chain_in parse lands, and that landing
518
+ # already force-invalidates this subtree (dict_host's
519
+ # _notify_callers) - so the pane refreshes once per debounced
520
+ # parse instead of per keystroke.
521
+ # DEBUG (dict-pane keystroke cost): snapshot the pane's tile
522
+ # state BEFORE the call - was it dirty (someone invalidated
523
+ # it) or clean (cache gate re-ran the pane anyway)?
524
+ _dbg_key = getattr(draw_state, "_dbg_dict_key", None)
525
+ _dbg_t = (Melty.cache._tiles.get(_dbg_key)
526
+ if _dbg_key and Melty.cache is not None else None)
527
+ _pane_ms["dict_pre"] = (
528
+ f"dirty={_dbg_t.dirty}/inv=f{_dbg_t.last_invalidated_frame}"
529
+ f"/clean=f{_dbg_t.last_clean_frame}/now=f{Melty.frame_count}"
530
+ if _dbg_t is not None else "tile=?")
531
+ m_changed, m_out = RenderFuncs.draw_collection(
532
+ gp, excluded=["__cst__", "__origin__"],
533
+ child_kwargs={"show_bg": False, "shadow": False, "folder_type":(dict), "use_cache": True, "z_offset": 0, "view_func":RenderFuncs.draw_collection_as_tabs},
534
+ show_system=False,
535
+ disable_scroll=False, show_header=False, show_add_delete=False,
536
+ width=col_width, **size_kwargs, show_parent_add_delete=False,
537
+ name=f"draw_collection##{unique}", selectable=False)
538
+ if _dbg_key is None and Melty.cache is not None:
539
+ # One-shot: resolve the pane's tile key by name and arm the
540
+ # tile's invalidation stack-print on its draw_state.
541
+ _nm = f"draw_collection##{unique}"
542
+ for _k, _ds2 in Melty.cache.key_to_draw_state.items():
543
+ if _ds2 is not None and getattr(_ds2, "name", None) == _nm:
544
+ draw_state._dbg_dict_key = _k
545
+ # Arm the tile-bump tracer (see draw_offscreen's
546
+ # _bump_note): every code path that dirties this
547
+ # pane's tile names itself in the perf log.
548
+ _ds2._bump_trace_armed = True
549
+ break
550
+ if m_changed:
551
+ notify("dict changed", tag="save bug", tint=(1, 1, 0.5))
552
+ # A rebuilt top-level dict (reorder / add / delete) replaces the
553
+ # host value; an in-place value edit already bubbled the host
554
+ # dirty. Either way the host chain_outs + updates on its own draw.
555
+ if m_out is not gp and isinstance(m_out, dict):
556
+ dict_host[dict_host.value_key] = m_out
557
+ gp = m_out
558
+ live_apply_edits(root_input, gp)
559
+ draw_state.invalidate_up(max_depth=2)
560
+ _pane_ms[getattr(view_func, "__name__", str(idx))] = \
561
+ (time.monotonic() - _t_pane0) * 1000.0
562
+
563
+ cols.finish()
564
+ _dt_tabs = (time.monotonic() - _t_tabs0) * 1000.0
565
+ if dict_host is not None and (
566
+ _dt_tabs >= 10.0
567
+ or (isinstance(_pane_ms.get("draw_collection"), float)
568
+ and _pane_ms["draw_collection"] >= 3.0)):
569
+ # Render time time inside the tabs subtree this frame - per-pane
570
+ # split so a slow frame shows the pane (text editor vs structured
571
+ # collection) instead of one opaque total. UNratelimited while the
572
+ # dict pane takes time, so a typing burst shows every occurrence
573
+ # (bounded by keystroke rate; dict_pre shows the pane tile's dirty
574
+ # state going in).
575
+ _panes = " ".join(f"{k}={v:.0f}ms" if isinstance(v, float) else f"{k}={v}"
576
+ for k, v in _pane_ms.items())
577
+ _ptrace(f"tabs frame took {_dt_tabs:.0f}ms [{_panes}]",
578
+ host=_host_label(dict_host))
579
+ return False, None
580
+
581
+
582
+ def draw_jump_to(input_value: Address, unique, width=30, error_msg=None,
583
+ draw_state=None):
584
+ file_name = input_value.path.name if input_value.path is not None else "Unknown file"
585
+ line_number = input_value.start + 1 if input_value.start is not None else None
586
+ # Unicode escape (not a literal string) for the Font Awesome folder icon - a
587
+ # pasted PUA char gets stripped to empty on save, which is why it vanished.
588
+ folder_icon = "" # FA folder
589
+
590
+ # Label with the enclosing function name + line number. Prefer the function
591
+ # already attached to the address (.source); otherwise retrieve it from the
592
+ # line via the cached _enclosing_function helper.
593
+ fn = input_value.source if isinstance(input_value.source, types.FunctionType) else None
594
+ if fn is None and line_number is not None and input_value.path is not None:
595
+ from meltygui.code.chain_converters import _enclosing_function
596
+ fn = _enclosing_function(str(input_value.path), line_number)
597
+
598
+ label = f"{file_name}:{line_number}" if line_number is not None else file_name
599
+ if fn is not None:
600
+ label = f"{fn.__name__} ({label})"
601
+
602
+ # File-header bar: a rounded filled rect spanning the content width, drawn
603
+ # behind the label + jump button. Packed ABGR colors per the codebase idiom.
604
+ # When there's an error, the bar grows a second row to hold the message, and
605
+ # both the fill and the outline tint red so the header reads as "this file has
606
+ # a problem".
607
+ draw_list = imgui.get_window_draw_list()
608
+ x0, y0 = imgui.get_cursor_screen_pos()
609
+ pad_x, pad_y = 8, 3
610
+ row_h = imgui.get_frame_height() + pad_y * 2
611
+ msg = str(error_msg).split('\n', 1)[0] if error_msg else None
612
+ msg_row_h = (imgui.get_text_line_height() + 4) if msg else 0
613
+ x1, y1 = x0 + width, y0 + row_h + msg_row_h
614
+ # Round only the top two corners so the bar reads as a box sitting flush
615
+ # on top of the body below it.
616
+ rounding = 4.0
617
+ top_corners = imgui.DRAW_ROUND_CORNERS_TOP
618
+ if msg:
619
+ fill_col = pack_color(70 / 255, 30 / 255, 40 / 255, 235 / 255) # dark red-tinted fill
620
+ line_col = pack_color(150 / 255, 60 / 255, 70 / 255, 1.0) # red outline
621
+ else:
622
+ fill_col = pack_color(44 / 255, 52 / 255, 62 / 255, 230 / 255)
623
+ line_col = pack_color(66 / 255, 78 / 255, 90 / 255, 1.0)
624
+ draw_list.add_rect_filled(x0, y0, x1, y1, fill_col, rounding, top_corners)
625
+ draw_list.add_rect(x0, y0, x1, y1, line_col, rounding, top_corners)
626
+
627
+ # Row 1: label (vertically centered against the button frame) + icon jump
628
+ # button. flat_button (draw-list + on_action through the EDITOR's
629
+ # draw_state - this bar is drawn inside draw_text's body), not a
630
+ # @render_func button: the old widget re-rendered its full wrapper every
631
+ # editor frame. The measured rect is stashed in the editor's state so its
632
+ # selection pass can null the PRESS inside it (the old button's own
633
+ # draw_state used to claim that press; without the null, clicking Open
634
+ # would also place the caret in the document under the floating bar).
635
+ from meltygui.view.header_view import flat_button
636
+ from meltygui.core.melty import Melty
637
+ imgui.set_cursor_screen_pos((x0 + pad_x, y0 + pad_y))
638
+ _open_label = f"{folder_icon} Open"
639
+ _bw = imgui.calc_text_size(_open_label).x + Melty.px(15)
640
+ _bh = Melty.px(18.0)
641
+ _bx, _by = imgui.get_cursor_screen_pos()
642
+ if draw_state is not None:
643
+ draw_state._jump_btn_rect = (_bx, _by, _bx + _bw, _by + _bh)
644
+ if flat_button(f"{_open_label}##jump_to{unique}", draw_state,
645
+ view_id=f"jump_open{unique}", width=_bw, height=_bh):
646
+ from meltygui.core.runtime.extensions import open_source as open_in_editor
647
+ open_in_editor(str(input_value.path), line_number=line_number,
648
+ token=fn.__name__ if fn is not None else None)
649
+
650
+ imgui.same_line()
651
+ imgui.align_text_to_frame_padding()
652
+ imgui.text(label)
653
+
654
+ # Row 2: the full error message, in red, spanning the bar. Truncated to the
655
+ # bar width so a long message can't overflow.
656
+ if msg:
657
+ avail = max(0, width - 2 * pad_x)
658
+ if imgui.calc_text_size(msg).x > avail:
659
+ ch_w = max(1.0, imgui.calc_text_size("x").x)
660
+ keep = max(3, int(avail / ch_w) - 1)
661
+ msg = msg[:keep] + "…"
662
+ imgui.set_cursor_screen_pos((x0 + pad_x, y0 + row_h - 2))
663
+ imgui.text_colored(msg, 1.0, 0.5, 0.46, 1.0)
664
+
665
+ # Reserve the bar's full height so following content doesn't overlap it.
666
+ imgui.set_cursor_screen_pos((x0, y1 + 2))
667
+
668
+
669
+ def draw_code_line_fast(draw_list, x, y, text, char_w, line_h, max_width=None,
670
+ block_tint=None, spans=(), line_open=None, alpha=1.0,
671
+ block_rounding=4.0, emphasis=None, dim_alpha=None):
672
+ """Paint `text` (one line, no newline) at (x, y) with the editor's syntax
673
+ colours. `char_w` / `line_h` come from `push_code_font` (the font must
674
+ be pushed). `max_width` truncates by whole cells. `block_tint` (rgb)
675
+ paints the definition block wash under the line — the enclosing tinted
676
+ class/def — and `spans` = [(col_start, col_end, rgb, scale)] the
677
+ occurrence washes, LINE-relative columns (see `CodeLineTints`).
678
+ `line_open` is the lexer state at the line's start (None = code,
679
+ 'comment', or the (quote, kind) pair of a string the line begins
680
+ inside). `alpha` fades everything (a context row). `emphasis` =
681
+ [(col_start, col_end)] keeps those glyphs at full brightness and fades
682
+ every other glyph by `dim_alpha` (default the diff-collapse preview
683
+ fade, `Toggles.TextEditor.diff_preview_alpha`) — an EMPTY list dims the
684
+ whole line, None dims nothing. Washes never dim. Returns the painted
685
+ width in pixels."""
686
+ from meltygui.editor.code_line_fast import _split_emphasis
687
+ from meltygui.editor.code_line_fast import _u32
688
+ from meltygui.editor.code_line_fast import _wash_factors
689
+
690
+ from meltygui.editor.text_editor import COLORS
691
+ from meltygui.editor.text_editor import _tokenize_from
692
+ from meltygui.editor.text_editor import _bg_adjust
693
+ from meltygui.editor.text_editor import _mix_packed
694
+ from meltygui.editor.text_editor import _fade_packed
695
+ if max_width is not None:
696
+ cells = max(0, int(max_width // char_w))
697
+ if len(text) > cells:
698
+ text = text[:cells]
699
+ width = len(text) * char_w
700
+ block_f, sym_f, text_f = _wash_factors()
701
+
702
+ # ── block wash: the tinted definition body this line belongs to ──
703
+ if block_tint is not None:
704
+ rgb = _bg_adjust(tuple(block_tint[:3]), block_f)
705
+ a = max(0.0, min(1.0, Toggles.TextEditor.def_block_alpha)) * alpha
706
+ draw_list.add_rect_filled(x, y, x + max(len(text) + 1, 2) * char_w,
707
+ y + line_h, _u32(rgb, a), block_rounding)
708
+
709
+ # ── occurrence washes (+ their outline), same rects as the editor ──
710
+ sym_a = Toggles.TextEditor.def_symbol_alpha * alpha
711
+ ol_a = Toggles.TextEditor.def_symbol_outline_alpha * alpha
712
+ ol_b = Toggles.TextEditor.def_symbol_outline_brightness
713
+ ol_t = Toggles.TextEditor.def_symbol_outline_thickness
714
+ mixes = [] # (col_start, col_end, rgb_adjusted_for_text, scale)
715
+ for c0, c1, rgb, scale in spans:
716
+ c0, c1 = max(0, c0), min(len(text), c1)
717
+ if c1 <= c0:
718
+ continue
719
+ sa = _bg_adjust(tuple(rgb[:3]), sym_f)
720
+ sx, ex = x + c0 * char_w, x + c1 * char_w
721
+ draw_list.add_rect_filled(sx - 1, y + 1, ex + 1, y + line_h - 1,
722
+ _u32(sa, sym_a * scale), 3.0)
723
+ if ol_a > 0:
724
+ ol = (min(1.0, sa[0] * ol_b), min(1.0, sa[1] * ol_b), min(1.0, sa[2] * ol_b))
725
+ draw_list.add_rect(sx - 1, y + 1, ex + 1, y + line_h - 1,
726
+ _u32(ol, ol_a * scale), 3.0, thickness=ol_t)
727
+ mixes.append((c0, c1, _bg_adjust(tuple(rgb[:3]), text_f), scale))
728
+
729
+ # ── glyphs: tokens → palette, mixed by the wash under them ──
730
+ mix_k = Toggles.TextEditor.def_text_tint_mix
731
+ default = COLORS["default"]
732
+ try:
733
+ tokens = _tokenize_from(text, line_open)
734
+ except Exception:
735
+ tokens = [(text, "default")]
736
+ if emphasis is not None and dim_alpha is None:
737
+ dim_alpha = Toggles.TextEditor.diff_preview_alpha
738
+ col = 0
739
+ run_parts, run_x, run_col = None, 0.0, 0
740
+ pieces = [] # (col, text, kind) - tokens cut at emphasis boundaries
741
+ for token, kind in tokens:
742
+ if not token:
743
+ continue
744
+ nl = token.find("\n")
745
+ if nl != -1:
746
+ token = token[:nl]
747
+ if not token:
748
+ break
749
+ if kind == "clipped":
750
+ col += len(token)
751
+ continue
752
+ if emphasis is not None:
753
+ for pc, pt, bright in _split_emphasis(col, token, emphasis):
754
+ pieces.append((pc, pt, kind, bright))
755
+ else:
756
+ pieces.append((col, token, kind, True))
757
+ col += len(token)
758
+ if nl != -1:
759
+ break
760
+ for col, token, kind, bright in pieces:
761
+ color = COLORS.get(kind, default)
762
+ for c0, c1, rgb, scale in mixes:
763
+ if c0 <= col < c1:
764
+ color = _mix_packed(color, rgb, mix_k * scale)
765
+ break
766
+ if alpha < 1.0:
767
+ color = _fade_packed(color, alpha)
768
+ if not bright:
769
+ color = _fade_packed(color, dim_alpha)
770
+ tx = x + col * char_w
771
+ if kind == "icon":
772
+ if run_parts is not None:
773
+ draw_list.add_text(run_x, y, run_col, "".join(run_parts))
774
+ run_parts = None
775
+ # Icons aren't monospaced: one per standard cell, nudged 1px left.
776
+ ix = tx
777
+ for ch in token:
778
+ draw_list.add_text(ix - 1, y, color, ch)
779
+ ix += char_w
780
+ elif token.isascii() and "\t" not in token:
781
+ if run_parts is not None and run_col == color:
782
+ run_parts.append(token)
783
+ else:
784
+ if run_parts is not None:
785
+ draw_list.add_text(run_x, y, run_col, "".join(run_parts))
786
+ run_parts, run_x, run_col = [token], tx, color
787
+ else:
788
+ if run_parts is not None:
789
+ draw_list.add_text(run_x, y, run_col, "".join(run_parts))
790
+ run_parts = None
791
+ draw_list.add_text(tx, y, color, token)
792
+ if run_parts is not None:
793
+ draw_list.add_text(run_x, y, run_col, "".join(run_parts))
794
+ return width
795
+
796
+
797
+ def draw_live_view_overlay(x=0, y=0, w=0, h=0, draw_state=None, char_w=8.0,
798
+ line_px=20.0, node=None, span=None, root=None,
799
+ line_offset=0, jump_to=None, sel_lo=None,
800
+ sel_hi=None, **kwargs):
801
+ """token_views overlay callback for CallParse nodes (plain function — the
802
+ overlay pass calls it with raw screen coords, no render_func wrapper)."""
803
+ from meltygui.code.live_view import live_values_for
804
+ from meltygui.code.live_view import site_for_line
805
+ from meltygui.editor.live_view_views import _code_end_col
806
+ from meltygui.editor.live_view_views import _draw_marker_at
807
+ from meltygui.editor.live_view_views import _inline_value_text
808
+ from meltygui.editor.live_view_views import _line_in_selection
809
+ from meltygui.editor.live_view_views import _marker_idle_skip
810
+ from meltygui.editor.live_view_views import _stable_key_name
811
+ from meltygui.editor.live_view_views import _store_key_names
812
+ from meltygui.editor.live_view_views import _store_name
813
+ from meltygui.editor.live_view_views import _token_in_selection
814
+
815
+ if getattr(node, "func_name", None) != "live_view":
816
+ return
817
+ from meltygui.core.runtime.toggles import Toggles
818
+ if not Toggles.TextEditor.enable_live_view:
819
+ return
820
+ # Viewport cull FIRST: the parse walk visits every node in the buffer, not
821
+ # just the visible ones - each off-screen marker is a full render_func call
822
+ # for nothing (its latched value will propagate via root_draw_states
823
+ # either way, exactly as when its liveosh scrolls in). Culling before
824
+ # the store lookup also keeps site_for_line (span parse + linemap, per
825
+ # node per frame) off every out-of-view live_view node.
826
+ clip = getattr(draw_state, "abs_clip_rect", None)
827
+ _off_view = clip is not None and (y + line_px < clip[1] or y > clip[3])
828
+ if (_off_view and getattr(draw_state, "_lv_full_overlay_until", 0)
829
+ <= Core.melty.frame_count):
830
+ # _lv_full_overlay_until: non-instrumented-run forward pass - an
831
+ # off-viewport marker with an OPEN window still renders once (with a
832
+ # FROZEN anchor, below) so the next value flows into the window.
833
+ return
834
+ filename = (getattr(root, "file_path", None)
835
+ or getattr(getattr(root, "address", None), "path", None)
836
+ or getattr(jump_to, "path", None))
837
+ if filename is None:
838
+ return
839
+ # span lines are 1-indexed relative to the editor buffer; line_offset is
840
+ # the 0-based file line of buffer line 0 → absolute 1-indexed file line.
841
+ store_obj, key_path = site_for_line(str(filename),
842
+ line_offset + span.start_line)
843
+ if store_obj is None:
844
+ return
845
+ token_cells = max(1, span.end_col - span.start_col)
846
+ if getattr(span, "end_line", span.start_line) != span.start_line:
847
+ # Multi-line call: box just the first line, from the token start to
848
+ # the end of that line's text.
849
+ src_lines = (getattr(root, "source", "") or "").split("\n")
850
+ if 1 <= span.start_line <= len(src_lines):
851
+ token_cells = max(1, len(src_lines[span.start_line - 1].rstrip())
852
+ - span.start_col)
853
+ pad = 2.0
854
+ # Selection predicate in buffer space: span lines are parse-relative,
855
+ # so map through the parse→buffer bridge before comparing with the
856
+ # editor's (line, col) selection bounds. The widget counts as "inside"
857
+ # only when its whole token lies within the selection - a bare caret
858
+ # (no selection) never shows the window.
859
+ _lm = kwargs.get("line_map")
860
+ _sl = _lm(span.start_line) if _lm else span.start_line
861
+ cursor_inside = _token_in_selection(
862
+ _sl, span.start_col, span.start_col + token_cells, sel_lo, sel_hi)
863
+ snap = live_values_for(store_obj)
864
+ # The SHARED per-store name map (generation-keyed) - never a private
865
+ # copy: a stale private map minted names another consumer's fresh map
866
+ # gave to different keys (duplicate view IDs). The fallback name the
867
+ # key_path too, so a not-yet-mapped key still ranks against its twins.
868
+ _me = _store_key_names(store_obj)
869
+ _mname = (f"lvm::{_store_name(store_obj)}"
870
+ f"::{_me.get(key_path) or _stable_key_name(key_path, snap)}")
871
+ _frozen_pos = None
872
+ if _off_view:
873
+ # Forward pass for a culled marker: only proceed when its window is
874
+ # open, and draw at the marker's current absolute position - anchoring
875
+ # the pinned window at the true off-screen anchor parks it at
876
+ # _pinned_base = an editor-bottom clamp (the window "disappears").
877
+ _mreg = getattr(draw_state, "_lv_marker_ds", None)
878
+ _mds = _mreg.get(_mname) if _mreg else None
879
+ _w = getattr(_mds, "_lv_window_ds", None) if _mds else None
880
+ if _mds is None or _w is None or _w.closed:
881
+ return
882
+ _frozen_pos = (_mds.abs_left, _mds.abs_top)
883
+ _value = snap.get(key_path)
884
+ # A marker with an inline value draws every editor repaint (the value
885
+ # bakes into the tile), so it can never take the idle skip.
886
+ if (_frozen_pos is None
887
+ and _inline_value_text(_value) is None
888
+ and _marker_idle_skip(
889
+ draw_state, _mname, x - pad, y - pad,
890
+ token_cells * char_w + 2 * pad, line_px + 2 * pad,
891
+ key_path in snap, cursor_inside, store_obj, key_path,
892
+ (_sl or span.start_line) - 1, True)):
893
+ return
894
+ # Inline pill over a live_view() call: the call is instrumentation, not
895
+ # code worth peeking at, so the card FILLS the entire call span; the
896
+ # value may grow past it only when the span ends its line's code
897
+ # (source split shared with the snapshot overlay's memo, same memo
898
+ # invalidation).
899
+ _memo_ent = draw_state.__dict__.get("_lv_snap_memo")
900
+ _rsrc = getattr(root, "source", "") or ""
901
+ if _memo_ent is None or _memo_ent[0] is not _rsrc or len(_memo_ent) < 3:
902
+ _memo_ent = (_rsrc, {}, _rsrc.split("\n"))
903
+ object.__setattr__(draw_state, "_lv_snap_memo", _memo_ent)
904
+ _lines = _memo_ent[2]
905
+ _ltext = (_lines[span.start_line - 1]
906
+ if 1 <= span.start_line <= len(_lines) else "")
907
+ _tok_end = span.start_col + token_cells
908
+ _overflow = _tok_end >= _code_end_col(_ltext)
909
+ _draw_marker_at(draw_state,
910
+ _frozen_pos if _frozen_pos is not None
911
+ else (x - pad, y - pad),
912
+ cursor_inside,
913
+ (_sl, span.start_col, span.start_col + token_cells),
914
+ "/".join(map(str, key_path)),
915
+ value=_value,
916
+ captured=key_path in snap,
917
+ store_obj=store_obj, key_path=key_path,
918
+ inline_values=True,
919
+ inline_span_w=token_cells * char_w,
920
+ inline_overflow=_overflow, inline_fill=True,
921
+ in_selection=_line_in_selection(_sl, sel_lo, sel_hi),
922
+ caret_line=kwargs.get("caret_line"),
923
+ width=token_cells * char_w + 2 * pad,
924
+ height=line_px + 2 * pad,
925
+ buffer_line=(_sl or span.start_line) - 1,
926
+ name=_mname)
927
+
928
+
929
+ @render_func(use_cache=False, show_bg=False, shadow=False, with_header=None,
930
+ show_name=False, selectable=False, disable_scroll=True, wrap=True,
931
+ z_offset=4, max_height=32, auto_state=False)
932
+ def draw_live_view_marker(input_value=None, draw_state=None,
933
+ store_obj=None, key_path=None, captured=False,
934
+ code_tree_node=None, auto_open=True,
935
+ inline_values=False, inline_dx=0.0,
936
+ inline_span_w=None, inline_overflow=False,
937
+ inline_fill=False,
938
+ in_selection=False, caret_line=None,
939
+ corner_radius=4.0, value=None,
940
+ left_mouse_double_clicked=False,
941
+ cursor_inside=False, editor_ds=None,
942
+ buffer_line=None, def_node=None, unique=0, **kwargs):
943
+ """The live-view token widget — draw_bool_token's pattern plus one extra
944
+ call, draw_any(value, mode=WINDOW). `value` is the captured value (None +
945
+ captured=False while the site hasn't run); the window call just forwards
946
+ it and the framework routes it by type. input_value is a cheap STABLE
947
+ TOKEN (the key string), deliberately NOT the value: the wrapper's
948
+ recursion guard tracks non-primitive input_values by id, and a captured
949
+ object that also sits in the render ancestry (draw_state/ds aliases, the
950
+ menu's own target) tripped it — painting "Recursive reference detected"
951
+ over the code — while big values also paid wrapper bookkeeping per
952
+ marker. draw_text positioned this view inline over the symbol and the
953
+ draw_state carries its size — no rect plumbing.
954
+
955
+ (A bare-function version — draw_number_token_plain's pattern — was tried
956
+ and REVERTED: the swoosh connector anchors the value window back to the
957
+ marker's draw_state, so the marker must stay a render_func. Off-viewport
958
+ markers are culled by the overlays, which bounds the wrapper cost to
959
+ visible markers.)
960
+
961
+ `code_tree_node` is the owning scope's dict from draw_text's parse; the
962
+ site's `# [...]` comment is already formatted as a dict there
963
+ (__overrides__['__<key>__']) and IS, 1:1, the window call's **kwargs.
964
+ The box wears its `tint`.
965
+
966
+ Window close: the one place a Modes.WINDOW child differs from a direct
967
+ call — the framework can't see when a parent STOPS calling draw_any (it
968
+ approximates liveness with abs_closed) — so the window ds is tracked by
969
+ hand and, once it exists, called every render with visibility driven
970
+ through the closed= kwarg. (Maybe the framework can own this someday.)
971
+
972
+ `auto_open=False` (the snapshot/param markers) keeps the window closed
973
+ until the box is double-clicked — only explicit live_view() tokens pop
974
+ their value unprompted.
975
+
976
+ Interaction follows the number-widget convention: a single press/click
977
+ passes straight through to the editor (caret placement, selection —
978
+ plain text editing; nothing is declared to latch it away), and the
979
+ widget's own gesture is separate — DOUBLE-click toggles the value
980
+ window. left_mouse_double_clicked is declared (never read) as the
981
+ subscription half: hover-routed — the marker's higher z outranks the
982
+ editor's word-select for the doubled press — and its delivery
983
+ invalidates the tile so the body renders on the frame the raw
984
+ is_mouse_double_clicked read below is true."""
985
+ from meltygui.code.live_view import auto_dim_names_for
986
+ from meltygui.code.live_view import watch
987
+ from meltygui.editor.live_view_views import _NO_VALUE
988
+ from meltygui.editor.live_view_views import _SWATCH_HOLE
989
+ from meltygui.editor.live_view_views import _auto_run_on_user_open
990
+ from meltygui.editor.live_view_views import _def_name
991
+ from meltygui.editor.live_view_views import _display_key
992
+ from meltygui.editor.live_view_views import _drop_captured_value
993
+ from meltygui.editor.live_view_views import _ds_in_window
994
+ from meltygui.editor.live_view_views import _inline_swatch_rgba
995
+ from meltygui.editor.live_view_views import _inline_value_text
996
+ from meltygui.editor.live_view_views import _left_of_window_pos
997
+ from meltygui.editor.live_view_views import _merged_dim_names
998
+ from meltygui.editor.live_view_views import _mouse_in_window_tree
999
+ from meltygui.editor.live_view_views import _override_owner
1000
+ from meltygui.editor.live_view_views import _padded_dim_names
1001
+ from meltygui.editor.live_view_views import _paint_value_pill
1002
+ from meltygui.editor.live_view_views import _pill_tint
1003
+ from meltygui.editor.live_view_views import _stacked_list_value
1004
+ from meltygui.editor.live_view_views import current_live_root
1005
+ from meltygui.editor.live_view_views import release_live_value
1006
+ from meltygui.view.header_view import draw_header
1007
+
1008
+ ds = draw_state
1009
+ # Gutter registry: tell the editor which buffer lines carry a live marker
1010
+ # so its line-number gutter can draw a raw open/close button per line
1011
+ # (see the gutter pass in text_editor / set_marker_open below). Rebuilt
1012
+ # from scratch each frame the overlays render - frame-stamped so stale
1013
+ # entries from a previous parse never linger on the editor ds.
1014
+ if editor_ds is not None and buffer_line is not None:
1015
+ if getattr(editor_ds, "_lv_gutter_frame", None) != Core.melty.frame_count:
1016
+ editor_ds._lv_gutter_frame = Core.melty.frame_count
1017
+ editor_ds._lv_gutter_markers = {}
1018
+ editor_ds._lv_gutter_markers.setdefault(buffer_line, []).append(ds)
1019
+ # Marker-ds registry for the overlays' idle fast path (_marker_idle_skip):
1020
+ # lets them consult this site's state without paying the wrapper.
1021
+ if editor_ds is not None:
1022
+ reg = getattr(editor_ds, "_lv_marker_ds", None)
1023
+ if reg is None:
1024
+ reg = editor_ds._lv_marker_ds = {}
1025
+ reg[ds.name] = ds
1026
+ # Backlink for set_marker_open (the gutter pass only has the marker
1027
+ # ds): the value window anchors to the draw TEXT's left edge, which
1028
+ # only the editor ds knows.
1029
+ ds._lv_editor_ds = editor_ds
1030
+ # First only only: a gray box whose code then runs gets invalidated
1031
+ # on the key's FIRST value, flips green (and auto-opens below, when this
1032
+ # marker auto-opens) — one editor re-render per new key, nothing per
1033
+ # steady-state publish. The invalidation re-runs the overlay, which boxes
1034
+ # the marker and passes the new value in.
1035
+ watch(store_obj, key_path, ds, first_only=True)
1036
+
1037
+ # The comment data, already a dict in the code tree. Statement keys ARE
1038
+ # the symbol name; a `line:N#name` tail (frame snapshots, twin-site
1039
+ # params) carries the name behind the hash - the `# [...]` comment is
1040
+ # stamped in __overrides__ under the STATEMENT key, so resolve by it
1041
+ # either way (this is what keeps comment overrides like tint/cam_zoom
1042
+ # flowing into the value windows after the line-key migration). The
1043
+ # owning dict is found by _override_owner - a loop/if/try-body site's
1044
+ # comment lives in ITS block's nested dict, not the top of the scope.
1045
+ comment_args = {}
1046
+ lookup_key = None
1047
+ live_root = code_tree_node
1048
+ _locator = None
1049
+ if key_path:
1050
+ tail = str(key_path[-1])
1051
+ lookup_key = (tail.split("#", 1)[1]
1052
+ if tail.startswith("line:") and "#" in tail else tail)
1053
+ if isinstance(code_tree_node, dict) and lookup_key:
1054
+ # Locator for current_live_root (def name + start line, the editor
1055
+ # ds only as a fallback during readers), stamped BEFORE the comment
1056
+ # read so this render's own splat also resolves through the code
1057
+ # host's held tree - `code_tree_node` is whatever the cached tabs
1058
+ # body last captured and can lag a reparse by frames.
1059
+ _dname = _def_name(def_node)
1060
+ _locator = None
1061
+ if _dname:
1062
+ _dspan = getattr(def_node, "span", None)
1063
+ _locator = (editor_ds, _dname, getattr(_dspan, "start_line", 0) or 0)
1064
+ # Owner through the editor's per-def index (one BFS per def per
1065
+ # source version), not a BFS per marker; the raw walk stays the
1066
+ # fallback when there's no editor / def to key on.
1067
+ _osrc = None
1068
+ if editor_ds is not None and _locator is not None:
1069
+ _ecd = editor_ds.__dict__.get("code_dict")
1070
+ if not isinstance(_ecd, dict):
1071
+ _ecd = editor_ds.__dict__.get("code_tree")
1072
+ _osrc = getattr(_ecd, "source", None) if isinstance(_ecd, dict) else None
1073
+ live_root = _override_owner(
1074
+ code_tree_node, lookup_key,
1075
+ index_host=editor_ds if _osrc is not None else None,
1076
+ def_key=(_locator[1], _locator[2]) if _locator else None,
1077
+ src=_osrc)
1078
+ object.__setattr__(ds, "live_root", live_root)
1079
+ object.__setattr__(ds, "live_key", lookup_key)
1080
+ object.__setattr__(ds, "_lv_locator", _locator)
1081
+ _resolved = current_live_root(ds)
1082
+ if isinstance(_resolved, dict):
1083
+ live_root = _resolved
1084
+ _ov = live_root.get("__overrides__")
1085
+ _ca = _ov.get(f"__{lookup_key}__") if isinstance(_ov, dict) else None
1086
+ if isinstance(_ca, dict):
1087
+ comment_args = {k: v for k, v in _ca.items()
1088
+ if not (isinstance(k, str) and k.startswith("__"))}
1089
+
1090
+ # A list of same-shape tensors renders as ONE stacked tensor (leading
1091
+ # dim = list index) - the accumulator normally stacks at capture time,
1092
+ # but a raw captured list, its ragged-shape fast path, or a store built
1093
+ # by older code all arrive here as lists; healing at display time makes
1094
+ # "stacked" unconditional.
1095
+ value = _stacked_list_value(value, ds)
1096
+
1097
+ # dim_names is a SPECIAL input for loop sites: an accumulation value
1098
+ # carries auto-named leading dims (one per enclosing loop - stamped in
1099
+ # live_view._stack), and the site's own `# [dim_names=...]` names the
1100
+ # per-iteration value's dims. Prepend auto to user before the window
1101
+ # renders, so `for l_idx ...` over a (head, query, key) accumulator reads
1102
+ # ('l_idx', 'head', 'query', 'key') in the voxel tab. Tensor-shaped
1103
+ # values only - a list accumulator has no dims to name.
1104
+ _auto_dims = auto_dim_names_for(store_obj, key_path)
1105
+ _vkind = type(value).__name__
1106
+ _user_dims = comment_args.get("dim_names")
1107
+ _merge_auto = (_auto_dims if _auto_dims
1108
+ and _vkind in ("Tensor", "ndarray") else None)
1109
+ if _merge_auto:
1110
+ comment_args = dict(comment_args)
1111
+ comment_args["dim_names"] = _merged_dim_names(_merge_auto, _user_dims)
1112
+ # Too few names for the value's dims (or none at all)? pad with
1113
+ # positional dim<i> entries - AFTER the auto merge, so the pad covers
1114
+ # whatever the merged list still leaves out.
1115
+ _ndim = (len(getattr(value, "shape", ()))
1116
+ if _vkind in ("Tensor", "ndarray") else 0)
1117
+ _pad = _padded_dim_names(comment_args.get("dim_names"), _ndim)
1118
+ if _pad:
1119
+ comment_args = dict(comment_args)
1120
+ comment_args["dim_names"] = _pad
1121
+
1122
+ # Input-tab lookup: the context menu resolves this site's inputs off the
1123
+ # draw_state graph (draw_input_tab's live_root branch), so attach the same
1124
+ # OWNING dict the comment-args splat reads - the nested block dict for a
1125
+ # loop-body site - restamped every render like everything else per-site.
1126
+ # set_anywhere's lazy `# []` entry then materializes at the level the
1127
+ # save patch then writes back (a top-of-scope entry for a loop site
1128
+ # would never reach the site). Same name-normalized key as the comment
1129
+ # lookup, so line-keyed sites find their statement entry too.
1130
+ ds.live_root = live_root
1131
+ ds.live_key = lookup_key
1132
+ # Locator for current_live_root (stamped first, before the comment
1133
+ # read): readers that run while this marker ISN'T rendering (replayed
1134
+ # window, set_anywhere from its panel) resolve the owner dict through
1135
+ # the code host's held tree instead of the stamp, which every reparse
1136
+ # orwrites. The owner memo (_lv_owner_dict) is keyed on tree identity
1137
+ # and therefore survives renders - the def lookup runs once per
1138
+ # reparse, not once per frame.
1139
+ object.__setattr__(ds, "_lv_locator", _locator)
1140
+
1141
+ from meltygui.core.windowing.window_visibility import sync_marker_visibility
1142
+ sync_marker_visibility(ds, comment_args)
1143
+ auto_open = comment_args.get("auto_open", auto_open)
1144
+
1145
+ # ── INLINE VALUE: a simple builtin (int/float/str/bool/shortlist/enum)
1146
+ # renders as a text label overlapping the top of its code line instead
1147
+ # of a separate window. Both modes pass inline_values=True (explicit
1148
+ # live_view() tokens and instrumented-run snapshot markers alike):
1149
+ # every boxed symbol with a simple value shows up in place.
1150
+ inline_text = (_inline_value_text(value)
1151
+ if captured and inline_values else None)
1152
+ inline_swatch = None
1153
+ if inline_text is not None:
1154
+ _rgba = _inline_swatch_rgba(value)
1155
+ if _rgba is not None:
1156
+ inline_text = _SWATCH_HOLE + inline_text
1157
+ inline_swatch = (_rgba, 0)
1158
+ # An inline value has NO value window at all - no auto-open, no
1159
+ # preview, no double-click toggle. A window still open (persisted state,
1160
+ # or the value just turned simple) closes through the ordinary closing
1161
+ # draw_any call below. _lv_open resets to None, so a value that later
1162
+ # turns complex (str → tensor between runs) auto-opens again.
1163
+ if inline_text is not None and getattr(ds, "_lv_open", False):
1164
+ ds._lv_open = None
1165
+ # The gutter swaps the magnifier for an info glyph on inline markers
1166
+ # (no open/close left to toggle) - after the _lv_open pass in text.py.
1167
+ if getattr(ds, "_lv_inline", None) != (inline_text is not None):
1168
+ ds._lv_inline = inline_text is not None
1169
+
1170
+ # Manual window-ds tracking (see docstring).
1171
+ win_ds = getattr(ds, "_lv_window_ds", None)
1172
+ if not captured and win_ds is not None and not win_ds.closed:
1173
+ # The captured value vanished (store dropped) while the window was
1174
+ # open: with captured False the draw_any block below never runs, so
1175
+ # nothing else would stamp closed= and the window would linger
1176
+ # orphaned in root_draw_states. Stamp it directly; _lv_open resets to
1177
+ # None so the next value auto-opens it.
1178
+ win_ds.closed = True
1179
+ ds._lv_open = None
1180
+ from meltygui.core.windowing.glfw_utils import request_render
1181
+ request_render()
1182
+ if (getattr(ds, "_lv_open", None) is None and captured and auto_open
1183
+ and inline_text is None):
1184
+ ds._lv_open = True # first value seen → show it without a click
1185
+ elif win_ds is not None and win_ds.closed and getattr(ds, "_lv_open", False):
1186
+ ds._lv_open = False # user closed the window via its own header X
1187
+ # Arm the cursor-dismissed latch too: with the caret still inside the
1188
+ # symbol, preview_show would otherwise re-show the window on the very
1189
+ # next frame - the X X pins first (it's a press inside the window
1190
+ # rect), then closes, and an unarmed latch made the close a no-op.
1191
+ # The latch resets itself once the focused caret leaves the symbol.
1192
+ ds._lv_cursor_dismissed = True
1193
+ # Closing a live view forgets its DATA (the loop accumulator + the
1194
+ # tensor, often hundreds of MB) - the store keeps the marker with a
1195
+ # rerun hint so the widget stays, and the next run refills it.
1196
+ _drop_captured_value(store_obj, key_path)
1197
+ open_now = bool(getattr(ds, "_lv_open", False))
1198
+
1199
+ # ── EDIT-PIN: engaging with a preview window's own UI latches it open.
1200
+ # A cursor-held preview closes the moment the editor loses editor focus -
1201
+ # but the click that starts a param edit (a field in the window's
1202
+ # controls, or a context menu) IS such a focus change, so editing the
1203
+ # view's input params yanked the window (and the panel mid-edit) away.
1204
+ # The PRESS is the trigger, not focus: click processing clears the
1205
+ # editor's focus at frame start, this marker then sees and would stamp
1206
+ # the close, and only at end-of-frame dispatch would the clicked field
1207
+ # render and grab focus - by which point a closed window's panel never
1208
+ # renders at all. So on any engaged mouse press, rect-test the mouse
1209
+ # against the window and its controls and act exactly as if the marker
1210
+ # had been double-clicked; the focus check remains as the late-signal
1211
+ # fallback (e.g. focus handed over without a press). The header X still
1212
+ # unpins via the win_ds.closed branch above.
1213
+ if (captured and not open_now and inline_text is None
1214
+ and win_ds is not None and not win_ds.closed):
1215
+ _m = Core.melty
1216
+ pin = any(f is not None and _ds_in_window(f, win_ds)
1217
+ for f in (_m.focused_ds, _m.text_focused_ds,
1218
+ _m.popover_focused_ds))
1219
+ if not pin and (imgui.is_mouse_down(0) or imgui.is_mouse_clicked(0)
1220
+ or imgui.is_mouse_released(0)
1221
+ or imgui.is_mouse_down(1)
1222
+ or imgui.is_mouse_clicked(1)):
1223
+ _io = imgui.get_io()
1224
+ pin = _mouse_in_window_tree(win_ds, _io.mouse_pos.x,
1225
+ _io.mouse_pos.y)
1226
+ if pin:
1227
+ ds._lv_open = True
1228
+ open_now = True
1229
+ ds.invalidate()
1230
+
1231
+ x, y = imgui.get_cursor_screen_pos()
1232
+ w = max(1.0, ds.width)
1233
+ h = max(1.0, ds.height)
1234
+ io = imgui.get_io()
1235
+ hovered = x <= io.mouse_pos.x < x + w and y <= io.mouse_pos.y < y + h
1236
+ # Hover-edge invalidation (enter/leave only, never per-frame): the
1237
+ # outline is never-stated, so a cached tile must repaint exactly when
1238
+ # visibility changes.
1239
+ if getattr(ds, "_lv_hovered", None) != hovered:
1240
+ ds._lv_hovered = hovered
1241
+ ds.invalidate()
1242
+
1243
+ # PREVIEW: temporarily show the captured marker's live value - same
1244
+ # window, same placement - with double-click below still latching it
1245
+ # open permanently. Two trigger modes on Toggles.TextEditor.
1246
+ # live_hover_preview: ON → mousing over the marker previews and
1247
+ # mouse-leave closes; OFF → the editor TEXT CURSOR coming inside the
1248
+ # symbol previews and caret-leave closes. The hover mode needs a
1249
+ # per-frame keep-alive while previewing (the leave edge can only be
1250
+ # SEEN by a running body - a cached tile never re-tests hover); the
1251
+ # cursor mode doesn't: the caret only moves on frames the editor
1252
+ # renders, and the cursor_inside edge below invalidates the tile.
1253
+ from meltygui.core.runtime.toggles import Toggles
1254
+ hover_mode = bool(Toggles.TextEditor.live_hover_preview)
1255
+ _raw_ci = bool(cursor_inside) and not hover_mode
1256
+ # Dismissed latch: an X-closed cursor preview stays dismissed until the
1257
+ # FOCUSED caret genuinely leaves the symbol once. clear_focus alone was
1258
+ # not enough - the close's focus drop raced re-grants, and any regained
1259
+ # focus with the caret still in the symbol instantly re-showed the
1260
+ # window. The latch is positional, so it holds through editor churn;
1261
+ # while the editor is unfocused the overlay passes null cursor coords
1262
+ # (_raw_ci False), so the reset below keys off actual editor focus.
1263
+ _ed_focused = (editor_ds is not None
1264
+ and Core.melty.text_focused_ds is editor_ds)
1265
+ if _ed_focused and not _raw_ci:
1266
+ ds._lv_cursor_dismissed = False
1267
+ cursor_inside = _raw_ci and not getattr(ds, "_lv_cursor_dismissed", False)
1268
+ # Close observed one frame late (window rendered from root_draw_states
1269
+ # while this editor tile was cached): mark dismissal as the last
1270
+ # detection after draw_any below.
1271
+ if (cursor_inside and not open_now and win_ds is not None
1272
+ and win_ds.closed
1273
+ and getattr(ds, "_lv_cursor_preview_shown", False)):
1274
+ Core.melty.clear_focus()
1275
+ ds._lv_cursor_dismissed = True
1276
+ ds._lv_cursor_preview_shown = False
1277
+ cursor_inside = False
1278
+ if getattr(ds, "_lv_cursor_in", None) != cursor_inside:
1279
+ ds._lv_cursor_in = cursor_inside
1280
+ ds.invalidate()
1281
+ preview_show = (captured and not open_now and inline_text is None
1282
+ and ((hovered and hover_mode) or cursor_inside))
1283
+ if preview_show and hover_mode:
1284
+ from meltygui.core.windowing.glfw_utils import request_render
1285
+ ds.invalidate()
1286
+ request_render()
1287
+
1288
+ tint = comment_args.get("tint")
1289
+ if captured:
1290
+ if tint is not None:
1291
+ base = tuple(min(1.0, c + (0.15 if open_now else 0.0))
1292
+ for c in tint[:3])
1293
+ else:
1294
+ base = (0.36, 0.85, 0.46) if open_now else (0.26, 0.62, 0.34)
1295
+ else:
1296
+ base = (0.45, 0.45, 0.45)
1297
+ if hovered:
1298
+ base = tuple(min(1.0, c + 0.18) for c in base)
1299
+ # Outline only under the mouse — the boxes read as clutter when every
1300
+ # instrumented symbol is permanently framed; hover reveals the
1301
+ # affordance. An inline marker skips it: its pill wears the token's
1302
+ # background tint (painted below), and there's no window gesture for
1303
+ # hover to advertise.
1304
+ if hovered and inline_text is None:
1305
+ dl: _DrawList = imgui.get_window_draw_list()
1306
+ dl.add_rect(x, y + 2, x + w, y + h - 3,
1307
+ pack_color(*base, 0.9 if open_now else 0.6),
1308
+ rounding=corner_radius)
1309
+ imgui.dummy(w, h)
1310
+
1311
+ if inline_text is not None:
1312
+ # The label must repaint on every publish - the first_only watch
1313
+ # above fires once; this full watch invalidates per value.
1314
+ watch(store_obj, key_path, ds)
1315
+ # Only the FILL pill (a live_viewed call token - instrumentation
1316
+ # worth painting whole) paints here; comment/snapshot binding
1317
+ # markers paint through the trailing-gap pass instead (see
1318
+ # _draw_usage_labels - the code is repositioned, never covered).
1319
+ # Caret on this line, or the line inside the text selection: show
1320
+ # the real code, nothing painted - typing or deleting under a pill
1321
+ # would be blind. The pill comes back when the caret/selection
1322
+ # leave (the kwarg change re-renders this).
1323
+ if (inline_fill
1324
+ and (caret_line is None or caret_line != buffer_line)
1325
+ and not in_selection):
1326
+ # Drawn IN PLACE in the editor's code font (the current font -
1327
+ # no push), covering the instrumentation call token. The marker rect
1328
+ # wraps the token with a 2 px pad, so the token's text starts
1329
+ # at x + 2.
1330
+ text_x = x + 2.0 + inline_dx
1331
+ text_y = y + 2.0
1332
+ # The editor clipped this body to the token's own rect; the
1333
+ # pill is value-sized and can sit past that (the RHS). Pop out
1334
+ # to the ENCLOSING clip (the editor window) through Melty's own
1335
+ # stack and re-push the SAME rect after: push_clip intersects
1336
+ # with its parent, so pushing the saved (already-intersected)
1337
+ # rect restores it exactly, and the clip bookkeeping the tile
1338
+ # engine reads stays coherent.
1339
+ saved_clip = (Core.melty.clip_stack[-1]
1340
+ if Core.melty.clip_stack else None)
1341
+ if saved_clip is not None:
1342
+ Core.melty.pop_clip()
1343
+ # The pill wears the token's background tint (the comment
1344
+ # tint, else the enclosing def/class, else the file) so an RHS
1345
+ # pill far down the line is colored as this scope's value.
1346
+ _paint_value_pill(inline_text, text_x, text_y,
1347
+ span_width=inline_span_w,
1348
+ allow_overflow=inline_overflow,
1349
+ fill=inline_fill,
1350
+ tint=_pill_tint(editor_ds, buffer_line,
1351
+ str(key_path[-1]) if key_path
1352
+ else None, tint),
1353
+ swatch=inline_swatch)
1354
+ if saved_clip is not None:
1355
+ Core.melty.push_clip(saved_clip)
1356
+
1357
+ # Double-click toggles the value window. Read RAW imgui here (the same
1358
+ # split the single-click version used): the declared event param is the
1359
+ # subscription/wake half - its delivery invalidates the tile so the body
1360
+ # renders on the very frame is_mouse_double_clicked is true - while the raw
1361
+ # read is the single trigger, so the two halves can never toggle twice
1362
+ # for one gesture.
1363
+ if hovered and inline_text is None and imgui.is_mouse_double_clicked(0):
1364
+ open_now = not open_now
1365
+ ds._lv_open = open_now
1366
+ from meltygui.core.windowing.window_visibility import marker_user_visibility
1367
+ marker_user_visibility(ds, not open_now)
1368
+ if open_now:
1369
+ _auto_run_on_user_open(editor_ds, store_obj)
1370
+ if open_now and win_ds is not None and "window_pos" not in comment_args:
1371
+ # Reopening: snap the window back to the LEFT of the editor
1372
+ # window (it may have been dragged onto the code). The window's
1373
+ # real size is known here, so the display clamps are exact.
1374
+ pos = _left_of_window_pos(
1375
+ editor_ds.abs_left if editor_ds is not None else None,
1376
+ x, marker_y=y, win_h=win_ds.height, win_w=win_ds.width)
1377
+ if pos is not None:
1378
+ win_ds.window_pos = pos
1379
+ ds.invalidate()
1380
+
1381
+ # Draw the value window only while it shows - plus ONE closing call when
1382
+ # it just stopped showing (closed=True must be stamped on the ds so the
1383
+ # deferred dispatch discards it; skipping that call would leave an orphan
1384
+ # window rendering from root_draw_states). A window the user X-closed is
1385
+ # already stamped, so a closed marker costs zero draw_any calls per
1386
+ # render - and the full per-publish watch below stops too, leaving only
1387
+ # the cheap first_only watch above.
1388
+ _show = open_now or preview_show
1389
+ if captured and (_show or (win_ds is not None and not win_ds.closed)):
1390
+ # Full (per-publish) watch once a window exists so the value streams
1391
+ # in - the first_only call above only flips the box green.
1392
+ watch(store_obj, key_path, ds)
1393
+ from meltygui.core.rendering.render_dispatch import draw_any
1394
+ # Named by the code line's STABLE name - the marker's own name (raw,
1395
+ # line number stripped), never the full line-keyed path and never
1396
+ # draw_state.line: the name hashes into the unique ID, so a line
1397
+ # number here re-identified the window every time a rerun/edit
1398
+ # re-stamped the site's line.
1399
+ win_kwargs = dict(
1400
+ name=f"{'/'.join(map(_display_key, key_path))}"
1401
+ f"##lv::{ds.name}",
1402
+ mode=Modes.LIVE_WINDOW, closed=not (open_now or preview_show),
1403
+ open_requested=bool(preview_show),
1404
+ with_header=draw_header, disable_scroll=True, return_extras=True,
1405
+ # Anchor like a context menu: pinned to the marker, so the window
1406
+ # tracks it live and takes the pinned base's clamp - it rides the
1407
+ # code only as far as the editor window's edges instead of
1408
+ # chasing the marker off screen. (Swoosh style, alone: these
1409
+ # get the ribbon.) Both anchors are TOP_LEFT so the pinned base is
1410
+ # the marker's top-left - exactly what the window_pos offsets below
1411
+ # are measured from. hide_offscreen=False keeps it drawn once the
1412
+ # marker itself scrolls away: the window is what keeps it on screen.
1413
+ pin_to_clip=Pin.PARENT, anchor=Anchor.TOP_LEFT,
1414
+ parent_anchor=Anchor.TOP_LEFT, hide_offscreen=False,
1415
+ # Edits made anywhere in this window's subtree (params panel,
1416
+ # popup menus) should land on this site's `# [...]` comment -
1417
+ # set_anywhere reads the flag off the window's kwargs (walking
1418
+ # up from nested windows) and creates the binding there if the
1419
+ # comment hasn't set the param yet.
1420
+ preferred_source="code comment")
1421
+ # First creation: anchor the window to the LEFT of the editor's
1422
+ # window, always on top of the code, lifted clear of the editor
1423
+ # bottom (estimated height - the real one doesn't exist yet).
1424
+ # window_pos persists on the spawned window's draw_state
1425
+ # (parent-relative, so it tracks the editor window) - set once; user
1426
+ # drags it preserved after.
1427
+ if win_ds is None and "window_pos" not in comment_args:
1428
+ pos = _left_of_window_pos(
1429
+ editor_ds.abs_left if editor_ds is not None else None,
1430
+ x, marker_y=y)
1431
+ if pos is not None:
1432
+ win_kwargs["window_pos"] = pos
1433
+ elif win_ds is not None:
1434
+ # REUSE the tracked window draw_state: draw_any routes by VALUE
1435
+ # type and folds the render func into the unique, so a value
1436
+ # whose type changed between runs (None→tensor, int→str...) would
1437
+ # otherwise mint a fresh same-named ds - position/size/open
1438
+ # state gone, the old window orphaned in root_draw_states as a
1439
+ # duplicate-ID phantom. Pinning the ds keeps the window's
1440
+ # identity; the wrapper restamps _view_func per call, so the
1441
+ # body still re-routes by the value type.
1442
+ win_kwargs["draw_state"] = win_ds
1443
+ # NEW VALUE → repaint, exactly once per publish: the window is a
1444
+ # deferred nested root, so this call only restamps its kwargs;
1445
+ # the publish's own invalidation (_notify_watchers, during the
1446
+ # run) will arrive BEFORE this marker re-renders; the window
1447
+ # repaints on the new input, and a restamped kwargs is
1448
+ # not itself a dirty signal: the window then held its last
1449
+ # frame (voxels) or the run-start None ("No view for type")
1450
+ # until something else triggered it. Gate on identity: an
1451
+ # unchanged value never invalidates.
1452
+ if (getattr(win_ds, "_raw_input_value", _NO_VALUE) is not value
1453
+ and win_ds._tile_id is not None):
1454
+ Core.melty.cache.invalidate_up(win_ds._tile_id, force=True,
1455
+ max_depth=8)
1456
+ _c, _v, win_ds = draw_any(value, **(win_kwargs | comment_args))
1457
+ # Showstop: if the framework still handed back a different ds
1458
+ # (a path that ignores the pinned draw_state), close the replaced
1459
+ # one on the spot so it can never orphan.
1460
+ _prev_win = getattr(ds, "_lv_window_ds", None)
1461
+ if (_prev_win is not None and _prev_win is not win_ds
1462
+ and not _prev_win.closed):
1463
+ _prev_win.closed = True
1464
+ ds._lv_window_ds = win_ds
1465
+ # The menu usually opens on the WINDOW - stamp the site context there
1466
+ # too (the _parent chain isn't guaranteed to pass through this marker
1467
+ # after a root_draw_states re-dispatch).
1468
+ win_ds.live_root = live_root
1469
+ win_ds.live_key = ds.live_key
1470
+ # Same contract as the marker's (see current_live_root): the window
1471
+ # outlives this render - its replay re-splat and its panel's
1472
+ # set_anywhere must not trust a stamped tree a reparse may have
1473
+ # replaced since; they resolve through the host's held tree (memo
1474
+ # keyed by host identity, so it self-refreshes on every reparse).
1475
+ object.__setattr__(win_ds, "_lv_locator", _locator)
1476
+ from meltygui.core.windowing.window_visibility import override_state, user_window_closed
1477
+ _window_state = override_state(ds)
1478
+ if _window_state.pending_marker_closed is not None:
1479
+ user_window_closed(win_ds, _window_state.pending_marker_closed)
1480
+ _window_state.pending_marker_closed = None
1481
+ # The window's dispatch (Melty.draw, root_draw_states) re-reads
1482
+ # the CURRENT value for this key from the store, so a publish while
1483
+ # this marker is culled off-viewport still swaps the window's tensor
1484
+ # (fresh display, and the previous generation is released on the
1485
+ # spot instead of riding the stale kwargs until the marker next
1486
+ # renders).
1487
+ win_ds._lv_store_obj = store_obj
1488
+ win_ds._lv_key_path = key_path
1489
+ # Auto loop dims for the REPLAY path: the deferred root_draw_states
1490
+ # dispatch re-splats the site's raw `# [...]` comment over the stored
1491
+ # kwargs (meltygui.py, "Live-view comment re-splat"), and would clobber
1492
+ # the merged dim_names above with the comment's un-merged list. Stamp
1493
+ # the raw names and the value's dim count so the replay can redo the
1494
+ # same loop + dim<i> padding.
1495
+ win_ds._lv_auto_dims = _merge_auto
1496
+ win_ds._lv_ndim = _ndim
1497
+ # Window X-close detection: the header's close button runs DURING the
1498
+ # draw_any call above, so a caret-held preview closed this instant
1499
+ # shows as closed=True right after a window it passed closed=False.
1500
+ # Dismiss immediately - deterministic, no dependence on which order
1501
+ # the window and the editor re-render in adjacent frames.
1502
+ if (preview_show and cursor_inside and not open_now
1503
+ and win_ds.closed):
1504
+ Core.melty.clear_focus()
1505
+ ds._lv_cursor_dismissed = True
1506
+ ds._lv_cursor_in = False
1507
+ cursor_inside = False
1508
+ ds.invalidate()
1509
+ # The X on a caret-held preview parks the hint too (same
1510
+ # contract as the latched window above).
1511
+ _drop_captured_value(store_obj, key_path)
1512
+
1513
+ # Stamp whether THIS frame's window visibility is caret-held - the X-close
1514
+ # should only fire for a window the cursor preview was in.
1515
+ ds._lv_cursor_preview_shown = bool(preview_show and cursor_inside)
1516
+
1517
+ # A closed value window must not keep its last tensor (+ GL texture)
1518
+ # alive while the store streams on - release once per close (the flag
1519
+ # re-arms on the next show, when draw_any re-supplies the value).
1520
+ if win_ds is not None and win_ds.closed and not _show:
1521
+ if not getattr(win_ds, "_lv_released", False):
1522
+ win_ds._lv_released = True
1523
+ release_live_value(win_ds)
1524
+ # A closed window no longer earns its stack: drop the key's
1525
+ # accumulator and park the rerun hint in the store (the key
1526
+ # stays, the marker stays green), severing the marker/window
1527
+ # pins so nothing references the value any more.
1528
+ try:
1529
+ from meltygui.code.live_view import park_rerun_hint
1530
+ park_rerun_hint(store_obj, key_path)
1531
+ except Exception as e:
1532
+ print(f"live_view: park hint for {key_path} failed: {e!r}")
1533
+ try:
1534
+ from meltygui.core.runtime.gc_manager import release_cuda_cache_soon
1535
+ release_cuda_cache_soon(label="live window close")
1536
+ except Exception:
1537
+ pass
1538
+ elif win_ds is not None and getattr(win_ds, "_lv_released", False):
1539
+ win_ds._lv_released = False
1540
+
1541
+ # The marker itself must not outlive the value it was handed: the
1542
+ # framework stores this call's kwargs on the ds (_kwargs), and a marker
1543
+ # that isn't rendered again (scrolled off; culled; key pruned) would pin
1544
+ # the tensor until the next time it draws. The value was only ever
1545
+ # needed inside this body (the window got its own copy via draw_any).
1546
+ _kw = ds.__dict__.get("_kwargs")
1547
+ if isinstance(_kw, dict) and _kw.get("value") is not None:
1548
+ _kw["value"] = None
1549
+
1550
+ return False, None
1551
+
1552
+
1553
+ def draw_snapshot_overlay(x=0, y=0, w=0, h=0, draw_state=None, char_w=8.0,
1554
+ line_px=20.0, node=None, span=None, root=None,
1555
+ line_offset=0, jump_to=None, **kwargs):
1556
+ """Per-FUNCTION-scope overlay: anchor every captured value that has NO
1557
+ live_view token to anchor to — assignment keys published by an
1558
+ instrumented twin (live_instrument) and line-keyed sites in while/with
1559
+ bodies — with the same marker/window/watcher stack as the call tokens.
1560
+
1561
+ Registered for GeneralParse, so the walk calls it for many nodes; it acts
1562
+ only on def scopes (__cst__ FunctionDef). Deliberately NO identity checks
1563
+ against `root`: the code-host route hands the walk Bubbling proxy wrappers
1564
+ whose identities don't survive re-access, which is exactly how the first
1565
+ root-guarded version of this overlay silently never ran."""
1566
+ from meltygui.code.live_view import live_values_for
1567
+ from meltygui.code.live_view import watch
1568
+ from meltygui.editor.live_view_views import _NO_VALUE
1569
+ from meltygui.editor.live_view_views import _SWATCH_HOLE
1570
+ from meltygui.editor.live_view_views import _draw_marker_at
1571
+ from meltygui.editor.live_view_views import _draw_usage_labels
1572
+ from meltygui.editor.live_view_views import _inline_swatch_rgba
1573
+ from meltygui.editor.live_view_views import _inline_value_text
1574
+ from meltygui.editor.live_view_views import _is_funcdef_node
1575
+ from meltygui.editor.live_view_views import _key_anchor
1576
+ from meltygui.editor.live_view_views import _label_line_index
1577
+ from meltygui.editor.live_view_views import _line_in_selection
1578
+ from meltygui.editor.live_view_views import _marker_idle_skip
1579
+ from meltygui.editor.live_view_views import _node_owns_function
1580
+ from meltygui.editor.live_view_views import _parse_line_band
1581
+ from meltygui.editor.live_view_views import _scope_function
1582
+ from meltygui.editor.live_view_views import _snap_line_to_label
1583
+ from meltygui.editor.live_view_views import _snap_line_to_text
1584
+ from meltygui.editor.live_view_views import _stable_key_name
1585
+ from meltygui.editor.live_view_views import _store_key_names
1586
+ from meltygui.editor.live_view_views import _symbol_cols
1587
+ from meltygui.editor.live_view_views import _token_in_selection
1588
+ from meltygui.editor.live_view_views import is_volume
1589
+
1590
+ if not _is_funcdef_node(node) or span is None:
1591
+ return
1592
+ from meltygui.core.runtime.toggles import Toggles
1593
+ live_store = kwargs.get("live_store")
1594
+ if live_store is None and not Toggles.TextEditor.enable_live_view:
1595
+ return
1596
+ if live_store is not None:
1597
+ # PASSED-IN store (draw_text's live_store=, e.g. the stack trace
1598
+ # window): no global resolution at all - the caller computed the
1599
+ # values locally (live_view.frame_value_store), and this overlay
1600
+ # reads only what it was handed. Act on exactly the def the store
1601
+ # was built for: name match (rules out enclosing defs, whose spans
1602
+ # also contain the target's lines) + the store's def line inside
1603
+ # this node's span (rules out unrelated same-named defs).
1604
+ from meltygui.code.libcst_conversion import parse_def_name
1605
+ _def_line = getattr(live_store, "__def_line__", None)
1606
+ if (_def_line is None
1607
+ or parse_def_name(node) != getattr(live_store, "__name__", None)
1608
+ or not (span.start_line + line_offset <= _def_line
1609
+ <= getattr(span, "end_line", span.start_line)
1610
+ + line_offset)):
1611
+ return
1612
+ fn = live_store
1613
+ else:
1614
+ filename = (getattr(root, "file_path", None)
1615
+ or getattr(getattr(root, "address", None), "path", None)
1616
+ or getattr(jump_to, "path", None))
1617
+ if filename is None:
1618
+ return
1619
+ fn = _scope_function(str(filename), span.start_line + line_offset)
1620
+ if fn is None:
1621
+ return
1622
+ # A NESTED def resolves to its ENCLOSING function (closures attach
1623
+ # their captures to the outer store and are no module var, so the
1624
+ # nearest module-level def at or above the line wins). Its keys are
1625
+ # line-keyed and _key_anchor reads the line alone, so this node's
1626
+ # overlay would repaint every visible key of the outer store with
1627
+ # the SAME view as the outer node's overlay already used - the
1628
+ # red "ID ..." duplicate label over the outer body (09-02). Only the
1629
+ # node that IS the resolved function's own def draws it.
1630
+ if not _node_owns_function(node, fn, span, line_offset):
1631
+ return
1632
+ # Store-level registration before any markers exist: the first
1633
+ # instrumented run's brand-new keys invalidate this editor, the
1634
+ # overlay re-runs, and the markers materialize (closed - these
1635
+ # auto_open=False boxes wait for a click). Without it the first run
1636
+ # stays invisible until an instrument happens. (A passed-in store is
1637
+ # a static snapshot - nothing will ever publish to it, so no watch.)
1638
+ watch(fn, None, draw_state)
1639
+
1640
+ # Parse→buffer line bridge (dispatch passes it while a merge is in
1641
+ # flight): the given y is already mapped, so deriving origin from the
1642
+ # MAPPED start keeps origin == buffer line 1; each anchor then maps
1643
+ # individually - lines above an edit stay, lines below shift, lines
1644
+ # within the changed region skip the frame. Content lookups keep the
1645
+ # PARSE-space line: outside the changed region both texts hold the
1646
+ # identical line, by construction of the diff.
1647
+ _lmap = kwargs.get("line_map")
1648
+ _sl = _lmap(span.start_line) if _lmap else span.start_line
1649
+ if _sl is None:
1650
+ return
1651
+ origin_y = y - (_sl - 1) * line_px
1652
+ origin_x = x - getattr(span, "start_col", 0) * char_w
1653
+ _src = getattr(root, "source", "") or ""
1654
+ # Viewport cull bounds: the store can have a marker per binding in the
1655
+ # def (frame snapshots publish the whole scope), and the walk visits the
1656
+ # scope regardless of scroll - every off-screen marker skipped here is a
1657
+ # full render_func call saved per frame. Latched value windows persist
1658
+ # via root_draw_states without a marker, same as when the whole editor
1659
+ # scrolls away.
1660
+ _clip = getattr(draw_state, "abs_clip_rect", None)
1661
+ if live_store is not None:
1662
+ # PASSED-IN store (the stack trace view's panes): the pane is a
1663
+ # BOUNDED span whose tile bakes once and then blitted while the
1664
+ # PARENT scrolls - its own body doesn't re-run per scroll frame,
1665
+ # so a clip cull here baked only the then-visible band's markers
1666
+ # and they popped in/out as the scroll crossed rows (08-31;
1667
+ # re-confirmed 09-01: with the cull, draw_text's 5.6k-line pane
1668
+ # showed gutter markers only for the first ~180 lines). Render
1669
+ # every marker - so the per-key scan below must stay cheap: for
1670
+ # that pane it runs 5k+ regex scans every selection frame.
1671
+ _clip = None
1672
+ # Snap memo: the label/content relocation scans are O(def lines) per
1673
+ # STALE stamp - with frame snapshots holding a key per occurrence that's
1674
+ # hundreds of the scans per repaint if run hot. Snap results only
1675
+ # change when the text changes, so memoize per (stamp, label) against
1676
+ # the source OBJECT - identity is the content-free change signal (a
1677
+ # reparse builds a new string; edits-in-flight are the _lmap's job).
1678
+ # Per-EDITOR (this draw_state) because the same scope can be overlaid from
1679
+ # several editors at once (same def in two tiles), each with its own
1680
+ # source object - a shared memo would ping-pong between their sources
1681
+ # and rescan every stamp every frame. Raw-written like the other editor
1682
+ # memo caches (_anc_scroll_cache): @live's __setattr__ would run a
1683
+ # value != original_value compare on full value-carrying tuples.
1684
+ # The tuple also carries the module source's line split: splitting the
1685
+ # WHOLE file per FunctionDef per frame is ~2/3 of draw_text in profiling
1686
+ # - same invalidation (source object identity), so it rides the memo.
1687
+ _memo_ent = draw_state.__dict__.get("_lv_snap_memo")
1688
+ if _memo_ent is None or _memo_ent[0] is not _src or len(_memo_ent) < 3:
1689
+ _memo_ent = (_src, {}, _src.split("\n"))
1690
+ object.__setattr__(draw_state, "_lv_snap_memo", _memo_ent)
1691
+ _snap_memo = _memo_ent[1]
1692
+ source_lines = _memo_ent[2]
1693
+ # Exit-line washes: where the last instrumented run CAME OUT.
1694
+ # __live_return_line__ (stamped by live_view.twin_ret / the body-capture
1695
+ # profile hook) washes green; __live_error_line__ ((line, msg, text),
1696
+ # stamped
1697
+ # by live_instrument._stamp_error_line when the run raised) washes red
1698
+ # with the message in a wrapped box flush above the line, right-aligned -
1699
+ # the live-run twin of the editor's routed error markers. Both are absolute file coords, mapped
1700
+ # through the same parse→buffer bridge as the anchors; both cleared at
1701
+ # run start, so a rerun never shows the previous run's exit. A couple of
1702
+ # attribute accesses + at most two rects per frame.
1703
+ _exit_marks = []
1704
+ _ret_mark = getattr(fn, "__live_return_line__", None)
1705
+ if _ret_mark:
1706
+ _rline, _rtext = (_ret_mark if isinstance(_ret_mark, tuple)
1707
+ else (_ret_mark, None))
1708
+ _exit_marks.append((_rline, None, _rtext,
1709
+ (0.157, 0.824, 0.31, 0.16)))
1710
+ _err_mark = getattr(fn, "__live_error_line__", None)
1711
+ if _err_mark:
1712
+ _exit_marks.append((_err_mark[0], _err_mark[1],
1713
+ _err_mark[2] if len(_err_mark) > 2 else None,
1714
+ (0.824, 0.157, 0.157, 0.22)))
1715
+ for _ml_line, _ml_msg, _ml_text, _ml_col in _exit_marks:
1716
+ # Same follow-the-code snap as the markers: the stamp is run-time
1717
+ # coordinates, so if edits moved the statement, re-find it by its
1718
+ # stamped CONTENT inside the def before mapping to buffer space.
1719
+ _mk = ("exit", _ml_line, _ml_text)
1720
+ _rl = _snap_memo.get(_mk)
1721
+ if _rl is None:
1722
+ _rl = _snap_line_to_text(_ml_text, _ml_line - line_offset,
1723
+ source_lines,
1724
+ span.start_line, span.end_line)
1725
+ _snap_memo[_mk] = _rl
1726
+ _rlm = _lmap(_rl) if _lmap else _rl
1727
+ if _rlm is None or _rlm < 1:
1728
+ continue
1729
+ _ry = origin_y + (_rlm - 1) * line_px
1730
+ if _clip is not None and (_ry + line_px < _clip[1]
1731
+ or _ry > _clip[3]):
1732
+ continue
1733
+ _rdl = imgui.get_window_draw_list()
1734
+ _cw = getattr(draw_state, "content_width", 800.0)
1735
+ _rdl.add_rect_filled(
1736
+ origin_x - 4.0, _ry, origin_x + _cw, _ry + line_px,
1737
+ pack_color(*_ml_col))
1738
+ if _ml_msg:
1739
+ # Same treatment as the editor's parse-error box (text_editor's
1740
+ # draw_text): a wrapped, capped-width box sitting flush ABOVE the
1741
+ # line, right-aligned, so the box never covers the code.
1742
+ _pad_x, _pad_y, _margin = 6, 4, 6
1743
+ _bx1 = (_clip[2] if _clip is not None
1744
+ else origin_x + _cw) - _margin
1745
+ _max_w = min(420.0, max(80.0, (_bx1 - origin_x) - 2 * _pad_x))
1746
+ _ts = imgui.calc_text_size(_ml_msg, False, _max_w)
1747
+ _bx0 = _bx1 - (_ts.x + 2 * _pad_x)
1748
+ _by1 = _ry
1749
+ _by0 = _by1 - (_ts.y + 2 * _pad_y)
1750
+ if _clip is not None and _by0 < _clip[1] + _margin:
1751
+ _by0 = _ry + line_px # no room above - box below
1752
+ _by1 = _by0 + _ts.y + 2 * _pad_y
1753
+ _rdl.add_rect_filled(
1754
+ _bx0, _by0, _bx1, _by1,
1755
+ pack_color(0.275, 0.118, 0.157, 0.922), 4.0)
1756
+ _rdl.add_rect(
1757
+ _bx0, _by0, _bx1, _by1,
1758
+ pack_color(0.588, 0.235, 0.275, 1.0), 4.0)
1759
+ _save_cursor = imgui.get_cursor_screen_pos()
1760
+ imgui.set_cursor_screen_pos((_bx0 + _pad_x, _by0 + _pad_y))
1761
+ imgui.push_text_wrap_pos(imgui.get_cursor_pos_x() + _max_w)
1762
+ imgui.text_colored(_ml_msg, 1.0, 0.72, 0.68, 1.0)
1763
+ imgui.pop_text_wrap_pos()
1764
+ imgui.set_cursor_screen_pos(_save_cursor)
1765
+ _sot0 = time.perf_counter()
1766
+ _soc = [0, 0, 0, 0] # keys seen, culled(index+clip), idle-skipped, drawn
1767
+ _snap_vals = live_values_for(fn)
1768
+ _soc[0] = len(_snap_vals)
1769
+
1770
+ # Line-bucketed anchor index: resolving an anchor per key per frame is
1771
+ # O(store) - a frame-snapshotted big def holds THOUSANDS of keys, nearly
1772
+ # all off-viewport, and during an edit burst each also paid an _lmap
1773
+ # call (this was the measured 12-16ms/frame). Anchors only move when the
1774
+ # source or the key set changes, so resolve them ONCE into a sorted
1775
+ # (rel_line, key) list and bisect the visible band per frame. rel_line
1776
+ # is pre-_lmap (parse space); the 64-line slack on the band covers any
1777
+ # plausible in-buffer region shift until the reparse rebuilds _src (which
1778
+ # rebuilds the index - same identity signal as _snap_memo).
1779
+ _ik = (len(_snap_vals), line_offset)
1780
+ _ie = draw_state.__dict__.get("_lv_key_index")
1781
+ # The function is part of the identity; a def view's exec-fn runs
1782
+ # publish to a FRESH function object per run, so a stale index would
1783
+ # iterate the PREVIOUS run's key paths against the new values - every
1784
+ # lookup None, rendered as captured markers (the phantom "second set"),
1785
+ # whose None-routed windows then collide with the real ones. Weakref so
1786
+ # the index never pins a replaced run's function (id() of a dead object
1787
+ # could recycle).
1788
+ if (_ie is None or _ie[0] is not _src or _ie[1] != _ik
1789
+ or len(_ie) < 8 or _ie[4]() is not fn):
1790
+ # Append + ONE sort (C-speed): the first version insort-ed each key
1791
+ # (list.insert, O(n) memmove → O(n²) per rebuild), and a publish
1792
+ # storm - a stack-trace snapshot landing thousands of keys with
1793
+ # renders interleaved - meant a rebuild per render. That froze
1794
+ # the editor the moment a stack was published.
1795
+ _pairs = []
1796
+ _ilabels = getattr(fn, "__live_labels__", None) or {}
1797
+ _cols_memo = draw_state.__dict__.get("_lv_cols_memo")
1798
+ if _cols_memo is None or len(_cols_memo) > 200000:
1799
+ _cols_memo = {}
1800
+ object.__setattr__(draw_state, "_lv_cols_memo", _cols_memo)
1801
+ for key_path in _snap_vals:
1802
+ if not key_path or not isinstance(key_path[-1], str):
1803
+ continue
1804
+ if key_path[-1].split("#", 1)[0] == "live_view()":
1805
+ continue # anchored by its own call-token marker
1806
+ _a = _key_anchor(node, key_path, line_offset)
1807
+ if _a is None:
1808
+ continue
1809
+ _rl = _a[0]
1810
+ if _a[2] is None:
1811
+ tail = key_path[-1]
1812
+ _mk = ("label", tail)
1813
+ _snapped = _snap_memo.get(_mk)
1814
+ if _snapped is None:
1815
+ # Label→line index, built ONCE per source version: after
1816
+ # an edit shifts lines, EVERY stored label mismatches at
1817
+ # the next reparse, and the old per-key def-wide regex
1818
+ # scan cost keys × def-lines (2640 × 2900 ≈ 9.6 SECONDS,
1819
+ # the post-edit baseline). With the index each key is a
1820
+ # dict hit + nearest-line bisect.
1821
+ _lidx = _snap_memo.get(("lidx",))
1822
+ if _lidx is None:
1823
+ _lidx = _label_line_index(
1824
+ source_lines, span.start_line, span.end_line)
1825
+ _snap_memo[("lidx",)] = _lidx
1826
+ label = ((getattr(fn, "__live_labels__", None) or {})
1827
+ .get(key_path)
1828
+ or (tail.split("#", 1)[1] if "#" in tail else None))
1829
+ _snapped = _snap_line_to_label(
1830
+ label, _rl, source_lines,
1831
+ span.start_line, span.end_line, lidx=_lidx)
1832
+ _snap_memo[_mk] = _snapped
1833
+ _rl = _snapped
1834
+ _cols = _symbol_cols(_a, _rl, key_path, source_lines, _ilabels,
1835
+ memo=_cols_memo)
1836
+ if _cols is None:
1837
+ continue
1838
+ _pairs.append((_rl, key_path, (_rl, _cols[0], _cols[1])))
1839
+ _pairs.sort(key=lambda p: p[0])
1840
+ # Stable hash-free cache for the whole snapshot, built WITH the index
1841
+ # (same invalidation) - per-key naming was O(store) hash → O(store²)
1842
+ # per pass on frame-snapshot stores.
1843
+ # The resolved anchor rides with its key: _key_anchor is a
1844
+ # locals-walk per key, and re-running it per line per frame
1845
+ # was O(visible band) of dict descents for every repaint.
1846
+ _ie = (_src, _ik, [p[0] for p in _pairs], [p[1] for p in _pairs],
1847
+ weakref.ref(fn), None, [p[2] for p in _pairs],
1848
+ {p[1]: i for i, p in enumerate(_pairs)})
1849
+ object.__setattr__(draw_state, "_lv_key_index", _ie)
1850
+ _ilines, _ikeys, _ianchors = _ie[2], _ie[3], _ie[6]
1851
+ # Names from the SHARED generation-keyed store map - never the anchor
1852
+ # index's own cache; differently-stale name maps across consumers
1853
+ # minted duplicate value IDs (see _stable_key_names).
1854
+ _skey_names = _store_key_names(fn)
1855
+ if (_clip is not None
1856
+ and getattr(draw_state, "_lv_full_overlay_until", 0)
1857
+ <= Core.melty.frame_count):
1858
+ _blo, _bhi = _parse_line_band(_lmap, _clip, origin_y, line_px)
1859
+ _i0 = bisect.bisect_left(_ilines, _blo)
1860
+ _i1 = bisect.bisect_right(_ilines, _bhi)
1861
+ _cand = _ikeys[_i0:_i1]
1862
+ _cand_anchors = _ianchors[_i0:_i1]
1863
+ _soc[1] = len(_ikeys) - len(_cand)
1864
+ else:
1865
+ # Post-run forward pass renders EVERY key in the loop - the per-key
1866
+ # cull below still drops off-viewport keys unless their marker has
1867
+ # an OPEN window (rendered at the frozen anchor, which forwards the
1868
+ # actual value into the window's draw_any).
1869
+ _cand = _ikeys
1870
+ _cand_anchors = _ianchors
1871
+
1872
+ # Binding-value pills, built by the marker loop and painted by the
1873
+ # trailing-gap pass: (display 0, boundary buffer col, "=value",
1874
+ # symbol length) - usage-label pairs, one per captured target.
1875
+ #
1876
+ # IDLE-PASS MEMO (the no-clip pane below: every key, every frame). An
1877
+ # idle key's whole per-frame outcome is its gutter registration and
1878
+ # its pill, and both only change with the index, a publish, the
1879
+ # geometry or the focus - all in `_pk`. On a memo hit only the ACTIVE
1880
+ # keys run the per-key logic: keys non-idle last pass (hovered, open,
1881
+ # caret-inside - they must observe their state), keys on the mouse's
1882
+ # line (for hover could begin), keys on an exact single-line selection
1883
+ # (`_token_in_selection`); everything else replays. draw_text's
1884
+ # context-menu pane: 5,145 keys → 70 ms a selection frame before.
1885
+ _sel_lo, _sel_hi = kwargs.get("sel_lo"), kwargs.get("sel_hi")
1886
+ _col_shift = kwargs.get("col_shift", 0)
1887
+ _fc = Core.melty.frame_count
1888
+ _full_pass = getattr(draw_state, "_lv_full_overlay_until", 0) > _fc
1889
+ try:
1890
+ _pub_gen = vars(fn).get("__live_pub_gen__", 0)
1891
+ except TypeError:
1892
+ _pub_gen = 0
1893
+ # The line map is either None, the fold projection (a fresh lambda per
1894
+ # frame carrying the layout list in `_d2b` - see draw_text's
1895
+ # _get_fold_lm; the list only changes on a fold toggle, so it is the
1896
+ # identity the memo keys on) or an edit bridge (a change in flight - no
1897
+ # memo, every frame is a potential change).
1898
+ _layout = getattr(_lmap, "_d2b", None) if _lmap is not None else None
1899
+ _lm_key = getattr(_lmap, "_lv_key", None) if _lmap is not None else None
1900
+ _pk = (id(_ie), _pub_gen, origin_x, origin_y, line_px, char_w,
1901
+ _col_shift, Core.melty.text_focused_ds is draw_state,
1902
+ _lm_key)
1903
+ _record = (_clip is None and not _full_pass
1904
+ and (_lmap is None or _lm_key is not None))
1905
+ _pm = draw_state.__dict__.get("_lv_pass_memo")
1906
+ _replay = (_record and _pm is not None and len(_pm) >= 7
1907
+ and _pm[0] == _pk)
1908
+ _bind_pills = []
1909
+ if _replay:
1910
+ _idle_gutter, _pills, _nonidle = _pm[1], _pm[2], _pm[3]
1911
+ _static_gutter, _static_pills = _pm[5], _pm[6]
1912
+ _active = set(_nonidle)
1913
+
1914
+ def _mark_display_line(dl):
1915
+ # display line (1-based) → parse line: identity without folds,
1916
+ # else through the fold layout (0-based parse line per
1917
+ # display line); hidden / out-of-range lines have no keys.
1918
+ if _layout is None:
1919
+ rel = dl
1920
+ elif 0 <= dl - 1 < len(_layout):
1921
+ rel = _layout[dl - 1] + 1
1922
+ else:
1923
+ return
1924
+ _active.update(_ikeys[bisect.bisect_left(_ilines, rel):
1925
+ bisect.bisect_right(_ilines, rel)])
1926
+ if line_px:
1927
+ _mdl = int((imgui.get_io().mouse_pos.y - origin_y) / line_px) + 1
1928
+ for _dl in (_mdl - 1, _mdl, _mdl + 1):
1929
+ _mark_display_line(_dl)
1930
+ if (_sel_lo is not None and _sel_hi is not None
1931
+ and _sel_lo[0] == _sel_hi[0]):
1932
+ _mark_display_line(_sel_lo[0])
1933
+ # Replay the idle keys' gutter registration (same per-frame reset
1934
+ # the loop body / idle skip do) and their pills - from the memo's
1935
+ # STATIC structures, not a rebuild (5k entries a frame): the
1936
+ # gutter map is a ChainMap over a fresh front dict (the active
1937
+ # keys' markers get a filtered copy there so their own registration
1938
+ # can't duplicate the static entry); the pill list is the memo's
1939
+ # own object (copied only when an active key changes it - its
1940
+ # identity keys _stamp_and_paint's merge memo).
1941
+ _pos = _ie[7]
1942
+ if getattr(draw_state, "_lv_gutter_frame", None) != _fc:
1943
+ draw_state._lv_gutter_frame = _fc
1944
+ _front = {}
1945
+ for _k in _active:
1946
+ _ig = _idle_gutter.get(_k)
1947
+ if _ig is not None and _ig[0] not in _front:
1948
+ _front[_ig[0]] = [m for m in _static_gutter.get(_ig[0], ())
1949
+ if m is not _ig[1]]
1950
+ draw_state._lv_gutter_markers = collections.ChainMap(
1951
+ _front, _static_gutter)
1952
+ else:
1953
+ _gm = draw_state._lv_gutter_markers # another frame for first
1954
+ for _k, (_gl, _gmds) in _idle_gutter.items():
1955
+ if _k not in _active:
1956
+ _gm.setdefault(_gl, []).append(_gmds)
1957
+ _bind_pills = _static_pills
1958
+ for _k in _active:
1959
+ _p = _pills.get(_k)
1960
+ # A key non-idle when the memo was taken has no static pill
1961
+ # (it re-adds its own below); an idle one's is pulled so its
1962
+ # re-add can't duplicate it.
1963
+ if _p is not None and _k not in _nonidle:
1964
+ if _bind_pills is _static_pills:
1965
+ _bind_pills = list(_static_pills)
1966
+ try:
1967
+ _bind_pills.remove(_p)
1968
+ except ValueError:
1969
+ pass
1970
+ _loop = [(k, _ianchors[_pos[k]]) for k in _active if k in _pos]
1971
+ _soc[2] = len(_ikeys) - len(_loop)
1972
+ else:
1973
+ _idle_gutter, _pills = {}, {}
1974
+ _static_pills = None
1975
+ _loop = zip(_cand, _cand_anchors)
1976
+ _new_nonidle = set()
1977
+ _mutated = not _replay
1978
+ _created = 0
1979
+ _budget = int(Toggles.TextEditor.live_marker_create_budget)
1980
+ for key_path, anchor in _loop:
1981
+ value = _snap_vals.get(key_path)
1982
+ rel_line, start_col, end_col = anchor
1983
+ _ml = _lmap(rel_line) if _lmap else rel_line
1984
+ if _ml is None:
1985
+ continue # anchor inside the mid-edit region - skip a frame
1986
+ _my = origin_y + (_ml - 1) * line_px
1987
+ _frozen_pos = None
1988
+ if _clip is not None and (_my + line_px < _clip[1] or _my > _clip[3]):
1989
+ # Post-run forward pass (_lv_full_overlay_until): an off-viewport
1990
+ # key whose marker has an OPEN value window still renders once -
1991
+ # at the marker's LAST stamped location, NOT its true off-screen
1992
+ # spot (the pinned window would park at _pinned_base_y's screen-
1993
+ # bottom clamp and "disappear"). Everything else stays culled.
1994
+ _fmds = None
1995
+ if (getattr(draw_state, "_lv_full_overlay_until", 0)
1996
+ > Core.melty.frame_count):
1997
+ _mreg = getattr(draw_state, "_lv_marker_ds", None)
1998
+ _fmds = _mreg.get(
1999
+ f"lvs::{fn.__qualname__}"
2000
+ f"::{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}"
2001
+ ) if _mreg else None
2002
+ _fw = getattr(_fmds, "_lv_window_ds", None) if _fmds else None
2003
+ if _fw is None or _fw.closed:
2004
+ _fmds = None
2005
+ if _fmds is None:
2006
+ # TEMP diag: an off-viewport key skipped DURING an active
2007
+ # full pass means its open window didn't reflect the run's
2008
+ # value - name why (no marker ds for the expected key, or
2009
+ # its window closed/missing).
2010
+ if (getattr(draw_state, "_lv_full_overlay_until", 0)
2011
+ > Core.melty.frame_count):
2012
+ from meltygui.core.diagnostics.perf_trace import trace as _ptr
2013
+ _mreg2 = getattr(draw_state, "_lv_marker_ds", None) or {}
2014
+ _mk2 = (f"lvs::{fn.__qualname__}::"
2015
+ f"{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}")
2016
+ _ptr("lv full-pass skip", key=_mk2,
2017
+ have_marker=_mk2 in _mreg2,
2018
+ reg_keys=len(_mreg2))
2019
+ _soc[1] += 1
2020
+ continue # off-viewport - don't draw a marker for it
2021
+ _frozen_pos = (_fmds.abs_left, _fmds.abs_top)
2022
+ pad = 2.0
2023
+ # Selection containment: _ml is in buffer-space, cols are the
2024
+ # boxed symbol span - same test the call-token overlay does.
2025
+ cursor_inside = _token_in_selection(
2026
+ _ml, start_col, end_col, _sel_lo, _sel_hi)
2027
+ _snm = (f"lvs::{fn.__qualname__}"
2028
+ f"::{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}")
2029
+ _sao = (bool(Toggles.TextEditor.live_auto_open_volumes)
2030
+ and is_volume(value) and key_path not in
2031
+ (getattr(fn, "__frame_snapshot_keys__", None) or ()))
2032
+ # An inline-labeled marker (simple builtin value) paints NOTHING
2033
+ # itself: its pill is tracked here (_bind_pills) and painted raw
2034
+ # by _stamp_and_paint after the gap draw_text laid out. So an idle
2035
+ # one skips its @render_wrapper call like any other marker - once the
2036
+ # body has run at least once with the inline flag (the gutter glyph
2037
+ # and _lv_open button happen there; `_lv_inline` tracks it) - with
2038
+ # the body's full per-key watch replicated so a publish still
2039
+ # repaints the pill. Before this every visible captured binding of
2040
+ # a frame snapshot paid a full wrapper call per line: a big def
2041
+ # could draw_text (one binding on most lines) froze the editor.
2042
+ _btext = _inline_value_text(value)
2043
+ _pill = None
2044
+ if _btext is not None:
2045
+ _brgba = _inline_swatch_rgba(value)
2046
+ _pill = (_ml - 1, end_col + _col_shift,
2047
+ "=" + (_SWATCH_HOLE if _brgba is not None else "") + _btext,
2048
+ max(1, end_col - start_col),
2049
+ (_brgba, 1) if _brgba is not None else None)
2050
+ _mreg0 = draw_state.__dict__.get("_lv_marker_ds")
2051
+ _mds0 = _mreg0.get(_snm) if _mreg0 else None
2052
+ if (_frozen_pos is None
2053
+ and (_btext is None
2054
+ or (_mds0 is not None
2055
+ and getattr(_mds0, "_lv_inline", None) is True))
2056
+ and _marker_idle_skip(
2057
+ draw_state, _snm,
2058
+ origin_x + start_col * char_w - pad, _my - pad,
2059
+ max(1, end_col - start_col) * char_w + 2 * pad,
2060
+ line_px + 2 * pad, True, cursor_inside,
2061
+ fn, key_path, _ml - 1, _sao)):
2062
+ if _pill is not None:
2063
+ watch(fn, key_path, _mds0)
2064
+ if _bind_pills is _static_pills:
2065
+ _bind_pills = list(_static_pills)
2066
+ _bind_pills.append(_pill)
2067
+ if _record:
2068
+ if (_idle_gutter.get(key_path) != (_ml - 1, _mds0)
2069
+ or _pills.get(key_path, _NO_VALUE) != _pill):
2070
+ _mutated = True
2071
+ _idle_gutter[key_path] = (_ml - 1, _mds0)
2072
+ _pills[key_path] = _pill
2073
+ _soc[2] += 1
2074
+ continue
2075
+ if _mds0 is None and _frozen_pos is None and _budget > 0:
2076
+ # FIRST render of this marker (no draw_state yet) - a wrapper
2077
+ # call + DrawState + comment resolve each. A diff expand can
2078
+ # reveal thousands at once (15 s in one case, 09-01), so at
2079
+ # most `_budget` are created per pass; the rest stay pending
2080
+ # (nonidle state → active next pass) and the pills show now.
2081
+ if _created >= _budget:
2082
+ _new_nonidle.add(key_path)
2083
+ if _pill is not None:
2084
+ if _bind_pills is _static_pills:
2085
+ _bind_pills = list(_static_pills)
2086
+ _bind_pills.append(_pill)
2087
+ if _record:
2088
+ _mutated = True
2089
+ _pills[key_path] = _pill
2090
+ if _created == _budget:
2091
+ _created += 1
2092
+ from meltygui.core.windowing.glfw_utils import request_render
2093
+ draw_state.invalidate()
2094
+ request_render()
2095
+ continue
2096
+ _created += 1
2097
+ _soc[3] += 1
2098
+ _new_nonidle.add(key_path)
2099
+ if _record:
2100
+ if key_path in _idle_gutter or _pills.get(key_path, _NO_VALUE) != _pill:
2101
+ _mutated = True
2102
+ _idle_gutter.pop(key_path, None)
2103
+ _pills[key_path] = _pill
2104
+ # Frozen-anchor path: the marker draws at its previous position so
2105
+ # its pinned window doesn't chase the true off-screen coords.
2106
+ # Auto-open the VOLUMES (3-D tensors → orbiting voxel windows) only
2107
+ # when live_auto_open_volumes is enabled - off by default: with loop
2108
+ # accumulation stacking per-layer tensors into volumes, a run would
2109
+ # pop one window per captured tensor. Scalars/configs always stay
2110
+ # as click-to-open boxes so 19 locals don't bury the code.
2111
+ # Frame-snapshot keys (context-menu capture — tracked in
2112
+ # __frame_snapshot_keys__) never auto-open: opening a menu on a
2113
+ # widget must not spawn a window per captured tensor.
2114
+ # code_tree_node carries the scope dict: the marker reads the site's
2115
+ # comment dict from it and splats it 1:1 onto the value window's
2116
+ # draw_any (never onto the marker's own wrapper — show_bg=True with a
2117
+ # dark tint would paint an opaque bg over the very symbol it boxes).
2118
+ # The simple captured value renders EXACTLY like a usage label: the
2119
+ # gap opens right after the boxed target symbol and the line reads
2120
+ # `edited=False, new_text='...' = get_text(...)` - code repositions,
2121
+ # nothing is covered. Collected here, stamped + painted by
2122
+ # _draw_usage_labels below (sym_len drives the gap-derived ring).
2123
+ if _pill is not None:
2124
+ if _bind_pills is _static_pills:
2125
+ _bind_pills = list(_static_pills)
2126
+ _bind_pills.append(_pill)
2127
+ _draw_marker_at(
2128
+ draw_state,
2129
+ _frozen_pos if _frozen_pos is not None
2130
+ else (origin_x + start_col * char_w - pad, _my - pad),
2131
+ cursor_inside, (_ml, start_col, end_col),
2132
+ "/".join(map(str, key_path)), value=value,
2133
+ captured=True, store_obj=fn, key_path=key_path,
2134
+ width=max(1, end_col - start_col) * char_w + 2 * pad,
2135
+ height=line_px + 2 * pad,
2136
+ code_tree_node=node.get("locals") if isinstance(node, dict) else None,
2137
+ buffer_line=_ml - 1, name=_snm, auto_open=_sao,
2138
+ inline_values=True,
2139
+ in_selection=_line_in_selection(_ml, _sel_lo, _sel_hi),
2140
+ caret_line=kwargs.get("caret_line"), def_node=node)
2141
+ if _record:
2142
+ if _mutated or _replay and _new_nonidle != _nonidle:
2143
+ # Rebuild the static replay structures (a key changed state).
2144
+ _static_gutter = {}
2145
+ for _k, (_gl, _gmds) in _idle_gutter.items():
2146
+ _static_gutter.setdefault(_gl, []).append(_gmds)
2147
+ _static_pills = [p for _k, p in _pills.items()
2148
+ if p is not None and _k not in _new_nonidle]
2149
+ # `_lmap` (and what it was built from) rides along so the ids in
2150
+ # _pk stay pinned.
2151
+ object.__setattr__(draw_state, "_lv_pass_memo",
2152
+ (_pk, _idle_gutter, _pills, _new_nonidle, _lmap,
2153
+ _static_gutter, _static_pills))
2154
+ # Inline USAGE labels: `seq_len=384` inserted after every later
2155
+ # occurrence of a captured symbol - the code text is SHIFTED to make
2156
+ # room (display-time only - see live_usage + the positional-trail
2157
+ # machinery in draw_text's _window/_build_vcols).
2158
+ try:
2159
+ _draw_usage_labels(draw_state, fn, node, span, source_lines,
2160
+ _snap_vals, _ilines, _ikeys, origin_x, origin_y,
2161
+ char_w, line_px, _lmap, _clip,
2162
+ kwargs.get("col_shift", 0),
2163
+ binding_pills=_bind_pills)
2164
+ except Exception as e:
2165
+ print(f"live_view: usage labels failed: {e!r}", file=sys.stderr)
2166
+ # TEMP perf: one line per slow enough pass (keys=store size for this
2167
+ # def, culled=off-viewport, idle=fast-id skips, drawn=full wrapper calls).
2168
+ _soms = (time.perf_counter() - _sot0) * 1000.0
2169
+ if _soms >= 2.0:
2170
+ from meltygui.core.diagnostics.perf_trace import trace as _sotrace
2171
+ _sotrace("snapshot_overlay", fn=getattr(fn, "__qualname__", "?"),
2172
+ ms=round(_soms, 1), keys=_soc[0], culled=_soc[1],
2173
+ idle=_soc[2], drawn=_soc[3])
2174
+
2175
+
2176
+ @render_func(use_cache=False, show_bg=False, shadow=False, selectable=False,
2177
+ with_header=None, show_name=False)
2178
+ def draw_function_live(input_value, draw_state=None, unique=None,
2179
+ source_mode=None, column_edges=None, run_in_thread=True,
2180
+ **kwargs):
2181
+ """draw_function with transparent instrumentation, source side by side:
2182
+ the instrumented twin (live_instrument) runs AUTOMATICALLY — on first
2183
+ view, on every hotswap (id(fn.__code__) is the auto_run token, so a save
2184
+ in the editor compiles AND runs in one go), and on param edits — every
2185
+ assignment publishes one snapshot to the ORIGINAL function's store, and
2186
+ the editor column shows the ORIGINAL code with the snapshot overlay
2187
+ anchoring each captured value at its line.
2188
+
2189
+ `run_in_thread=True` (the default) runs the twin on a worker so a long
2190
+ pass (live_view_forward's full forward pass) never blocks the render
2191
+ loop. The live views need no special handling for this: each publis
2192
+ invalidates its watcher draw_states from the worker and wakes the loop
2193
+ (the terminal-reader pattern in live_view._notify_watchers), and all
2194
+ window rendering / voxel uploads happen on the GL thread next frame.
2195
+
2196
+ `source_mode` picks the source column's route: FILE_TREE (the default)
2197
+ is the text-only editor; NEW_CODE is the full code_file_io display —
2198
+ the draw_collection structured pane and the live-overlay text pane at
2199
+ the same time (live_view_forward uses it).
2200
+
2201
+ Ctrl+Enter over the lab presses the Run button (request_run) —
2202
+ overriding the global Ctrl+Enter's Pending-Saves-window flow while the
2203
+ mouse is here. The run itself already executes the latest source: the
2204
+ twin compiles from the pending in-memory text."""
2205
+ from meltygui.editor.live_view_views import _run_proxy
2206
+ from meltygui.editor.live_view_views import request_run
2207
+
2208
+ fn = input_value
2209
+ try:
2210
+ fn = inspect.unwrap(fn)
2211
+ except Exception:
2212
+ pass
2213
+
2214
+ if not callable(fn) or getattr(fn, "__code__", None) is None:
2215
+ imgui.text("draw_function_live: needs a plain function")
2216
+ return False, input_value
2217
+
2218
+ # Ctrl+Enter over the lab: press the Run button. registered BLOCKING
2219
+ # with a priority well above draw_main's non_blocking root handler
2220
+ # (512 - but any on-screen depth is < 512, so this always sorts first),
2221
+ # which stops the event chain at this view: the global re-save-all
2222
+ # window flow never fires while the mouse is here. This hook only
2223
+ # covers frames where the body renders; the blit-cached half is
2224
+ # draw_main's root BVH fallback → request_run (dual dispatch).
2225
+ if draw_state.on_action("ctrl_enter_down", priority_delta=1024):
2226
+ request_run(draw_state)
2227
+
2228
+ from meltygui.view.code_view import draw_function
2229
+ from meltygui.core.rendering.render_dispatch import draw_any
2230
+ from meltygui.core.layout.column_core import ColumnLayout
2231
+ from meltygui.core.layout.column_core import MIN_ROW_HEIGHT
2232
+ from meltygui.core.rendering.mode import Mode
2233
+ # Runner | source: a shared edge system (ColumnLayout): the divider is
2234
+ # a draggable line in the window's flat collision solve, and each column
2235
+ # manages its own height - no _columns_top capture to race with
2236
+ # cache-skipped siblings. Columns pin to the visible viewport so long
2237
+ # functions clip inside their cell instead of growing past the window
2238
+ # bottom. A NEW_CODE source column nests its own structured+text
2239
+ # ColumnLayout inside cell 1; the cell's edge dicts pass down with the
2240
+ # call (left_edge/right_edge, by reference - code_file_io forwards them
2241
+ # like jump_to), so the nested row's far edges ARE this row's divider and
2242
+ # right edge and can never drift apart from them.
2243
+ top_y = draw_state.abs_top + 0
2244
+ imgui.set_cursor_screen_pos((draw_state.abs_left, top_y))
2245
+ cols = ColumnLayout(draw_state, 2, column_edges=column_edges,
2246
+ column_widths=[244])
2247
+ clip = cols.clip if cols.clip is not None else draw_state.abs_clip_rect
2248
+ avail = (max(MIN_ROW_HEIGHT, clip[3] - cols.top) if clip is not None
2249
+ else 400.0)
2250
+ inner_h = avail - 2 * cols.padding
2251
+ with cols.cell(0, height=avail) as col_w:
2252
+ draw_function(_run_proxy(fn), height=inner_h, width=col_w, temp=True,
2253
+ name=f"{fn.__name__} runner", run_in_thread=run_in_thread, rounding=None)
2254
+ with cols.cell(1, height=avail) as col_w:
2255
+ draw_any(fn, mode=source_mode or Mode.FILE_TREE, height=inner_h,
2256
+ width=col_w, name=f"{fn.__name__} live source",
2257
+ left_edge=cols.edges[1], right_edge=cols.edges[2])
2258
+ cols.finish()
2259
+ return False, input_value
2260
+
2261
+
2262
+ @render_func
2263
+ def draw_source_preview(input_value=None, draw_state=None, preview: SourcePreviewState = None):
2264
+ from meltygui.view.text_view import draw_text
2265
+ from meltygui.core.melty import Melty
2266
+ from meltygui.code.new_converters import code_hosts_for
2267
+ if input_value is not None:
2268
+ preview.path, preview.line, preview.token = input_value
2269
+ if preview.path is None:
2270
+ return False, input_value
2271
+ path = Path(preview.path)
2272
+ text = Melty.read_code(path)
2273
+ if text is None:
2274
+ text = ''
2275
+ code_dict, dict_host = None, None
2276
+ if path.suffix == '.py':
2277
+ _, dict_host = code_hosts_for(path)
2278
+ code_dict = dict_host._held()
2279
+ from meltygui.editor.source_ui import _RowSpan
2280
+ _, _, pane = draw_text(text, name='source', editable=False, code_dict=code_dict, return_extras=True,
2281
+ jump_to=_RowSpan(0, path), show_header=False, width=draw_state.width,
2282
+ height=draw_state.height, show_widgets=True)
2283
+ if preview.line is not None and pane is not None and text:
2284
+ from meltygui.editor.text_editor import fold_project_jump
2285
+ lines = text.split('\n')
2286
+ row = max(0, min(int(preview.line) - 1, len(lines) - 1))
2287
+ offset = sum(len(line) + 1 for line in lines[:row])
2288
+ if preview.token and preview.token in lines[row]:
2289
+ offset += lines[row].index(preview.token)
2290
+ offset, row = fold_project_jump(pane, text, offset, row)
2291
+ pane.text_cursor_pos = offset
2292
+ pane.text_selection_start = pane.text_selection_end = offset
2293
+ pane.invalidate()
2294
+ preview.line = None
2295
+ from meltygui.core.windowing.glfw_utils import request_render
2296
+ request_render()
2297
+ return False, input_value
2298
+
2299
+
2300
+ def draw_pending_preview():
2301
+ import meltygui.editor.source_preview
2302
+
2303
+ request, meltygui.editor.source_preview._pending = meltygui.editor.source_preview._pending, None
2304
+ draw_source_preview(request, name='Source preview', closable=True,
2305
+ open_requested=request is not None, width=900, height=650)
2306
+
2307
+
2308
+ @render_func(use_cache=True, show_bg=True, shadow=True, selectable=False, temp=True,
2309
+ closable=True, melty_window=False, auto_resize=False, with_header=None,
2310
+ min_width=Toggles.UsagePicker.min_width, swoosh=False, min_height=Toggles.UsagePicker.min_height,
2311
+ enforce_max_height=True, # the content height cap holds mid-drag
2312
+ is_default_for=UsagePickerModel, tint=(0.071, 0.354, 0.511))
2313
+ def draw_usage_picker(input_value: UsagePickerModel, draw_state,
2314
+ row_height=Toggles.UsagePicker.row_height, row_gap=Toggles.UsagePicker.row_gap, tree_indent=16.0,
2315
+ code_font=Font.FONTAWESOME_MONO_19, collapsible=False,
2316
+ group_tint=None, change_kinds=None, **kwargs):
2317
+ """Paint the picker rows Code-tab style. Hover moves the highlight only
2318
+ while the pointer MOVES over the window (a resting pointer never steals
2319
+ the keyboard cursor); a click on a row sets `model.picked` for draw_text
2320
+ to consume. Returns (True, model) on a pick."""
2321
+ from meltygui.editor.usage_picker import paint_usage_rows
2322
+
2323
+ return paint_usage_rows(
2324
+ input_value, draw_state, row_height=row_height, row_gap=row_gap,
2325
+ tree_indent=tree_indent, code_font=code_font, collapsible=collapsible,
2326
+ group_tint=group_tint, change_kinds=change_kinds)
2327
+
2328
+
2329
+ @render_func(is_default_for=types.ModuleType, use_cache=True,
2330
+ show_bg=True, with_header=draw_header, with_footer=draw_footer)
2331
+ def draw_module(input_value: types.ModuleType, draw_state, **kwargs):
2332
+ imgui.text(f"Module: {input_value.__name__}")
2333
+
2334
+
2335
+ @render_func(is_default_for=(type), tint=(0.928, 0.836, 0.655, 0.308), use_cache=True,
2336
+ header_single_line=True, show_name=True, temp=True, is_tree=False, shadow=False,
2337
+ show_bg=True, with_header=draw_header)
2338
+ def draw_type_name(input_value, **kwargs):
2339
+ try:
2340
+ if isinstance(input_value, str):
2341
+ imgui.text(f"{input_value}")
2342
+
2343
+ else:
2344
+ imgui.text(f"{input_value.__name__}")
2345
+ except Exception as e:
2346
+ imgui.text(f"Error displaying type: {e}")
2347
+
2348
+
2349
+ @render_func(use_cache=True, is_default_for=SymbolUsage)
2350
+ def draw_symbol_usage(input_value):
2351
+ imgui.text(str(input_value))
2352
+
2353
+
2354
+ @render_func(is_default_for=(property))
2355
+ def draw_property(input_value: property, draw_state, **kwargs):
2356
+ imgui.text_colored(f"Property: {input_value.fget.__name__}", 1.0, 0.5, 0.0, 1.0)
2357
+
2358
+
2359
+ @render_func(show_bg=True, align_header=False, use_cache=True, shadow=False,
2360
+ with_header=draw_header)
2361
+ def draw_type(input_value: type, **kwargs):
2362
+ from meltygui.view.collection_view import draw_collection
2363
+
2364
+ try:
2365
+ class_vars = {**{k: getattr(input_value, k) for k in vars(input_value)}}
2366
+
2367
+ changed, new_dict = draw_collection(class_vars, real_type=input_value, disable_scroll=True,
2368
+ name=f"Class: {input_value.__name__}")
2369
+
2370
+ if changed:
2371
+ for k, v in new_dict.items():
2372
+ if k.startswith("_"):
2373
+ continue
2374
+ try:
2375
+ imgui.text(f"Setting attribute {k} to value {v} on class {input_value.__name__}")
2376
+ setattr(input_value, k, v)
2377
+ except Exception as e:
2378
+ imgui.text(f"Error setting attribute {k} on class {input_value.__name__}: {e}")
2379
+ except Exception as e:
2380
+ imgui.text(f"Error rendering type {input_value}: {e}")
2381
+
2382
+
2383
+ @render_func(is_default_for=UsageRef, use_cache=True, shadow=True, z_offset=2, show_bg=True, with_header=draw_header,
2384
+ is_tree=True, tint=(0.11, 0.1, 0.16))
2385
+ def draw_usage(input_value: UsageRef):
2386
+ imgui.text(
2387
+ f"{input_value.path} {input_value.line}:{input_value.column} {input_value.scope} {input_value.module_name}")
2388
+
2389
+ return False, input_value
2390
+
2391
+
2392
+ @render_func(is_default_for=(Comment), shadow=False, header_same_line=True, initial={"expanded": False}, icon="",
2393
+ is_tree=True, show_name=False, indent_size=8, selectable=False, use_cache=False,
2394
+ tint=(0.137, 0.683, 0.299, 0.708),
2395
+ show_bg=False, with_header=draw_header, temp=False, expanded_mode=ExpandMode.MANUAL)
2396
+ def draw_comment(input_value: Comment, draw_state, style_manager, cursor_hover=False, font=Font.JETBRAINS_MONO_13):
2397
+ changed, value = False, input_value
2398
+
2399
+ imgui.dummy(0, 0)
2400
+ depth = max(0.3, Core.melty.bg_depth)
2401
+ depth_scale = 0.047
2402
+
2403
+ # [tint=(0.883, 0.712, 0.206, 0.34)]
2404
+ name_style = {
2405
+ 'value': -0.420, 'saturation': 1.06,
2406
+ 'alpha': 0.047, 'max_value': 0.704,
2407
+ 'depth_factor': 0.34
2408
+ }
2409
+ depth_intensity = float(depth) * depth_scale
2410
+ name_style['value'] = depth_intensity * name_style['depth_factor'] + name_style['value']
2411
+
2412
+ # [tint=(0.767, 0.379, 0.379)]
2413
+ alpha = 1.02
2414
+
2415
+ sat_depth_factor = 0.0
2416
+ sat_depth_offset = 0.188
2417
+ sat_shift = float(depth + sat_depth_offset) * sat_depth_factor
2418
+ name_style['saturation'] = name_style['saturation'] + sat_shift
2419
+
2420
+ name_color = style_manager.make_color_style_value(input=name_style)
2421
+
2422
+ # A grouped multi-line comment is a '\n'-joined run of '# ' lines; strip the
2423
+ # '#'/'# ' prefix from EACH line so it displays as clean prose, not just the
2424
+ # first (str[2:] would leave a stray '#' on every continuation line).
2425
+ def _strip_hash(ln):
2426
+ return ln[2:] if ln.startswith("# ") else (ln[1:] if ln.startswith("#") else ln)
2427
+
2428
+ display = "\n".join(_strip_hash(ln) for ln in str(input_value).split("\n"))
2429
+
2430
+ # The framework header (is_tree=True) owns the expand/collapse button.
2431
+ # ExpandMode.MANUAL keeps this body visible while collapsed, with the
2432
+ # first line standing in for the whole comment.
2433
+ if "\n" in display and not draw_state.expanded:
2434
+ # Collapsed: one line truncated to the available width - never let it
2435
+ # spill onto a second row.
2436
+ flat = " ".join(display.split("\n"))
2437
+ avail = draw_state.abs_left + draw_state.width - imgui.get_cursor_screen_pos().x
2438
+ if imgui.calc_text_size(flat).x > avail:
2439
+ lo, hi = 0, len(flat)
2440
+ while lo < hi:
2441
+ mid = (lo + hi + 1) // 2
2442
+ if imgui.calc_text_size(flat[:mid]).x <= avail:
2443
+ lo = mid
2444
+ else:
2445
+ hi = mid - 1
2446
+ flat = flat[:lo].rstrip()
2447
+ imgui.push_style_color(imgui.COLOR_TEXT, *name_color[:3], alpha)
2448
+ imgui.text(flat)
2449
+ imgui.pop_style_color()
2450
+ else:
2451
+ imgui.push_text_wrap_pos(draw_state.abs_left + draw_state.width)
2452
+ imgui.push_style_color(imgui.COLOR_TEXT, *name_color[:3], alpha)
2453
+ imgui.text_wrapped(display)
2454
+ imgui.pop_style_color()
2455
+ imgui.pop_text_wrap_pos()
2456
+
2457
+ if changed:
2458
+ return True, value
2459
+ return False, input_value
2460
+
2461
+
2462
+ @render_func(is_default_for=(Parameter), wraps=render_func, with_header=draw_header)
2463
+ def draw_parameter(input_value):
2464
+ from meltygui.core.rendering.render_dispatch import draw_any
2465
+
2466
+ parameter_default = input_value.default
2467
+ if parameter_default is inspect.Parameter.empty:
2468
+ imgui.same_line()
2469
+ imgui.text("<No Default>")
2470
+ else:
2471
+ return draw_any(parameter_default, show_name=False, show_add_delete=False)
2472
+
2473
+
2474
+ @render_func(is_default_for=(types.FunctionType, types.MethodType), z_offset=0, use_cache=True,
2475
+ show_add_delete=False, selectable=False, show_bg=True,
2476
+ parent_show_add_delete=False, is_tree=False, show_name=False, with_header=draw_header)
2477
+ def draw_function(input_value, name, draw_state, unique, auto_run=None, wrap=False,
2478
+ show_run_button=True, run_in_thread=False, result_fade_frames=None,
2479
+ **kwargs):
2480
+ """`auto_run`: opt-in compile-and-run — pass any comparable version token
2481
+ (e.g. id(fn.__code__)); the function runs whenever the token CHANGES or a
2482
+ parameter is edited, no button click. The token is stored before running
2483
+ so a throwing function doesn't retry every frame. `show_run_button=False`
2484
+ drops the named run button (the streamlined live-lab look).
2485
+
2486
+
2487
+ `run_in_thread=True` runs the function on a daemon worker instead of
2488
+ blocking the render loop (long model passes). Single-flight: a click or
2489
+ auto_run while a run is in flight is skipped — but the auto_run token is
2490
+ only latched when a run actually starts, so a hotswap landing mid-run
2491
+ re-fires on completion instead of being lost. The worker only writes
2492
+ draw_state attrs and uses the cross-thread invalidation path (the
2493
+ Background.run completion pattern); all rendering stays on the GL thread."""
2494
+ from meltygui.core.windowing.glfw_utils import request_render
2495
+ from meltygui.utils.render_utils import print_colored_traceback
2496
+ from meltygui.view.collection_view import draw_collection
2497
+ from meltygui.view.control_view import button
2498
+ from meltygui.core.rendering.render_dispatch import _RUN_RESULT_HOLDERS
2499
+ from meltygui.core.rendering.render_dispatch import _format_run_error
2500
+ from meltygui.core.rendering.render_dispatch import _respond_to_cuda_oom
2501
+ from meltygui.core.rendering.render_dispatch import draw_any
2502
+ from meltygui.core.rendering.render_dispatch import is_run_busy
2503
+ from meltygui.core.rendering.render_dispatch import run_busy_begin
2504
+ from meltygui.core.rendering.render_dispatch import run_busy_end
2505
+
2506
+ if not callable(input_value):
2507
+ imgui.text("Not a callable function")
2508
+ return False, input_value
2509
+ params_edited = False
2510
+ try:
2511
+ signature = inspect.signature(input_value)
2512
+ params = signature.parameters
2513
+ if len(draw_state.params) != len(params):
2514
+ param_dict = {}
2515
+
2516
+ for name, param in params.items():
2517
+ if name == 'kwargs':
2518
+ continue
2519
+ if param.default is not inspect.Parameter.empty:
2520
+ param_dict[name] = param.default
2521
+ else:
2522
+ param_type = param.annotation
2523
+ default_value = param.default
2524
+ if default_value is not inspect.Parameter.empty:
2525
+ param_dict[name] = default_value
2526
+ else:
2527
+ if name in Core.melty.global_attrs:
2528
+ param_dict[name] = Core.melty.global_attrs[name]
2529
+
2530
+ draw_state.params = param_dict
2531
+ if len(draw_state.params) > 0:
2532
+ from meltygui.core.rendering.mode import Mode
2533
+ changed, new_val = draw_collection(draw_state.params, name="Parameters", initial={"expanded": True},
2534
+ use_cache=True,
2535
+ mode=Mode.FUNCTION_PARAMS,
2536
+ show_add_delete=False, shadow=True, z_offset=1,
2537
+ parent_show_add_delete=False,
2538
+ horizontal=False, wrap=wrap,
2539
+ child_kwargs={"max_width": 397, "shadow": False,
2540
+ "show_bg": False, "use_cache": True, "z_offset": 0.0
2541
+ }, tint=(0.34, 0.40, 0.45))
2542
+ if changed:
2543
+ draw_state.params = new_val
2544
+ params_edited = True
2545
+ except Exception as e:
2546
+ imgui.text(f"Error inspecting function parameters: {e}")
2547
+ draw_state.params = {}
2548
+
2549
+ sees_this = 0
2550
+
2551
+ # A pkl written while the old misc-latch was active reloads as busy -
2552
+ # scrub it; the latch is _RUN_BUSY now (never persisted).
2553
+ draw_state.misc.pop("_run_busy", None)
2554
+
2555
+ def _run():
2556
+ if run_in_thread:
2557
+ if not run_busy_begin(draw_state):
2558
+ return # single-flight: one run per runner at a time
2559
+ # Snapshot params so a mid-run edit can't give the worker a
2560
+ # half-updated dict; never run a @render_func WRAPPER off-thread
2561
+ # (it mutates process-global Melty stacks - see run_in_background),
2562
+ # otherwise take the bare function.
2563
+ params = dict(draw_state.params)
2564
+ fn = getattr(input_value, "__wrapped__", input_value)
2565
+
2566
+ def _worker():
2567
+ try:
2568
+ draw_state.result = fn(**params)
2569
+ _RUN_RESULT_HOLDERS.add(draw_state)
2570
+ draw_state.misc["_result_frame"] = Melty.frame_count
2571
+ draw_state.misc.pop("_run_error", None)
2572
+ except Exception as e:
2573
+ draw_state.misc["_run_error"] = _format_run_error(e)
2574
+ print(f"Error calling function '{input_value.__name__}': {e}")
2575
+ print_colored_traceback(*sys.exc_info())
2576
+ _respond_to_cuda_oom(e, input_value.__name__)
2577
+ finally:
2578
+ run_busy_end(draw_state)
2579
+ # invalidate_up_current reads the live render stack - only
2580
+ # valid mid-render on the GL thread. Off-thread completion
2581
+ # marks the runner's subtree by tile id (force: the result
2582
+ # pane is a cached descendant) and wakes the loop; the
2583
+ # validation itself happens on the render thread.
2584
+ from meltygui.core.cache.invalidation_tracker import Note
2585
+ Melty.cache.invalidate_up(
2586
+ draw_state._tile_id, force=True,
2587
+ note=Note(name="draw_function run complete",
2588
+ reason=f"func={input_value.__name__}",
2589
+ tint=(0, 0, 1)))
2590
+ request_render()
2591
+
2592
+ threading.Thread(target=_worker, daemon=True,
2593
+ name=f"draw_function:{input_value.__name__}").start()
2594
+ Core.melty.cache.invalidate_up_current(force=True) # show spinner now
2595
+ request_render()
2596
+ return
2597
+ try:
2598
+ draw_state.result = input_value(**draw_state.params)
2599
+ _RUN_RESULT_HOLDERS.add(draw_state)
2600
+ draw_state.misc["_result_frame"] = Melty.frame_count
2601
+ draw_state.misc.pop("_run_error", None)
2602
+ Core.melty.cache.invalidate_up_current(force=True)
2603
+ except Exception as e:
2604
+ # Surfaced in the UI (red text where the result goes), anchored at
2605
+ # the deepest frame in PROJECT code - the line the user can fix.
2606
+ draw_state.misc["_run_error"] = _format_run_error(e)
2607
+ Core.melty.cache.invalidate_up_current(force=True)
2608
+ print(f"Error calling function '{input_value.__name__}': {e}")
2609
+ print_colored_traceback(*sys.exc_info())
2610
+ _respond_to_cuda_oom(e, input_value.__name__)
2611
+
2612
+ busy = run_in_thread and is_run_busy(draw_state)
2613
+ # One-offed run request (draw_function_live's Ctrl+Enter - the
2614
+ # hotkey IS the Run button): always popped, so it can't replay on later
2615
+ # frames; dropped while busy, matching a click during a threaded run.
2616
+ if draw_state.misc.pop("_run_requested", None) and not busy:
2617
+ _run()
2618
+ if auto_run is not None and not busy and (
2619
+ params_edited or draw_state.misc.get("_auto_run_ver") != auto_run):
2620
+ draw_state.misc["_auto_run_ver"] = auto_run
2621
+ _run()
2622
+
2623
+ imgui.new_line()
2624
+ if show_run_button and button(f"Run {input_value.__name__}()##{unique}", icon=kwargs.get("icon", ""), height=35,
2625
+ bg_offset=0, tint=(0.499, 0.844, 0.488, 0.32), shadow=True, rounding=None)[0]:
2626
+ _run()
2627
+
2628
+ # Fading result (result_fade_frames): the check mark + result text hold,
2629
+ # then fade out and clear - modeled on code_file_io's recompile_status
2630
+ # (frame-based fade, invalidate + request_render pump while fading).
2631
+ # None (default) keeps the persistent result pane.
2632
+ result_fade = 1.0
2633
+ if result_fade_frames and draw_state.result is not None:
2634
+ shown_for = float(Melty.frame_count
2635
+ - draw_state.misc.get("_result_frame", Melty.frame_count))
2636
+ result_fade = min(1.0, max(0.0, 2.0 - shown_for / float(result_fade_frames)))
2637
+ if result_fade > 0.01:
2638
+ draw_state.invalidate()
2639
+ request_render()
2640
+ else:
2641
+ draw_state.result = None
2642
+ draw_state.misc.pop("_result_frame", None)
2643
+
2644
+ if run_in_thread and is_run_busy(draw_state):
2645
+ imgui.same_line(spacing=10)
2646
+ imgui.text_colored("", 0.55, 0.75, 1.0, 1.0)
2647
+ imgui.new_line()
2648
+ elif draw_state.result is not None:
2649
+ imgui.same_line(spacing=10)
2650
+ imgui.text_colored("", 0.55, 0.75, 1.0, result_fade)
2651
+ if result_fade_frames and isinstance(draw_state.result, str):
2652
+ # Fading summary rides the button row inline, so hiding it never
2653
+ # reflows the content below (the row holds its height).
2654
+ imgui.same_line(spacing=8)
2655
+ imgui.text_colored(draw_state.result, 0.55, 0.75, 1.0, result_fade)
2656
+ imgui.new_line()
2657
+ else:
2658
+ imgui.same_line()
2659
+ imgui.text_colored(" ", 0.55, 0.75, 1.0, 1.0)
2660
+ imgui.new_line()
2661
+
2662
+ run_error = draw_state.misc.get("_run_error")
2663
+ if run_error:
2664
+ imgui.push_text_wrap_pos(0.0)
2665
+ imgui.text_colored(run_error, 1.0, 0.45, 0.40, 1.0)
2666
+ imgui.pop_text_wrap_pos()
2667
+
2668
+ if draw_state.result is not None and not (result_fade_frames
2669
+ and isinstance(draw_state.result, str)):
2670
+ imgui.text_colored(" Result", *(1.0, 1.0, 1.0, 0.5))
2671
+ imgui.set_cursor_pos_y(imgui.get_cursor_pos_y() - 15)
2672
+ draw_any(draw_state.result, name="Result", header_same_line=True, show_header=False,
2673
+ show_add_delete=False)
2674
+
2675
+ # pop_style_var(3)
2676
+
2677
+ return False, input_value