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,1083 @@
1
+ """
2
+ RenderHost — a dict that drives a stateful wrapper and holds its editable value.
3
+
4
+ A `RenderHost` is just a `dict`. It is NOT a view function. Instead it WRAPS a
5
+ stateful func (`code_file_io`, `convert_in_and_out_value`) that follows the shape
6
+
7
+ stateful_work() → view_func(value) → stateful_work()
8
+
9
+ (load → edit → save for code_file_io; chain_in → edit → chain_out for convert). The
10
+ host hands that wrapper its OWN private view_func; whatever the wrapper passes in —
11
+ a source STRING from code_file_io, the parsed TREE from convert_in_and_out_value —
12
+ is materialized into the host's dict (held under `value_key`, type intact), rendered,
13
+ and any edit flows back out the same channel. So the host "looks like a dict with a
14
+ single value inside," and you use it like that value.
15
+
16
+ Chaining is plain data-flow through `input_value`: a proxy whose `input_value` is
17
+ another `RenderHost` reads that upstream proxy's held value ("used like a string").
18
+ Edits flow back upstream — `draw()` writes the wrapper's output onto the upstream
19
+ proxy, which goes dirty and saves one frame later. That one-frame lag is the only
20
+ cost of decoupling the proxies into independent `draw()` calls.
21
+
22
+ string_proxy = RenderHost(wrapper=code_file_io, input_value=InCode)
23
+ dict_proxy = RenderHost(wrapper=convert_in_and_out_value, input_value=string_proxy,
24
+ child_kwargs={chain_in, chain_out, route})
25
+
26
+ `draw_main` calls `draw()` on each registered host every frame (upstream first), so
27
+ each proxy renders into its own little Melty window; and because it's a plain dict,
28
+ `draw_collection(proxy)` renders it anywhere too. Nested edits bubble (see
29
+ bubbling.py) so a deep change to the held tree marks the host without a manual touch.
30
+ """
31
+
32
+ import sys
33
+ import threading
34
+ import time
35
+
36
+ from meltygui.core.melty import Melty
37
+ from meltygui.core.diagnostics.notifications import notify
38
+ from meltygui.core.rendering.render_funcs import RenderFuncs
39
+ from meltygui.core.windowing.glfw_utils import request_render
40
+ from meltygui.core.windowing.glfw_utils import print_stack_trace
41
+ from meltygui.core.diagnostics.perf_trace import trace as _ptrace
42
+ from meltygui.core.diagnostics.perf_trace import trace_rl as _ptrace_rl
43
+ from meltygui.core.conversion.bubbling import install_bubbling
44
+ from meltygui.core.conversion.bubbling import _reinstall_children
45
+ from meltygui.core.conversion.bubbling import _DeepAttrMixin
46
+ from meltygui.core.core_render import render_func
47
+ from meltygui.core.rendering.core_decoration import defaults
48
+ from meltygui.core.rendering.core_decoration import Core
49
+ from meltygui.core.cache.invalidation_tracker import Note
50
+
51
+
52
+ _UNSET = object()
53
+
54
+
55
+ @defaults()
56
+ class RenderHost(_DeepAttrMixin, dict):
57
+ """A dict that drives a stateful `wrapper` and holds the value it edits.
58
+
59
+ Args:
60
+ io_function: the stateful func to drive (code_file_io / convert_in_and_out_value).
61
+ `None` → a plain data-bag: `draw()` just renders the dict. This can contain UI but does not need to
62
+ Any UI drawn as part of this method will end up in a settings panel, not the main UI
63
+ input_value: what the wrapper operates on — a raw value (a class, a path), or
64
+ an upstream `RenderHost` (resolved to its held value each frame).
65
+ child_kwargs: extra kwargs for the wrapper (e.g. convert's chain_in/out + route).
66
+ settings_renderer: This is draw inside the settings panel, and entirely optional
67
+
68
+ (default: `draw_any`, which routes str → editor, dict → collection).
69
+ value_key: the dict key the held value lives under (type intact).
70
+ standalone: register with Melty so draw_main calls `draw()` (its own window)."""
71
+
72
+ # The host currently rendering, so render_host_view can find it without threading
73
+ # it through cache-hashed kwargs.
74
+ _active = []
75
+
76
+ # Log every change the proxy triggers (dict-dirty + outbound edits) with a caller
77
+ # trail - for diagnosing spurious saves. `RenderHost.debug_changes = False` silences.
78
+ debug_changes = False
79
+
80
+ # Class-level fallbacks for the consumer-notify debounce fields, so host
81
+ # instances born before a hotswap of this class still resolve them.
82
+ _local_edit_time = 0.0
83
+ _pending_notify_name = None
84
+ _notify_timer = None
85
+
86
+ def __init__(self, io_function=None, *args, input_value=None, child_kwargs=None,
87
+ settings_renderer=None, name=None, hidden=False, window=True, standalone=True,
88
+ value_key="value", evictable=False, **extra):
89
+ super().__init__(*args)
90
+ self.io_function = io_function
91
+ # `is None` (not falsy): an empty host-dict is a valid input_value.
92
+ self.input_value = self if input_value is None else input_value
93
+ # Wrapper options (chain_in/out, route, ...). `extra` kwargs fold in too, so you
94
+ # can pass them directly instead of nesting a child_kwargs dict.
95
+ self.child_kwargs = {**(child_kwargs or {}), **extra}
96
+ self.settings_renderer = settings_renderer
97
+ self.name = name or getattr(io_function, "__name__", None) or "RenderHost"
98
+ self.hidden = hidden
99
+ self.window = window
100
+ self.standalone = standalone
101
+ # evictable → the idle sweep (RenderHost.sweep) may DEREGISTER this host
102
+ # from Melty.render_hosts when ALL its consumer windows close - it stays
103
+ # in the code-host cache and re-registers on reopen (notify_on_change →
104
+ # register). Module-level singleton proxies (files_proxy, claude_proxy, the
105
+ # playground demos) default this False so they're never swept; only
106
+ # code_hosts_for opts its short-lived cache pairs in.
107
+ self.evictable = evictable
108
+ self._birth_frame = Melty.frame_count # idle-sweep birth grace
109
+ # The held value lives under this key, type intact - so a GeneralParse stays a
110
+ # GeneralParse for chain_out, and the proxy "looks like a dict with one value".
111
+ self.value_key = value_key
112
+
113
+ self._external_change = False # dict (or a nested value) mutated since last render
114
+ self._draw_state = None # render_host_view's draw_state (the window envelope)
115
+ self._wrapper_draw_state = None # the WRAPPER's draw_state (code_file_io / convert) via
116
+ # return_extras - so a value edit can invalidate IT and
117
+ # the wrapper actually re-runs to process the change.
118
+ self._last_resolved = _UNSET # last resolved input - drives the wrapper's external_change
119
+ self._pending_external = False # input changed (detected in draw()) → external_change for the wrapper
120
+ self._awaiting_inbound = False # an upstream edit happened; a remote derived result (e.g.
121
+ # convert's BACKGROUND chain_in) is in flight - materialize it
122
+ # when it lands, not just on the pulse frame.
123
+ # Frame stamps for event ordering. The held value (a LOCAL edit) is set
124
+ # immediately; the wrapper's derived result (a re-parse) is asynchronous and may be
125
+ # based on a source that lags the last local edit. We only accept an inbound
126
+ # result when the source it's based on is at least as NEW as the last local edit
127
+ # - otherwise the newest event (the local edit) wins. See _internal_view_func.
128
+ self._input_change_frame = 0 # frame the resolved INPUT (source) last changed
129
+ self._local_edit_frame = 0 # frame the held value was last LOCALLY edited
130
+ self._local_edit_time = 0.0 # wall clock of the last local edit - drives the
131
+ # consumer-notify debounce (typing_hold)
132
+ self._pending_notify_name = None # deferred _notify_consumers note name
133
+ self._notify_timer = None # armed flush timer for the deferred notify
134
+ self._last_return = None
135
+ self._registered = False
136
+ # Draw_states that READ this host's value from OUTSIDE its own draw loop (e.g.
137
+ # draw_input_tab renders host.deep.parameters() inside the context menu). The
138
+ # host parses on a background worker in its own main window; when the value
139
+ # finally materializes, request_render() wakes the loop but doesn't reach the
140
+ # external cached subtrees - so they'd show nothing until an unrelated manual
141
+ # invalidation. They register here and _materialize invalidates them on change.
142
+ # Maps each consumer draw_state → the frame it last re-registered
143
+ # (notify_on_change); the idle sweep reads that last-seen stamp (plus
144
+ # abs_closed) to decide whether this host still has an alive user.
145
+ self._consumers = {}
146
+
147
+ # Bubble nested changes: upgrade existing contents + a non-self input tree so
148
+ # a deep edit (held['cfg']['rank'] = 16, or a grabbed GeneralParse) marks the
149
+ # host without a manual touch(). See bubbling.py.
150
+ if len(self):
151
+ _reinstall_children(self, self)
152
+ if self.input_value is not self and not isinstance(self.input_value, RenderHost):
153
+ self.input_value = install_bubbling(self.input_value, self)
154
+
155
+ if standalone:
156
+ self.register()
157
+
158
+ # ── Registration ──────────────────────────────────────────────────────────
159
+ def register(self):
160
+ """Add to Melty.render_hosts so draw_main calls draw() each frame. Idempotent.
161
+
162
+ Also re-registers the upstream proxy in the chain (a dict_host's str_host):
163
+ str_host has no consumers of its own, so reviving the dict_host after an idle
164
+ sweep must bring its source proxy back too, or the value goes stale. Guarded on
165
+ the upstream's own _registered flag, so a chain can't recurse forever."""
166
+ if not self._registered:
167
+ Melty.render_hosts[id(self)] = self
168
+ self._registered = True
169
+
170
+ iv = self.input_value
171
+ if isinstance(iv, RenderHost) and iv is not self and not iv._registered:
172
+ iv.register()
173
+
174
+ return self
175
+
176
+ def notify_on_change(self, draw_state):
177
+ """Register an EXTERNAL consumer's draw_state to be invalidated when this host's
178
+ held value next materializes/changes. Idempotent. For code that reads the host's
179
+ value (e.g. host.deep.parameters()) and draws it OUTSIDE the host's own draw loop
180
+ — without this its cached subtree never re-runs when a background parse lands.
181
+
182
+ Doubles as the per-frame "I am a live user" pulse the idle sweep reads: call it
183
+ every frame the consumer draws (not just on edit), so its last-seen stamp stays
184
+ current. Reviving a host the sweep deregistered happens here too — reopening a
185
+ closed view re-registers it (and its upstream proxy chain) in Melty.render_hosts."""
186
+ if draw_state is None:
187
+ return
188
+ self._consumers[draw_state] = Melty.frame_count
189
+ # Reopened view → the idle sweep had popped us from render_hosts; bring the
190
+ # host (and its str_host) back into the draw loop. Idempotent.
191
+ if self.standalone and not self._registered:
192
+ self.register()
193
+ # Toggle-off fallback: with no sweep pruning each frame, cap growth by
194
+ # dropping closed consumers when the map gets large.
195
+ if len(self._consumers) > 64:
196
+ self._consumers = {ds: f for ds, f in self._consumers.items()
197
+ if not getattr(ds, 'closed', False)}
198
+
199
+ def remove(self):
200
+ Melty.render_hosts.pop(id(self), None)
201
+ self._registered = False
202
+ return self
203
+
204
+ # One outstanding catch-up timer for the typing hold (class-wide - the hold
205
+ # gates ALL hosts at once in draw_main's loop).
206
+ _typing_wake_timer = None
207
+
208
+ @classmethod
209
+ def typing_hold(cls):
210
+ """True while host draws should be deferred because the user is typing:
211
+ a draw_text editor holds focus AND a key was pressed within the debounce
212
+ window. The draw_main host loop checks this next to its click/drag/
213
+ scroll skip — host draws (reconverts, saves) are deferrable work that
214
+ would otherwise eat into typing frames. Arms a one-shot wake for the
215
+ window's expiry so the skipped draws catch up even when no further
216
+ input produces a frame; a newer keypress simply re-enters the hold on
217
+ that wake's frame and chains the next one."""
218
+ from meltygui.core.runtime.toggles import Toggles
219
+ window = Toggles.HostLifecycle.host_typing_debounce_ms / 1000.0
220
+ if window <= 0 or Melty.text_focused_ds is None:
221
+ return False
222
+ # A physically HELD key counts as pressed regardless of the stamp -
223
+ # repeats don't reliably re-stamp it (Wayland repeat gaps exceed the OS
224
+ # repeat delay), so the hold would otherwise expire mid-press. Reconcile
225
+ # against glfw.get_key first: a RELEASE that went elsewhere (focus
226
+ # stolen mid-hold) would strand the key in the set and starve host
227
+ # draws forever.
228
+ held = bool(Melty._keys_down)
229
+ if held and Melty.glfw_window is not None:
230
+ import meltygui.core.windowing.window_api as glfw
231
+ for k in list(Melty._keys_down):
232
+ try:
233
+ if glfw.get_key(Melty.glfw_window, k) != glfw.PRESS:
234
+ Melty._keys_down.discard(k)
235
+ except Exception:
236
+ Melty._keys_down.discard(k)
237
+ held = bool(Melty._keys_down)
238
+ remaining = Melty._last_key_time + window - time.monotonic()
239
+ if not held and remaining <= 0:
240
+ return False
241
+ # Catch-up wake: for a debounce tail, at its expiry; while a key is
242
+ # held past the stamp window, a full window out - RELEASE re-stamps
243
+ # and forces a frame anyway, the hold is just the safety net.
244
+ timer = cls._typing_wake_timer
245
+ if timer is None or not timer.is_alive():
246
+ timer = threading.Timer(remaining if remaining > 0 else window,
247
+ cls._typing_wake)
248
+ timer.daemon = True
249
+ cls._typing_wake_timer = timer
250
+ timer.start()
251
+ return True
252
+
253
+ @classmethod
254
+ def _typing_wake(cls):
255
+ cls._typing_wake_timer = None
256
+ request_render()
257
+
258
+ @classmethod
259
+ def all(cls):
260
+ return list(Melty.render_hosts.values())
261
+
262
+ @classmethod
263
+ def draw_all(cls, mark=None):
264
+ """The per-frame host pump: draw every registered host that has work.
265
+ Hosts are self-registering (a code_file_io host loads its file on its
266
+ first draw, reparses after an edit, auto-saves), so whatever runs the
267
+ frame loop must call this once a frame — the studio from draw_main,
268
+ a meltygui app from Surface.frame — or nothing ever loads.
269
+
270
+ Skipped entirely while any mouse button is held (click or drag) or a
271
+ view is being scrolled (Melty.on_scroll lingers a few frames past the
272
+ last wheel event, bridging a trackpad's event stream): host draws are
273
+ deferrable work that would otherwise eat into drag/scroll frames;
274
+ they catch up after. The typing hold (keypress debounce, self-armed
275
+ catch-up wake) defers only HIDDEN CACHE hosts (##-named evictable
276
+ pairs — their reconverts/saves are the GIL-heavy work that eats
277
+ typing frames); VISIBLE hosts keep drawing through it, since a
278
+ params field's keystrokes publish fresh tensors into the value
279
+ windows and holding those froze every visualization for the burst.
280
+
281
+ Event-driven: an idle hidden cache host skips its draw entirely
282
+ (draw_needed — edit flags / upstream identity change / tile
283
+ invalidation / slow heartbeat). An open input tab registers ~20
284
+ code-cache pairs and each polled draw is ~0.1ms of wrapper overhead
285
+ doing nothing — that is what dropped drags to 70fps. ``mark(label)``,
286
+ if given, receives a perf-trace label per gate decision and host drawn."""
287
+ import meltygui_imgui as imgui
288
+ from meltygui.core.windowing.surface import Surface
289
+ import meltygui.core.windowing.window_api as glfw
290
+ any_mouse_held = (imgui.is_mouse_down(0) or imgui.is_mouse_down(1)
291
+ or imgui.is_mouse_down(2) or Melty.space_mouse_drag)
292
+ # The host pump is process-wide and runs on the first surface. A
293
+ # picker drag belongs to the child's input context, so the parent's
294
+ # ImGui state alone misses it and runs deferred hosts throughout
295
+ # the drag. Read the windows' button state without changing contexts.
296
+ any_mouse_held = any_mouse_held or any(
297
+ glfw.get_mouse_button(surface.window, button) == glfw.PRESS
298
+ for surface in Surface.all if surface.window is not None and not surface.closed
299
+ for button in (glfw.MOUSE_BUTTON_LEFT, glfw.MOUSE_BUTTON_RIGHT, glfw.MOUSE_BUTTON_MIDDLE))
300
+ typing_held = cls.typing_hold()
301
+ if mark is not None:
302
+ mark(f"hosts_gate(held={int(any_mouse_held)},typing={int(bool(typing_held))})")
303
+ if any_mouse_held or Melty.on_scroll:
304
+ return
305
+ # Snapshot: a host may register / remove during its draw.
306
+ for host in cls.all():
307
+ if (typing_held and getattr(host, "evictable", False)
308
+ and host.name.startswith("##")):
309
+ continue
310
+ if not host.draw_needed():
311
+ continue
312
+ host.draw()
313
+ if mark is not None:
314
+ mark(f"host[{str(host.name)[:24]}]")
315
+ # Drawing alone is not a change: cached or idle draws must let the
316
+ # loop sleep. Materialization, edits and background completions already
317
+ # request their own frames. A missing parse is not pending work either
318
+ # (plain data / cache hosts may never have a value_key entry).
319
+
320
+ @classmethod
321
+ def current(cls):
322
+ return cls._active[-1] if cls._active else None
323
+
324
+ # ── Idle sweep: deregister hosts whose consumer windows have all closed ─────
325
+ @staticmethod
326
+ def _consumer_closed(ds):
327
+ """A consumer draw_state counts as gone once its window is abs_closed —
328
+ the primary, immediate trigger. A vanished/broken draw_state (raises) is
329
+ treated as gone too."""
330
+ try:
331
+ return bool(ds.abs_closed)
332
+ except Exception:
333
+ return True
334
+
335
+ def _prune_consumers(self, now, k):
336
+ """Drop consumers that are abs_closed OR whose k-frame liveness net
337
+ tripped. The net keys on the LATER of two stamps: registration
338
+ (notify_on_change, body actually ran) and ds.last_seen (the blit
339
+ compositor stamps it every frame the tile is drawn, even when the body
340
+ cache-skips). A blit-cached view in an OPEN window stops registering
341
+ for minutes but keeps compositing, so it survives; a closed dropdown /
342
+ context-menu popover stops BOTH (and is never abs_closed — popovers
343
+ don't set closed), so it ages out instead of pinning the host in
344
+ Melty.render_hosts forever. Registration-only aging (the pre-2026-07
345
+ net) pruned the blit-cached case too — host swept, external file
346
+ change had no pump and no consumer, view froze on stale values.
347
+
348
+ The net's window is deliberately wider than k: last_seen lags while
349
+ an ANCESTOR's tile covers the consumer (only the topmost blitted tile
350
+ gets stamped — measured lags of a few hundred frames on live views),
351
+ so the raw k=120 would prune covered-but-live consumers and re-open
352
+ the frozen-view hole. 10·k keeps those safe with ~5x margin while a
353
+ popover orphan (measured 1000+ frames stale on BOTH stamps) still
354
+ ages out and lets its host sweep."""
355
+ k = 10 * k
356
+ cons = self._consumers
357
+ if not isinstance(cons, dict): # pre-hotswap list shape - reset
358
+ self._consumers = {}
359
+ return
360
+ self._consumers = {ds: seen for ds, seen in cons.items()
361
+ if not self._consumer_closed(ds)
362
+ and (now - max(seen, getattr(ds, 'last_seen', None) or 0)) <= k}
363
+
364
+ @classmethod
365
+ def sweep(cls):
366
+ """Deregister evictable hosts with no active consumer from Melty.render_hosts
367
+ so draw_main stops drawing them each frame. The host and its parse stay in the
368
+ code-host cache (NOT evicted) — reopening the view re-registers it via
369
+ notify_on_change → register. Toggle-gated; runs once per frame from end_frame."""
370
+ from meltygui.core.runtime.toggles import Toggles
371
+ if not Toggles.HostLifecycle.deregister_idle:
372
+ return
373
+ k = int(Toggles.HostLifecycle.idle_frames)
374
+ now = Melty.frame_count
375
+ hosts = list(Melty.render_hosts.values())
376
+
377
+ # First pass: direct liveness. Non-evictable singletons are always alive;
378
+ # a freshly-born host is held through a birth grace (its consumer may not
379
+ # have drawn yet - Mode.WINDOW bodies render deferred).
380
+ alive = {}
381
+ for h in hosts:
382
+ if not getattr(h, "evictable", False):
383
+ alive[id(h)] = True
384
+ continue
385
+ if now - getattr(h, "_birth_frame", 0) <= k:
386
+ alive[id(h)] = True
387
+ continue
388
+ h._prune_consumers(now, k)
389
+ alive[id(h)] = bool(h._consumers)
390
+
391
+ # Second pass: a host referenced as a live host's input_value stays alive.
392
+ # The str_host behind a dict_host has no consumers of its own, so it would
393
+ # otherwise be swept out from under the dict_host that still needs it.
394
+ changed = True
395
+ while changed:
396
+ changed = False
397
+ for h in hosts:
398
+ if not alive.get(id(h)):
399
+ continue
400
+ iv = getattr(h, "input_value", None)
401
+ if isinstance(iv, RenderHost) and iv is not h and not alive.get(id(iv)):
402
+ alive[id(iv)] = True
403
+ changed = True
404
+
405
+ for h in hosts:
406
+ if getattr(h, "evictable", False) and not alive.get(id(h)):
407
+ h.remove() # pop from render_hosts but KEEP the cache entry
408
+
409
+ # ── Visibility ────────────────────────────────────────────────────────────
410
+ def show(self):
411
+ self.hidden = False
412
+ request_render()
413
+ return self
414
+
415
+ def hide(self):
416
+ self.hidden = True
417
+ request_render()
418
+ return self
419
+
420
+ def toggle(self):
421
+ self.hidden = not self.hidden
422
+ request_render()
423
+ return self
424
+
425
+ # ── Change logging (diagnose spurious saves) ──────────────────────────────
426
+ def _caller_trail(self, frames=4, skip=2):
427
+ out = []
428
+ try:
429
+ f = sys._getframe(skip)
430
+ for _ in range(frames):
431
+ if f is None:
432
+ break
433
+ base = f.f_code.co_filename.rsplit("/", 1)[-1]
434
+ out.append(f"{base}:{f.f_lineno} {f.f_code.co_name}")
435
+ f = f.f_back
436
+ except Exception:
437
+ pass
438
+ return " <- ".join(out)
439
+
440
+ def _log_change(self, event, **info):
441
+ if not RenderHost.debug_changes:
442
+ return
443
+ extra = " ".join(f"{k}={v}" for k, v in info.items())
444
+ print(f"[RenderHost:{self.name}] {event} {extra} ║ {self._caller_trail()}")
445
+
446
+ # ── Dict mutation → external_change (bubble root) ──────────────────────────
447
+ def _mark_changed(self):
448
+ self._external_change = True
449
+ self._local_edit_frame = Melty.frame_count # stamp: a LOCAL edit happened NOW
450
+ self._local_edit_time = time.monotonic() # re-arms the consumer-notify debounce
451
+ # Debug timeline: who dirtied this host (a symbol-attach bubbling into the
452
+ # held gp would show up here as _distribute_by_name / _post_symbol_attach).
453
+ _ptrace_rl(("host-dirty", self.name), f"host DIRTY {self.name} <- {self._caller_trail(frames=5)}",
454
+ min_interval=0.05)
455
+ # Invalidate BOTH the window envelope AND the wrapper's own draw_state: the
456
+ # envelope so render_host_view re-runs and calls the wrapper, and the wrapper
457
+ # so its blit-cached body actually re-executes to process the edit (load/save
458
+ # or chain_out). Invalidating only the envelope leaves the wrapper replayed.
459
+ for ds in (self._draw_state, self._wrapper_draw_state):
460
+ if ds is not None:
461
+ ds._external_change = True
462
+ try:
463
+ pass
464
+ # ds.invalidate(frame_delta=0)
465
+ # ds.invalidate(frame_delta=1)
466
+ note = Note(name="RenderHost _mark_changed", tint=(1, 0.5, 0), draw_state=ds)
467
+ ds.invalidate(note=note)
468
+ notify("invalidate #1", tag="host", tint=(1,0,1))
469
+
470
+ except Exception:
471
+ pass
472
+ self._log_change("DIRTY (dict/nested mutated)")
473
+ request_render()
474
+
475
+ def touch(self):
476
+ """Manually flag a change. Rarely needed — nested dict/list/GeneralParse edits
477
+ bubble automatically (bubbling.py). Use for an ATTRIBUTE write on a nested
478
+ non-container, or an in-place edit of an opaque leaf. Returns self."""
479
+ self._mark_changed()
480
+ return self
481
+
482
+ # NOTE: deep path traversal is the EXPLICIT `.deep` property (from _DeepAttrMixin),
483
+ # never a blanket __getattr__. A blanket __getattr__ would answer the framework's
484
+ # bare attribute probes on the host with a _DeepPath stand-in instead of raising
485
+ # AttributeError, and rendering (which expects a _BubblingDict) chokes on it. Bare
486
+ # `host.foo` must behave like a normal dict/object; use `host.deep.foo...()` to
487
+ # traverse.
488
+
489
+ def __setitem__(self, key, value):
490
+ # Re-assigning an equal value isn't an edit (draw_collection writes its
491
+ # rendered value back each frame) - store it but don't mark dirty / redraw.
492
+ unchanged = key in self and dict.__getitem__(self, key) is value
493
+
494
+ value = install_bubbling(value, self)
495
+ super().__setitem__(key, value)
496
+ if not unchanged:
497
+ self._mark_changed()
498
+
499
+ def __delitem__(self, key):
500
+ super().__delitem__(key)
501
+ self._mark_changed()
502
+
503
+ def update(self, *args, **kwargs):
504
+ super().update(*args, **kwargs)
505
+ _reinstall_children(self, self)
506
+ self._mark_changed()
507
+
508
+ def setdefault(self, key, default=None):
509
+ existed = key in self
510
+ if not existed:
511
+ default = install_bubbling(default, self)
512
+ result = super().setdefault(key, default)
513
+ if not existed:
514
+ self._mark_changed()
515
+ return result
516
+
517
+ def pop(self, *args):
518
+ had = bool(self) and (not args or args[0] in self)
519
+ result = super().pop(*args)
520
+ if had:
521
+ self._mark_changed()
522
+ return result
523
+
524
+ def popitem(self):
525
+ result = super().popitem()
526
+ self._mark_changed()
527
+ return result
528
+
529
+ def clear(self):
530
+ if self:
531
+ super().clear()
532
+ self._mark_changed()
533
+ else:
534
+ super().clear()
535
+
536
+ # ── The host's private view_func (handed to its wrapper) ───────────────────
537
+ def _internal_view_func(self, input_value=None, external_change=False, **kwargs):
538
+ """The view_func the host hands its wrapper. The wrapper passes the value to
539
+ edit — a source STRING (code_file_io) or the parsed TREE (convert_in_and_out_
540
+ value) — and we:
541
+ INBOUND : materialize it into our dict (held under value_key), unless we're
542
+ mid-edit (dirty) — never clobber a change flowing the other way.
543
+ RENDER : draw the held value (str → editor, dict → collection). A dict/tree
544
+ edit mutates in place and bubbles (→ dirty); a string edit returns.
545
+ OUTBOUND: if anything changed, return (True, held) so the wrapper saves /
546
+ chain_outs it. Returns the held value in the wrapper's native shape."""
547
+ pre_dirty = self._external_change
548
+ self._external_change = False
549
+
550
+ # convert_in_and_out_value tags each finished chain_in with the GENERATION (the
551
+ # origin local-edit frame) of the source it parsed - the echo of our own edit
552
+ # carries that edit's frame, a genuine external change carries "now". Use it as
553
+ # the frame-precedence baseline below (not the frame the parse happens to LAND),
554
+ # so a re-parse of an OLDER edit lands strictly before a newer local edit and
555
+ # can't clobber it - that race is the value-flicker. None on non-finish frames and
556
+ # for the code_file_io path (which doesn't pass it). Pop so it never reaches the
557
+ # renderer's kwargs.
558
+ inbound_gen = kwargs.pop("inbound_gen", None)
559
+ if inbound_gen is not None:
560
+ self._input_change_frame = inbound_gen
561
+
562
+ # INBOUND. The HELD value is AUTHORITATIVE: it was set via __setitem__ (the
563
+ # user's draw_text string) and flows OUT to the wrapper. We do NOT materialize
564
+ # the wrapper's value (code_file_io's text_cache, which LAGS the latest
565
+ # keystrokes and is a stale-to-save snapshot) back over it - that's the "save
566
+ # replaces the text with the saved version, losing chars typed during edit" bug.
567
+ #
568
+ # Two exceptions:
569
+ # - a genuine UPSTREAM change - draw() saw a NEW resolved input object and set
570
+ # `_pending_external` (NOT the wrapper's `external_change` param, which for
571
+ # code_file_io is just its per-keystroke reconvert pulse) - wins even over a
572
+ # local dirty; drop the stale dirty so we don't surface a spurious outbound.
573
+ # - an INITIAL fill, when nothing is held yet.
574
+ # An upstream change (draw() set `_pending_external`) means the wrapper will
575
+ # produce a FRESH derived result - but convert's chain_in re-parses on a
576
+ # BACKGROUND thread, so it fires a frame+ after the pulse. Latch "awaiting" so we
577
+ # still materialize it when it arrives (a new object), not just on the pulse
578
+ # frame - otherwise the gate blocks the catch-up and the dict holds the PREVIOUS
579
+ # parse forever ("dict always holds the old snapshot").
580
+ if self._pending_external:
581
+ self._awaiting_inbound = True
582
+
583
+ # A fresh DERIVED-result pulse must reach external consumers even when
584
+ # the VALUE didn't change: a FAILED parse keeps last_good held (so
585
+ # _materialize never runs) and parks the error in the wrapper's
586
+ # ModesState - consumers that read that error (the folder-tree editor's
587
+ # red-line highlight, via draw_text_from_code_cache) would otherwise
588
+ # never re-run their cached subtrees to show or clear it.
589
+ if external_change and self._consumers:
590
+ self._notify_consumers(name="RenderHost result pulse")
591
+
592
+ # FILE-WATCHER / external reload. The codec registers a file watcher on the
593
+ # host's draw_state at resolve_address; when the file changes, FileWatch sets
594
+ # that draw_state dirty and code_file_io re-reads the file into text_cache. draw()
595
+ # CAN'T see this - code_file_io's input object (a class / path) is immutable, so
596
+ # _pending_external never fires for it. Detect it here instead: the wrapper passed
597
+ # us a value we did NOT produce (an external_change pulse whose input_value is a
598
+ # DIFFERENT object than our held one) while we're not mid-edit. A keystroke does
599
+ # NOT match - after we surface, text_cache IS our held object, so `is not held` is
600
+ # false. Treat it like an upstream change (stamp input_change so frame precedence
601
+ # treats the source as fresh, arm awaiting); the pull below then materializes it.
602
+ if (external_change and input_value is not None and input_value is not self
603
+ and input_value is not self._held() and not pre_dirty):
604
+ # Keep the inbound generation when convert provided one (above); only the
605
+ # code_file_io path (inbound_gen=None) falls back to "now".
606
+ if inbound_gen is None:
607
+ self._input_change_frame = Melty.frame_count
608
+ self._awaiting_inbound = True
609
+
610
+
611
+ # The file changed on disk - the wrapper/blit cache won't know to re-render
612
+ # the reloaded value, so invalidate this proxy's view explicitly (same path
613
+ # as the initial fill below).
614
+ if self._draw_state is not None and self._draw_state._parent is not None:
615
+ self._draw_state._parent.invalidate_up_by_obj(
616
+ obj=self, note=Note(name="file_reload", tint=(1, 0.6, 0.1)), max_depth=3)
617
+ request_render()
618
+
619
+ # FRAME PRECEDENCE (all O(1) — no content comparison). Is there a GENUINE pending
620
+ # local edit, newer than the source this inbound represents? `_input_change_frame`
621
+ # is the source's ORIGIN generation: for convert it's the edit-frame the parse
622
+ # round-trips from (tagged by convert_in_and_out_value, NOT the frame the parse
623
+ # landed - that distinction is what stops the flicker), and for code_file_io it's
624
+ # the frame its external source changed. Rendering the held value writes
625
+ # reconstructed children back (draw_collection) a frame later - a benign 1-frame
626
+ # "edit" echo - so a diff of ≤1 is NOT genuine; only a local edit MORE than 1
627
+ # frame ahead of what the inbound represents counts, and it WINS (inbound rejected).
628
+ local_ahead = self._local_edit_frame > self._input_change_frame + 1
629
+
630
+ if input_value is not None and input_value is not self:
631
+ if self.value_key not in self:
632
+ self._materialize(input_value)
633
+ note = Note(name="_materialize", tint=(1, 1.0, 1.0))
634
+ # Needed for initial load
635
+ self._draw_state._parent.invalidate_up_by_obj(obj=self, note=note, max_depth=3)
636
+ self._draw_state._parent.invalidate_up(note=note, max_depth=7)
637
+
638
+ request_render()
639
+
640
+ # initial fill
641
+ elif (self._awaiting_inbound and external_change
642
+ and input_value is not self._held()
643
+ and not local_ahead):
644
+
645
+ # Pull only the fresh parse. `external_change` is the wrapper signalling a
646
+ # FRESH derived result THIS frame (convert sets it only on the frame its
647
+ # background chain_in FINISHES, with the current source's parse - and
648
+ # run_in_background coalesces to latest-only, so a finished result is never
649
+ # superseded). Without this gate we'd pull whatever `primary` happens to be
650
+ # when `local_ahead` flips false - which, mid round-trip, is the STALE
651
+ # last-good parse of the PREVIOUS source → the 428↔445 snap-back/oscillation.
652
+ # `not local_ahead` still lets a genuine live edit win.
653
+ # Needed
654
+ note = Note(name="Render host, self", tint=(1, 1, 1))
655
+ self._draw_state._parent.invalidate_by_obj(obj=self, note=note)
656
+ self._materialize(input_value)
657
+ self._awaiting_inbound = False
658
+
659
+ if self.value_key not in self:
660
+ # Nothing held yet (pre-materialize): the renderer draws a blank
661
+ # where the content will be - stamp the placeholder frame so the
662
+ # wrapper commit doesn't restore persisted content heights over it
663
+ # (see Melty.pending_placeholder_frame).
664
+ Melty.pending_placeholder_frame = Melty.frame_count
665
+ renderer = self.settings_renderer or RenderFuncs.draw_blank
666
+ render_kwargs = {k: v for k, v in kwargs.items() if k != "routed"}
667
+ render_kwargs.setdefault("name", f"{self.name}##held")
668
+ result = renderer(self._held(), **render_kwargs)
669
+ r_changed, r_new = (result[0], result[1]) if isinstance(result, tuple) and len(result) >= 2 else (False, result)
670
+ # A string edit (immutable) comes back via the return; store it. A dict/tree
671
+ # edit mutated in place and already set _external_change via bubbling.
672
+ if r_changed and self.get(self.value_key) is not r_new:
673
+ # Needed
674
+ self[self.value_key] = r_new
675
+ self._draw_state._parent.invalidate_by_obj(obj=input_value)
676
+ self._draw_state._parent.invalidate_by_obj(obj=self)
677
+ request_render()
678
+
679
+ # OUTBOUND only a GENUINE local edit - one MORE than 1 frame ahead of the source
680
+ # (a real drag), NOT the 1-frame draw_collection frame echo. Without this, the
681
+ # echo re-surfaces the just-materialized value every cycle → chain_out → save in
682
+ # a self-sustaining loop. r_changed (the renderer's own edit) always surfaces.
683
+ if self._external_change:
684
+ self._wrapper_draw_state._parent.invalidate()
685
+ self._draw_state._parent.invalidate_by_obj(obj=input_value)
686
+ self._draw_state._parent.invalidate_by_obj(obj=self)
687
+ notify("invalidate #5", tag="host", tint=(1, 0, 1))
688
+
689
+ request_render()
690
+
691
+ _ext = self._external_change
692
+ edited = bool((pre_dirty and local_ahead) or _ext or r_changed)
693
+ self._external_change = False
694
+ if edited:
695
+ # Change timeline: WHY this host reports an edit to its wrapper - for the
696
+ # convert (dict) host this is exactly what starts a chain_out.
697
+ _ptrace(f"host OUTBOUND edited {self.name}",
698
+ pre_dirty=pre_dirty, local_ahead=local_ahead,
699
+ ext=_ext, r_changed=r_changed)
700
+ # Needed
701
+ note = Note(name="Render host, nested view edited", tint=(1, 1, 1))
702
+ # self._draw_state._parent.invalidate_by_obj(obj=input_value, note=note)
703
+ # notify("invalidate #6", tag="host", tint=(1, 0, 1))
704
+
705
+ request_render()
706
+ note = Note(name="Render host, self", tint=(1, 1, 1))
707
+ # self._draw_state._parent.invalidate_by_obj(obj=self, note=note)
708
+ # notify("invalidate #7", tag="host", tint=(1, 0, 1))
709
+ self._log_change("OUTBOUND → return (True, value)",
710
+ pre_dirty=pre_dirty, r_changed=r_changed, local_ahead=local_ahead)
711
+ return True, self._outbound_value()
712
+ return False, self._outbound_value()
713
+
714
+ # ── Materialize / surface ──────────────────────────────────────────────────
715
+ def _materialize(self, value):
716
+ """Hold an INBOUND value under value_key (type intact, bubbling installed).
717
+
718
+ IDEMPOTENT by identity: already holding this exact object → no-op (the wrapper
719
+ hands back the SAME parsed object every frame until a re-parse; without this
720
+ the render loop spins). RAW dict ops, so it doesn't re-fire _mark_changed."""
721
+ if value is self:
722
+ return
723
+ if self.value_key is not None:
724
+ if self.get(self.value_key) is value:
725
+ return
726
+ new = {self.value_key: value}
727
+ elif isinstance(value, dict):
728
+ if len(self) == len(value) and all(self.get(k) is v for k, v in value.items()):
729
+ return
730
+ new = value
731
+ else:
732
+ new = {"value": value}
733
+ dict.clear(self)
734
+ dict.update(self, new)
735
+ _reinstall_children(self, self)
736
+ # The held value just CHANGED - invalidate any external consumer subtrees so they
737
+ # re-run and pick it up (the background-parse-lands-into-a-cached-context-menu
738
+ # case).
739
+ self._notify_consumers(name="RenderHost value materialized")
740
+ request_render()
741
+
742
+ def _notify_consumers(self, name="RenderHost value changed"):
743
+ """Invalidate external consumer subtrees (notify_on_change) — DEBOUNCED
744
+ against active typing. While the held value is being locally edited
745
+ (a keystroke stream — each edit stamps _local_edit_time), every finished
746
+ background reconvert would pulse here and invalidate EVERY consumer,
747
+ including a second editor window over the same file, per keystroke —
748
+ large-file re-renders that stall typing. Instead the notify is deferred
749
+ (trailing, re-armed per edit) and fires ONCE, a quiet interval after the
750
+ last local edit. Notifies with no recent local edit (external reload,
751
+ initial load) pass through immediately. The typing editor itself doesn't
752
+ need the pulse — its keystrokes render through the focused editor
753
+ directly."""
754
+ from meltygui.core.runtime.toggles import Toggles
755
+ window = Toggles.HostLifecycle.consumer_notify_debounce_ms / 1000.0
756
+ remaining = self._local_edit_time + window - time.monotonic()
757
+ if remaining > 0:
758
+ self._pending_notify_name = name
759
+ self._arm_notify_timer(remaining)
760
+ return
761
+ self._notify_consumers_now(name)
762
+
763
+ def _arm_notify_timer(self, delay):
764
+ prev = self._notify_timer
765
+ if prev is not None:
766
+ prev.cancel()
767
+ t = threading.Timer(delay, self._flush_pending_notify)
768
+ t.daemon = True
769
+ self._notify_timer = t
770
+ t.start()
771
+
772
+ def _flush_pending_notify(self):
773
+ """Timer body: fire the deferred consumer notify — unless another local
774
+ edit landed while waiting, in which case re-arm for the remainder (the
775
+ trailing-debounce re-arm for edits that produced no new pulse)."""
776
+ self._notify_timer = None
777
+ name = self._pending_notify_name
778
+ if name is None:
779
+ return
780
+ from meltygui.core.runtime.toggles import Toggles
781
+ window = Toggles.HostLifecycle.consumer_notify_debounce_ms / 1000.0
782
+ remaining = self._local_edit_time + window - time.monotonic()
783
+ if remaining > 0:
784
+ self._arm_notify_timer(remaining)
785
+ return
786
+ self._pending_notify_name = None
787
+ self._notify_consumers_now(f"{name} (debounced)")
788
+
789
+ def _notify_consumers_now(self, name):
790
+ """The actual invalidation fan-out: they read this host's value/error from
791
+ OUTSIDE its own draw loop, so nothing else re-runs them. Climb their
792
+ ancestors (invalidate_up) since the consumer is usually a nested cached
793
+ view that won't re-run unless its parents do."""
794
+ # Snapshot: this can run on a background worker (via _materialize) while
795
+ # the render thread rebuilds _consumers in notify_on_change / the sweep -
796
+ # a live dict would raise "changed size during iteration".
797
+ # max_depth: the consumer renders THIS host's data, so a new value must
798
+ # invalidate its whole cached subtree - the blit cache has no data
799
+ # key, children replay purely on tile dirtiness, and a depth-2 cascade
800
+ # left every deeper view compositing its old capture (the window
801
+ # re-drew, flickered, and still showed stale data until a hover
802
+ # invalidated the subtree for real). Depth 8 covers the deepest
803
+ # consumer trees; the cascade's clip gate already skips scrolled-out
804
+ # children (invalidate_scrolled_in catches those later), and this
805
+ # runs once per materialized value, not per frame.
806
+ for cds in list(self._consumers):
807
+ tid = getattr(cds, "_tile_id", None)
808
+ if tid is not None:
809
+ Melty.cache.invalidate_up(tid, force=True, max_depth=8,
810
+ note=Note(name=name,
811
+ tint=(0.4, 1.0, 0.6), draw_state=cds))
812
+ # notify("invalidate #8", tag="host", tint=(1, 0, 1))
813
+
814
+ if self._consumers:
815
+ request_render()
816
+
817
+ def _held(self):
818
+ """The single held value (string / tree) the wrapper edits — None until first
819
+ materialized (so a not-yet-ready upstream proxy resolves to None). The whole
820
+ dict in mirror mode (value_key=None)."""
821
+ if self.value_key is not None:
822
+ return self.get(self.value_key)
823
+ return self
824
+
825
+ _outbound_value = _held
826
+
827
+ def _set_held(self, value):
828
+ """Cross-proxy write: a downstream proxy pushes the wrapper's output (a new
829
+ source string) onto this upstream proxy → it surfaces to ITS wrapper and saves.
830
+
831
+ The LOCAL __setitem__ value is AUTHORITATIVE: a pending local edit (dirty — the
832
+ user just typed) is NOT overwritten, so a late/stale background result (a
833
+ debounced chain_out snapshotted frames ago) can't clobber what's being typed.
834
+ All O(1) — no content comparison."""
835
+ if self.value_key is None:
836
+ return
837
+ if self._external_change:
838
+ return # pending local edit wins over a write-back
839
+ if self.get(self.value_key) is value:
840
+ return # same object - no change
841
+ self[self.value_key] = value
842
+
843
+ # ── Lifecycle: draw the wrapper in this host's window (draw_main calls this) ─
844
+ def draw_needed(self):
845
+ """Event-driven pump gate for draw_main's host loop: False when this
846
+ is a hidden cache host with nothing to do this frame, so keeping
847
+ dozens of code-cache pairs REGISTERED costs ~nothing (each polled
848
+ draw is ~0.1ms of pure wrapper/envelope overhead). All real work is
849
+ already invalidation-driven — the io bodies are use_cache=True, so on
850
+ a clean frame they blit-replay and do nothing — which makes "has
851
+ work" exactly:
852
+ - a flagged edit (_external_change / _pending_external),
853
+ - the upstream value changed identity (what draw() polls for),
854
+ - never drawn yet (initial load pending),
855
+ - the envelope/wrapper tile is invalidated (any ds.invalidate —
856
+ _mark_changed, run_in_background completions, consumer notifies),
857
+ - the slow staggered heartbeat (1-in-30): the safety net for
858
+ signals that don't touch tiles (debounce timers, missed edges) —
859
+ a missed wake costs ≤~250ms on pipelines already debounced by
860
+ hundreds of ms, and the save channel can never wedge.
861
+ Visible hosts (real windows / non-## names / non-evictable) always
862
+ draw — they are on screen every frame by definition."""
863
+ # A file-watch dispatch flagged the wrapper ds (external file change).
864
+ # The flag alone just bypasses the wrapper's OWN cache gate - but if
865
+ # the envelope above it is blit-cached, the replay never descends to
866
+ # the wrapper, the gate is never consulted, and the flag sits
867
+ # unconsumed forever (the io body never reloads). Invalidate this
868
+ # host's parent tile chain so the walk actually reaches the wrapper.
869
+ # Cheap and self-limiting: one host's tiles, and only until the body
870
+ # runs and clears the flag.
871
+ wds0 = self._wrapper_draw_state
872
+ if wds0 is not None and getattr(wds0, "_external_change", False):
873
+ if Melty.cache is not None and wds0._tile_id is not None:
874
+ Melty.cache.invalidate_up(wds0._tile_id, force=True, max_depth=4)
875
+ return True
876
+ # Every host is windowed (draw() draws each a stub envelope parked
877
+ # entirely OFF the screen - invisible chrome; the DATA windows a host
878
+ # opens are separate subwindows drawn from draw_main's registered loop), so
879
+ # `self.window` used to short-circuit this gate for ALL hosts and the
880
+ # event-driven pump below never ran: 17 hosts × ~0.2 ms of envelope +
881
+ # io-wrapper blit-replay on every frame the mouse wasn't held (Lukas
882
+ # 08-31: dropped the window term - flags, upstream identity, dirty
883
+ # tiles and the heartbeat cover every possible reason to draw).
884
+ if self.hidden:
885
+ return False
886
+ if self._external_change or self._pending_external:
887
+ return True
888
+ iv = self.input_value
889
+ if isinstance(iv, RenderHost):
890
+ iv = iv._held()
891
+ if iv is not self._last_resolved:
892
+ return True
893
+ env_ds = getattr(self, "_draw_state", None)
894
+ wds = self._wrapper_draw_state
895
+ if env_ds is None or wds is None:
896
+ return True
897
+ cache = Melty.cache
898
+ if cache is None or not cache.enabled:
899
+ # No blit cache (a meltygui app's surface, tests): no tiles, so
900
+ # nothing that can carry a dirty mark: a run_in_background
901
+ # completion (Melty.cache.invalidate on the io view's key) would
902
+ # only reach this host on the 1-in-30 heartbeat - which an
903
+ # edit-request app that idles between events never counts up to:
904
+ # the code editor's file host sat on its loading stand-in until
905
+ # a stray click (09-13). Poll instead: the io bodies are cheap
906
+ # (~0.1 ms of wrapper each) and writes only happen on request.
907
+ return True
908
+ if cache is not None:
909
+ for ds in (env_ds, wds):
910
+ tile = cache._tiles.get(getattr(ds, "_tile_id", None))
911
+ if tile is None:
912
+ # No tile: a zero-size host view (the voxel / line hosts'
913
+ # wrappers are 0 px wide - their output flows through
914
+ # the held value, not pixels) never gets one, and
915
+ # _is_dirty(None) read as "dirty" every frame. Only a
916
+ # NEVER-drawn view needs a first run.
917
+ if getattr(ds, "frame_count", 0) == 0:
918
+ return True
919
+ continue
920
+ if cache._is_dirty(tile):
921
+ return True
922
+ return (Melty.frame_count + (id(self) >> 4)) % 30 == 0
923
+
924
+ @staticmethod
925
+ def stub_width(stub_size):
926
+ """The width the stub envelope actually renders at: the size draw()
927
+ asks for, floored by Mode.HOST_WINDOW's min_width (the wrapper clamps
928
+ `width` up to it). draw() parks the envelope this far off the display."""
929
+ from typing import Any
930
+ from meltygui.core.rendering.mode import Mode
931
+ floor = Mode.HOST_WINDOW.value[Any].kwargs.get("min_width", 0)
932
+ return max(stub_size, floor)
933
+
934
+ def draw(self, **extra):
935
+ """Drive the wrapper for one frame. Resolve input_value (an upstream proxy →
936
+ its held value), run the wrapper with our private view_func via the window
937
+ envelope, and write the wrapper's output back upstream on edit."""
938
+ if self.hidden:
939
+ return None
940
+ from meltygui.core.rendering.mode import Mode
941
+
942
+ iv = self.input_value
943
+ if isinstance(iv, RenderHost):
944
+ iv = iv._held()
945
+
946
+ # Detect the input change HERE, not in render_host_view. draw() runs EVERY frame
947
+ # (draw_main is uncached), whereas render_host_view's window body is blit-cached
948
+ # and won't run on the frame an upstream proxy's value changed - so detecting it
949
+ # there drops the external_change event intermittently. On a change: flag it for
950
+ # the wrapper (so convert re-parses) AND invalidate the wrapper so its body
951
+ # actually re-runs to consume the flag. Identity compare - a changed source is a
952
+ # new string object; no content comparison needed.
953
+
954
+ draw=False
955
+ if iv is not self._last_resolved:
956
+ self._last_resolved = iv
957
+ self._pending_external = True
958
+ self._input_change_frame = Melty.frame_count # stamp: the input (source) changed here
959
+ # Invalidate BOTH the window envelope AND the wrapper's draw_state. The
960
+ # envelope so render_host_view re-runs and CALLS the wrapper; the wrapper
961
+ # so its blit-cached body actually re-executes (re-parses) - invalidating
962
+ # only the envelope re-calls a wrapper that just replays its cache, so the
963
+ # re-parse never happens and the change is lost (an intermittent bug).
964
+ for ds in (None, self._wrapper_draw_state):
965
+ if ds is not None:
966
+ note = Note(name="Renderhost _last_resolved", tint=(1, 0.0, 0.0), draw_state=ds)
967
+ # ds.invalidate(note=note)
968
+ # ds.invalidate_by_obj(obj=self, note=note)
969
+ notify("invalidate #9", tag="host", tint=(1, 0, 1))
970
+ draw=True
971
+
972
+ request_render()
973
+
974
+ # The envelope is chrome only (a host's DATA windows are separate
975
+ # @windows), so it is parked entirely OFF the display: its right edge
976
+ # sits `stub_gap` px left of x = 0, where nothing paints and nothing
977
+ # hit-tests. The mode's min_width / min_height floor the 40 px asked
978
+ # for (Mode outranks the caller, style guide rule 5), so the offset
979
+ # is taken off the floored width — it used to sit at x = -38 with the
980
+ # 50 px floor and showed a 12 px sliver down the left edge.
981
+ # [tint=(0.3, 0.0, 0.7)] stub_size = 40
982
+ # [tint=(0.3, 0.0, 0.7)] stub_gap = 10
983
+ # [tint=(0.3, 0.0, 0.7)] stub_top = 100
984
+ stub_size = 40
985
+ stub_gap = 10
986
+ stub_top = 100
987
+ win_kwargs = {"name": self.name}
988
+ if self.window:
989
+ win_kwargs.setdefault("mode", Mode.HOST_WINDOW)
990
+ win_kwargs["active_layer"] = 1
991
+ win_kwargs["unmanaged"] = True
992
+ win_kwargs["closed"] = False
993
+ win_kwargs['height'] = stub_size
994
+ win_kwargs['width'] = stub_size
995
+ win_kwargs["window_pos"] = (-(self.stub_width(stub_size) + stub_gap), stub_top)
996
+
997
+ win_kwargs.update(extra)
998
+
999
+ RenderHost._active.append(self)
1000
+ try:
1001
+ win_kwargs['tint'] = (0.3, 0, 0.7, 0.1)
1002
+ # win_kwargs['height'] = 40
1003
+
1004
+ result = render_host_view(iv, draw=draw, **win_kwargs)
1005
+ finally:
1006
+ RenderHost._active.pop()
1007
+ self._last_return = result if isinstance(result, tuple) else (False, result)
1008
+
1009
+ return self._last_return[1]
1010
+
1011
+ def __repr__(self):
1012
+ w = getattr(self.io_function, "__name__", self.io_function)
1013
+ flags = [f for f in ("hidden" if self.hidden else "", "dirty" if self._external_change else "",
1014
+ "" if self.standalone else "embedded") if f]
1015
+ tail = f" [{', '.join(flags)}]" if flags else ""
1016
+ return f"RenderHost({self.name!r} -> {w}{tail}, {dict.__repr__(self)})"
1017
+
1018
+
1019
+ @render_func(use_cache=True, selectable=False, temp=True, view_func_selection=False)
1020
+ def render_host_view(input_value, external_change=False, draw=False, draw_state=None, name=None, **kwargs):
1021
+ """Window envelope + wrapper driver for a RenderHost (draw_main → host.draw()).
1022
+
1023
+ A render_func so the host gets a Melty window (mode=Mode.WINDOW chrome here) and a
1024
+ persistent draw_state. It runs the host's wrapper with the host's private
1025
+ view_func, computes the wrapper's `external_change` (the resolved input changed
1026
+ since last frame — so convert's chain_in re-parses on new source; code_file_io
1027
+ self-manages and ignores it), and writes the wrapper's output back to the upstream
1028
+ proxy on edit.
1029
+
1030
+ Resolves its host by NAME, not the ambient `_active` stack: a Mode.WINDOW body
1031
+ renders DEFERRED (at frame end), after `draw()` has popped `_active`, so
1032
+ `RenderHost.current()` is None by then — the name (a passed kwarg) survives."""
1033
+ host = RenderHost.current()
1034
+ if host is None and name is not None:
1035
+ host = next((h for h in Melty.render_hosts.values() if h.name == name), None)
1036
+ if host is None:
1037
+ return False, input_value
1038
+ host._draw_state = draw_state
1039
+
1040
+ # data-bag (no wrapper): just render the dict itself.
1041
+ if host.io_function is None:
1042
+ from meltygui.view.collection_view import draw_collection
1043
+ return draw_collection(host, name=host.name)
1044
+
1045
+ # external_change for the wrapper: the framework's, OR the input-changed flag that
1046
+ # draw() set this frame (computed there because draw() runs every frame; consuming
1047
+ # it here, in the deferred/cached body, is reliable since draw() also runs us).
1048
+ ext = bool(external_change or host._pending_external)
1049
+
1050
+ # return_extras=True → the wrapper hands back its OWN draw_state as a 3rd value;
1051
+ # store it so a later value edit can invalidate it (see _mark_changed) and the
1052
+ # wrapper actually re-runs instead of replaying its blit cache. NOTE: reset
1053
+ # _pending_external AFTER the wrapper - _internal_view_func reads it (during this
1054
+ # call) to tell a genuine upstream change from the wrapper's own per-keystroke pulse.
1055
+ host.child_kwargs['temp'] = True
1056
+ notify(f"{host.io_function.__name__}", tag="host", tint=(1, 0.5, 0.5))
1057
+ host.child_kwargs["input_value"] = input_value
1058
+ result = host.io_function(draw=draw, view_func=host._internal_view_func,
1059
+ external_change=ext, return_extras=True, **host.child_kwargs)
1060
+ host._pending_external = False
1061
+ if isinstance(result, tuple) and len(result) == 3:
1062
+ edited, out, host._wrapper_draw_state = result
1063
+ else:
1064
+ edited, out = result
1065
+
1066
+
1067
+ # Cross-proxy write-back: the wrapper's output (e.g. convert's new source string)
1068
+ # lands on the upstream proxy, which goes dirty and saves one frame later.
1069
+ if edited and isinstance(host.input_value, RenderHost):
1070
+ host.input_value._set_held(out)
1071
+ # A PROGRAMMATIC edit just serialized (chain_out from a param panel /
1072
+ # lens / live-view window - typing never takes this branch: keystrokes
1073
+ # enter the str host directly and don't chain_out). The code editor
1074
+ # over this file is also consumer of this dict host (notify_on_change,
1075
+ # draw_text_from_code_cache), but the regular consumer notify is
1076
+ # trailing-debounced ~2s against typing and re-armed by the
1077
+ # _mark_changed - under quick successive edits it starves and the
1078
+ # editor keeps showing the pre-edit cache. Notify NOW: one fan-out per
1079
+ # completed edit round, the editors re-run and read the fresh held
1080
+ # string.
1081
+ host._notify_consumers_now("programmatic edit write-back")
1082
+
1083
+ return edited, out