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,2394 @@
1
+ from collections import defaultdict
2
+
3
+ from enum import Enum
4
+
5
+ import meltygui.core.windowing.window_api as glfw
6
+ import meltygui_imgui as imgui
7
+ from meltygui.hdr_color import pack_color
8
+
9
+ from meltygui.core.conversion.dict_conversion import DictConversion
10
+ from meltygui.core.runtime.toggles import shadow_depth_at
11
+ from meltygui.core.runtime.toggles import Toggles
12
+ from meltygui.core.windowing.glfw_utils import print_stack_trace
13
+ from meltygui.core.conversion.cache_tree import CacheTree
14
+ from meltygui.core.conversion.cache_tree import UNSET_VALUE
15
+ from meltygui.core.rendering.core_decoration import no_save
16
+ from meltygui.core.rendering.core_decoration import exclude
17
+ from meltygui.core.rendering.core_decoration import deep_refresh
18
+ from meltygui.core.rendering.core_decoration import no_save_exclude
19
+ from meltygui.core.rendering.core_decoration import Core
20
+ from meltygui.core.rendering.core_decoration import defaults
21
+
22
+
23
+ class SynthColors(DictConversion):
24
+ def __init__(self):
25
+ super().__init__()
26
+ self.letter_to_color = {}
27
+
28
+
29
+ class CSTDrawBits:
30
+ def __init__(self):
31
+ super().__init__()
32
+ self.path_key: tuple[tuple[str, int | None], ...] = ()
33
+ self.module_id: str = ""
34
+ self.root_gen: int = 0
35
+ self.anchor: tuple = ()
36
+ self.text_buf: str = "" # generic edit buffer
37
+
38
+
39
+ class DragMode(Enum):
40
+ NONE = 'none'
41
+ WINDOW = 'move'
42
+ RESIZE_BR = 'resize_br'
43
+
44
+
45
+ class Anchor(Enum):
46
+ TOP_LEFT = 'top_left'
47
+ TOP_CENTER = 'top_center'
48
+ TOP_RIGHT = 'top_right'
49
+ CENTER_LEFT = 'center_left'
50
+ CENTER = 'center'
51
+ CENTER_RIGHT = 'center_right'
52
+ BOTTOM_LEFT = 'bottom_left'
53
+ BOTTOM_CENTER = 'bottom_center'
54
+ BOTTOM_RIGHT = 'bottom_right'
55
+
56
+
57
+ # Anchor classification used for both anchor_offset (window placement) and
58
+ # clip-rect pinning. Anything not in left/right is horizontally centered;
59
+ # anything not in top/bottom is vertically centered.
60
+ class Pin(Enum):
61
+ """Symbolic pin targets for ``pin_to_clip`` when the caller doesn't have a
62
+ draw_state reference handy. Resolved to the declaring view's relatives at
63
+ render time: PARENT -> ``_parent`` (immediate render-tree parent), WINDOW ->
64
+ ``parent_window`` (enclosing Melty window)."""
65
+ PARENT = 'parent'
66
+ GRANDPARENT = 'grandparent'
67
+ WINDOW = 'window'
68
+ # The active clip rect (scissor test) rather than a view's box. Clamped to
69
+ # the parent window so corners track the visible edge - e.g. bottom becomes
70
+ # min(parent_bottom, clip_bottom). Useful for overlays that must fit inside
71
+ # a scrolled/clipped region.
72
+ CLIP = 'clip'
73
+
74
+
75
+ LEFT_ANCHORS = (Anchor.TOP_LEFT, Anchor.CENTER_LEFT, Anchor.BOTTOM_LEFT)
76
+ RIGHT_ANCHORS = (Anchor.TOP_RIGHT, Anchor.CENTER_RIGHT, Anchor.BOTTOM_RIGHT)
77
+ TOP_ANCHORS = (Anchor.TOP_LEFT, Anchor.TOP_CENTER, Anchor.TOP_RIGHT)
78
+ BOTTOM_ANCHORS = (Anchor.BOTTOM_LEFT, Anchor.BOTTOM_CENTER, Anchor.BOTTOM_RIGHT)
79
+
80
+
81
+ class ApplyMode(Enum):
82
+ INSTANT = 'instant'
83
+ ON_RELEASE = 'on_release'
84
+ CONFIRM = 'confirm'
85
+
86
+
87
+ class ExpandMode(Enum):
88
+ """How the render_func wrapper treats a collapsed (is_tree) view.
89
+
90
+ AUTO - wrapper owns collapse: the view func is skipped while collapsed
91
+ and the box shrinks to its header (default, legacy behavior).
92
+ MANUAL - the view func is ALWAYS called and reads draw_state.expanded to
93
+ choose its own collapsed rendering (e.g. draw_comment showing
94
+ just the first line). The wrapper skips its collapsed-state
95
+ sizing shortcuts so whatever the func draws is measured normally.
96
+ """
97
+ AUTO = 'auto'
98
+ MANUAL = 'manual'
99
+
100
+
101
+ @no_save_exclude()
102
+ class TabState(DictConversion):
103
+ def __init__(self):
104
+ super().__init__()
105
+ self.selected_tabs = []
106
+ self.tab_tints = {}
107
+ self.tab_icons = {}
108
+
109
+
110
+ class ColorPickerState(DictConversion):
111
+ """draw_color_picker's persisted state (injected via
112
+ `picker_state: ColorPickerState = None`): which tab is showing —
113
+ "wide" (the P3 + brightness square, the default), "srgb" (the
114
+ classic square) or "extended" (the classic square with the P3
115
+ extension to its right, the sRGB+ tab)."""
116
+
117
+ def __init__(self):
118
+ super().__init__()
119
+ self.tab = "wide"
120
+
121
+
122
+
123
+
124
+ @no_save("fit_phase")
125
+ class ContextMenuWindowState(DictConversion):
126
+ """Per-menu persisted state for the context menu WINDOW, draw_context_menu
127
+ (injected via `menu_state: ContextMenuWindowState = None` — the TabState
128
+ pattern). Not the input tab's host cache, new_core_view.ContextMenuState."""
129
+
130
+ def __init__(self):
131
+ super().__init__()
132
+ # True once the menu's first-load auto-fit has sized the window.
133
+ # PERSISTED on purpose: the menu draw_state (so its width/height)
134
+ # survives a restart, and a menu restored open must not be re-fitted -
135
+ # it overwrote the user's size with the default width + a content
136
+ # fit (an underscore draw_state attr never serialized, 08-27).
137
+ self.fit_done = False
138
+ # The fit's two-frame sequence (0: stamp header width, 1: measure
139
+ # height). Session-only - a fit never spans a restart.
140
+ self.fit_phase = 0
141
+
142
+
143
+ @exclude("restore_first_line", "restore_total_lines", "restore_text",
144
+ "restore_gutter_digits", "restore_line_offset", "restore_fold_keys",
145
+ "restore_gutter_rows", "restore_diff_collapsed", "restore_diff_rows",
146
+ "restore_preview_rows")
147
+ class TextEditorState(DictConversion):
148
+ """Per-editor persisted UI state for draw_text (injected via
149
+ `text_editor_state: TextEditorState = None` — the TabState pattern:
150
+ created into draw_state.misc and serialized with it)."""
151
+
152
+ def __init__(self):
153
+ super().__init__()
154
+ self._completion_explicit = False
155
+ self._token_matches = None
156
+ self._completion_buffer = None
157
+ self._completion_anchor = None
158
+ self._signature_dismissed = None
159
+ # {def_name: bool} - whether that function's parameter window is
160
+ # visible. The def widget reads/writes this bool DIRECTLY each
161
+ # render (visibility IS this bool); persisted, so a fresh session
162
+ # re-opens the panels that were open.
163
+ self.params_windows_open = {}
164
+ # {def_name: True} - whether that function's param panel re-runs
165
+ # Run Visualize automatically on every param edit. The panel's
166
+ # Auto Execute checkbox reads/writes this directly (the
167
+ # params_windows_open pattern); persisted with the editor state.
168
+ self.params_auto_execute = {}
169
+ # Width the Ctrl+B usage picker was drag-resized to, or None while
170
+ # it still fits its content; its height always fits the rows
171
+ # (usage_picker.py).
172
+ self.usage_picker_width = None
173
+ # Viewability snapshot for the instant-restore placeholder: what this
174
+ # editor SHOWED last frame - the visible band's first (display)
175
+ # line, its text, and the buffer's total line count. draw_text
176
+ # writes these at the end of every real draw and, when called with
177
+ # input_value=None (buffer still loading, e.g. draw_text_editor's
178
+ # loading branch), rebuilds a same-shape stand-in from them: blank
179
+ # lines up to the band, the band's text, blank lines below - same
180
+ # line count → same content height → the persisted scroll stays
181
+ # unmoved, and the first frame of a session shows the code the last
182
+ # one did. @exclude: pure passive state (all renders stamp
183
+ # them mid-session), so their per-scroll writes must not invalidate.
184
+ self.restore_first_line = 0
185
+ self.restore_total_lines = 0
186
+ self.restore_text = ""
187
+ # Gutter shape for the stand-in: digit count (0 = no gutter - the
188
+ # width decides where the code column starts, so a missing gutter
189
+ # made the text shift left↔right on the swap-in) and the sequential
190
+ # numbering offset (the number on buffer line 0 minus 1).
191
+ self.restore_gutter_digits = 0
192
+ self.restore_line_offset = 0
193
+ # Per-display-row gutter of the snapshot band, exactly as painted -
194
+ # ints are line NUMBERS (fold-remapped, so they skip across
195
+ # collapsed folds), -1/-2 denote a fold-header chevron row
196
+ # (expanded/collapsed), None a numberless row. Without this the
197
+ # stand-in numbered rows sequentially from restore_line_offset -
198
+ # wrong below every collapsed fold, and chevron-less - so the
199
+ # buffer visibly snapped when the real text landed.
200
+ self.restore_gutter_rows = None
201
+ # This editor's collapsed folds as their LINE-INDEPENDENT keys
202
+ # (ds._fold_keys - scope qualnames / header texts, designed to
203
+ # survive folds). ds._fold_keys is an underscore slot and not
204
+ # serialized, so fold state used to RESET to default_collapsed
205
+ # every session. draw_text seeds the fresh draw seed from this
206
+ # instead. None = never captured (let defaults seed); [] = captured
207
+ # with everything expanded (defaults must NOT re-collapse).
208
+ self.restore_fold_keys = None
209
+ # The compare split's hand-toggled DIFF gap state (ds
210
+ # ._diff_fold_collapsed - (header, last hidden line) buffer-line
211
+ # tuples; no line-independent keys exist for gaps; they re-derive
212
+ # from the live diff). Written by the snapshot block whenever the
213
+ # diff layer is active; a fresh draw_state in NEUTRAL mode
214
+ # (expand_diff None - the active switch is its own truth) maps
215
+ # them onto the current pieces by overlap, exactly like an edit's
216
+ # drift. None = never captured.
217
+ self.restore_diff_collapsed = None
218
+ # The band's diff-gap header rows, as painted: {band row offset:
219
+ # hidden line count} - 0 for an expanded gap's header, N for a
220
+ # collapsed one (its "N lines" label). The diff layer sits stand-in
221
+ # frames out (it needs the real buffer), so without this the
222
+ # stand-in painted a collapsed compare split as CONTINUOUS code -
223
+ # grey mid-row chevrons, no separator bands, no counts, no preview
224
+ # fade - and the collapsed look only arrived with the real text,
225
+ # seconds after boot (Lukas 09-01: "loads uncollapsed then 2
226
+ # seconds later makes the switch"). The stand-in paints its bands
227
+ # and labels from these rows instead. None = never captured.
228
+ self.restore_diff_rows = None
229
+ # The row offsets that were preview-faded (the rows around a
230
+ # collapsed diff gap, Toggles.TextEditor.diff_preview_lines_*), so
231
+ # the stand-in fades the same rows. None = never captured.
232
+ self.restore_preview_rows = None
233
+
234
+
235
+ @no_save_exclude("selected", "open_path", "cursor_path", "search_query", "search", "_focus_search",)
236
+ class DropDownState(DictConversion):
237
+ def __init__(self):
238
+ super().__init__()
239
+ self.selected = None
240
+ # The single chain of branch labels currently expanded, e.g. ("color",
241
+ # "rgb"). One path only - guarantees at most one sub-menu open per level.
242
+ self.open_path = ()
243
+ # Full key-path of the keyboard/hover-highlighted row (includes the leaf),
244
+ # e.g. ("color", "rgb", "red"). Drives arrow-key navigation + the
245
+ # highlight; open_path is derived from it (branch -> itself, leaf -> parent).
246
+ self.cursor_path = ()
247
+ # Live search box state (our own, not the framework search). search_query
248
+ # is the raw text the root menu's draw_text edits; search is its lowercased
249
+ # form, propagated to each level for filtering. _focus_search asks the box
250
+ # to grab text focus once (set when the menu opens); _search_box_tile is
251
+ # the box's tile id so key-nav can tell when the box holds focus.
252
+ self.search_query = ""
253
+ self.search = ""
254
+ self._focus_search = 0
255
+ self._search_box_tile = None
256
+ # True once the search box has held text focus this open. Lets us tell a
257
+ # brand-new open (still acquiring focus) from focus genuinely leaving the
258
+ # dropdown; the latter closes us, keeping text focus and open in lock-step.
259
+ self._had_focus = False
260
+ # The committed selection's key-path + text label (the trigger shows the
261
+ # label, e.g. "red", not the raw value). _picked_path is stamped by the row
262
+ # click / Enter so the path survives the close that resets cursor_path.
263
+ self.selected_path = ()
264
+ self.selected_label = ""
265
+ self._picked_path = ()
266
+ # Keyboard-select mode: once an arrow key is pressed, we keep moving the
267
+ # highlight until the mouse moves. _last_mouse detects that movement.
268
+ self._kbd_mode = False
269
+ self._last_mouse = None
270
+ # The popover's (width, height) when the user drag-resized it -
271
+ # persisted, re-applied on every later open; None = size to content
272
+ # (draw_dropdown's fit). _menu_ds is the popover's draw_state and
273
+ # _menu_fit the last size draw_dropdown stamped on it: a size that
274
+ # differs from _menu_fit is the wrapper's resize handle at work.
275
+ self.menu_size = None
276
+ self._menu_ds = None
277
+ self._menu_fit = None
278
+
279
+
280
+ class ContextMenuItemsState(DropDownState):
281
+ """The `context_menu={label: callable}` popover's dropdown state — the
282
+ root_state of the draw_dd_menu it opens — one per view carrying a menu,
283
+ kept in that view's draw_state.misc like an injected state
284
+ (new_core_view.draw_context_menu_items). Adds where the menu opened."""
285
+
286
+ def __init__(self):
287
+ super().__init__()
288
+ # The right-click's position relative to the view's top-left: the
289
+ # popover's window_pos, re-applied every frame so it stays put.
290
+ self.open_at = (0, 0)
291
+
292
+
293
+ @no_save("open_title", "_prev_text_focus", "_hovered_title")
294
+ class MenuBarState(DictConversion):
295
+ """draw_menu_bar's injected state (view/core_views/menu_bar.py): which
296
+ title's menu is showing and one DropDownState per title, so every menu
297
+ keeps its own open/cursor paths and its drag-resized menu_size — the
298
+ same per-popover state draw_dropdown keeps, one per menu."""
299
+
300
+ def __init__(self):
301
+ super().__init__()
302
+ # The title (top-level key of the bar's dict) whose menu is open;
303
+ # None while the bar is idle. Not saved: an open menu means nothing
304
+ # next session.
305
+ self.open_title = None
306
+ # title -> DropDownState. menu_size saved per menu.
307
+ self.menus = {}
308
+ # The last selection's full key-path, ("File", "Recent", "a.py").
309
+ self.selected_path = ()
310
+ # Who held text focus when a menu opened, handed focus back on close
311
+ # so a click out of the Edit menu leaves the editor typing again.
312
+ self._prev_text_focus = None
313
+ # The title under the pointer at the last run (hover-switching edge-cases).
314
+ self._hovered_title = None
315
+
316
+
317
+ @exclude("zoom", "center_u", "center_v", "brightness", "contrast", "hue", "saturation")
318
+ class ZoomState(DictConversion):
319
+ def __init__(self):
320
+ super().__init__()
321
+ self.zoom_level = 1.0
322
+ self.offset_x = 0.0
323
+ self.offset_y = 0.0
324
+
325
+ self.center_u = 0.5
326
+ self.center_v = 0.5
327
+ self.zoom = 1.0
328
+ self.brightness = 0.0
329
+ self.contrast = 1.0
330
+ self.hue = 0.0
331
+ self.saturation = 1.0
332
+
333
+
334
+ class AttrDict:
335
+ __slots__ = ('_data',)
336
+
337
+ def __init__(self, data):
338
+ object.__setattr__(self, '_data', data)
339
+
340
+ def __getattr__(self, name):
341
+ # `_data` itself must raise, not route through here: during deepcopy/
342
+ # pickle reconstruction the slot is briefly unset, and `self._data`
343
+ # would re-enter __getattr__('_data') -> infinite recursion. Dunders
344
+ # raise AttributeError so copy/pickle/unwrap protocol probes (__setstate__,
345
+ # __deepcopy__, ...) read as exceptions instead of a None that some
346
+ # protocols might try to call.
347
+ if name == "_data" or (name.startswith("__") and name.endswith("__")):
348
+ raise AttributeError(name)
349
+ return self._data.get(name, None)
350
+
351
+
352
+ def __setattr__(self, name, value):
353
+ # The `_data` slot is set directly; everything else is a data key.
354
+ # Without this, copy/pickle reconstruction (which setattrs `_data` back
355
+ # onto a fresh, slot-unset instance) would route into `self._data[...]`
356
+ # and hit the unset slot -> AttributeError. Pairs with __getattr__'s
357
+ # `_data` guard to make AttrDict round-trip through copy/pickle.
358
+ if name == "_data":
359
+ object.__setattr__(self, name, value)
360
+ return
361
+ self._data[name] = value
362
+
363
+ def rebind(self, data):
364
+ object.__setattr__(self, '_data', data)
365
+
366
+ @exclude("_items", "_cursor")
367
+ class CursorStack:
368
+ def __init__(self):
369
+ self._items = []
370
+ self._cursor = 0
371
+
372
+ def push(self, item):
373
+ # remove anything after cursor if you've popped, then append
374
+ self._items = self._items[:self._cursor]
375
+ self._items.append(item)
376
+ self._cursor += 1
377
+
378
+ def pop(self):
379
+ if self._cursor == 0:
380
+ raise IndexError("pop from empty stack")
381
+ self._cursor -= 1
382
+ return self._items[self._cursor]
383
+
384
+ def peek(self):
385
+ if self._cursor == 0:
386
+ raise IndexError("peek at empty stack")
387
+ return self._items[self._cursor - 1]
388
+
389
+ @property
390
+ def history(self):
391
+ return list(self._items)
392
+
393
+ def __repr__(self):
394
+ return f"{self._items} cursor={self._cursor}"
395
+
396
+ class TileMode(Enum):
397
+ MAX = 'max'
398
+ MIN = 'min'
399
+ NONE = 'none'
400
+ NO_MASK = 'no_mask'
401
+
402
+
403
+ # Search state persists across reloads: search_text, search_active, and
404
+ # text_search_current are saved so the find box reopens with the last query and
405
+ # selected match. text_search_count is derived (recomputed each frame) and stays
406
+ # transient below.
407
+ @no_save("mouse_btn_state", "mouse_up", "mouse_down", "unique",
408
+ "shadow", "size_change", "drag_released", "clicked", "dragged", "clipped_by_rect",
409
+ "dragged", "expanded_height", "clipped", "fully_clipped", "inside_clip",
410
+ "overhead_time", "scroll_visible", "imgui_is_toggled_open", "top", "left",
411
+ "hotkey_receiver", "use_child", "cst", "bg_color", "depth", "return_item",
412
+ "is_active", "clip_rect", "wrapped_top", "current_tint", "wrapped_left", "multi_line", "relative_pos", "footer_width", "footer_height",
413
+ "min_width", "min_height", "max_height", "is_focused", "drag_window_pos_x", "drag_window_pos_y",
414
+ "drag_mode", "is_hovered_last", "bg_shown", "draw_window_pos_x", "z_offset", "melty_window",
415
+ "misc_used", "draw_window_pos_y", "drag_delta", "screen_pos", "hover_rects", "melty_window", "auto_resize",
416
+ "imgui_is_item_activated", "frame_count", "text_search_count",
417
+ # A context menu open at quit came back open at the next boot,
418
+ # drawn under the view that spawned it (09-13, melty_code_editor).
419
+ "context_menu_open")
420
+ @exclude("current_tint", "overhead_time", "premature_break", "drag_mode",
421
+ "clip_rect", "_input_value", "flow_spacing", "expanded_rect", 'max_column', 'text_selection_start', 'text_selection_end',
422
+ 'width', "size_change", 'left', 'top', "clipped", "fully_clipped", "melty_window", "text_double_click_time", "text_cursor_blink_time",
423
+ "hovered", "wrapped_top", "params", "scroll_visible", "depth_and_layer", "clipped_by_rect", "multi_line", "text_prev_cursor_pos",
424
+ "text_cursor_pos", "text_h_scroll", "text_selection_start", "text_selection_end", "text_is_focused", "text_cursor_blink_time",
425
+ "premature_break", "wrapped_left", "_did_use_cache", "hover_rects", "window_pos", "content_width", "code_state",
426
+ "content_region", "value_hash", "drag_window", "content_region", "did_render", "footer_height", "footer_width",
427
+ "bounding_hovered", "dlt_count", "clip_rect",
428
+ "scrolled", "is_hovered_last", "frame_count", "z_pos",
429
+ "text_search_current", "text_search_count", "observed_content_height",
430
+ # The context menu's Eval tab: typed per keystroke and must not
431
+ # re-render the inspected view (the eval fires on Run / Enter).
432
+ "eval_code")
433
+ @no_save_exclude('_window_visibility_initialized', 'render_time', "total_z_offset", 'closable', 'has_full_tile', 'invalid_content_height',
434
+ "parent_window", "pressed", "bbox", "", "child_selected", "bg_color",
435
+ 'hover_rects', 'nested_window', 'use_cache', "header_top", "header_left", "left_offset",
436
+ "top_offset", 'kwargs', "just_shadow",
437
+ "header_natural_width", "max_header_width", "pin_to_clip", "pin_clip_rect", "pin_clamp",
438
+ "header_left_delta", "header_top_delta", "last_seen", "persistent", "shadow_margin", "bg_depth",
439
+ "anchor_pos", "parent_anchor_pos", "pin_to_clip", "pin_clip_rect", "just_shadow", 'hover_reported', 'explain_convert',
440
+ 'channel', 'next', 'previous', 'index_in_parent',
441
+ '_hover_eligible', 'just_shadow')
442
+ @deep_refresh( 'closed', 'search_text', 'search_active')
443
+ class DrawState(DictConversion):
444
+ """Holds per-widget runtime state (expand/collapse, etc.)."""
445
+
446
+ # _ancestor_scroll cache, keyed on (frame_count, Melty.scroll_version).
447
+ # Class-level defaults so pre-existing/deserialized instances resolve them
448
+ # without __init__; written via object.__setattr__ (never accessed or
449
+ # serialized; to_dict only walks the default instance's fields).
450
+ _anc_scroll_key = None
451
+ _anc_scroll_cache = (0, 0)
452
+
453
+ # Hot reload fallback: live instances predating the persisted rename read
454
+ # this, the wrapper's post-body stamp writes the instance attr.
455
+ observed_content_height = 0
456
+ # Same fallback for the persisted Eval snippet (context menu's Eval-tab).
457
+ eval_code = None
458
+
459
+ def __init__(self):
460
+ super().__init__()
461
+ self._external_change = False
462
+ # The context menu's Eval-tab snippet for THIS view, persisted so the
463
+ # tab reopens with what was last typed. Stored on the inspected view's
464
+ # draw_state (not the tab's) because the eval fires in this view's
465
+ # render wrapper and a scope-up event retargets the tab at an ancestor.
466
+ self.eval_code = None
467
+ self._input_value_cache = UNSET_VALUE
468
+ self._output_value_cache = UNSET_VALUE
469
+ self._file_meta = UNSET_VALUE
470
+ self._loading = False
471
+ self._pending = False
472
+ self._running = False
473
+ self.watch = ""
474
+ self._default_view_func = None
475
+
476
+ # Chain cache, split based on type, UNSET_VALUE as a default
477
+ self._chain_stack = CacheTree()
478
+ self._cursor_start_pos = None
479
+
480
+ self._children = {}
481
+ self._view_children = {}
482
+ self._wrapper = None
483
+ self._parent_ctx = None
484
+ self._bg_depth = 0
485
+ self._current_tint = None
486
+ self._bg_stack = None
487
+ self._parent = self
488
+ self._is_header = False
489
+ self._view_func = None
490
+ self._cursor_pos = (0, 0)
491
+ self._cursor_screen_pos = (0, 0)
492
+ self._source = defaultdict(dict)
493
+ self.next = None
494
+ self.previous = None
495
+ self.index_in_parent = 0
496
+ self.misc = {}
497
+ self._clean_args = {}
498
+ self._func = None
499
+ self.misc_used = set()
500
+ self.closed = False
501
+ self._window_visibility_initialized = False
502
+ self.frame_count = 0
503
+ # corner_radius is no longer a declared field - it migrated to the
504
+ # auto state system (see core_render's auto-state block): views that
505
+ # tune it declare a named `corner_radius` param (e.g. button), the
506
+ # default bg path stamps it from there, and background painters read
507
+ # it via getattr(ds, 'corner_radius', 6).
508
+ self.nested_window = False
509
+ self.use_cache = False
510
+ self.depth = 0
511
+ self.layer = 0
512
+ self.channel = 0
513
+ self.last_seen = None
514
+ self.z_offset = 0
515
+
516
+ self.total_z_offset = 0
517
+ self.shadow_margin = 0
518
+ self.bg_depth = 0
519
+ self.bg_color = (0, 0, 0, 0)
520
+ self.pressed = False
521
+ self.closable = False
522
+ self.has_full_tile = False
523
+ self.behind = False
524
+ self.tile_mode = TileMode.MAX
525
+
526
+
527
+ self.context_menu_open = False
528
+ self.context_menu_ds = None
529
+ self.context_menu_offset = 0
530
+ self._offset_ds = None
531
+ # Set on a context menu's target draw_state to reopen it after a deferred
532
+ # per-view screenshot lands (see screenshot.process_take_screenshot_flags).
533
+ self._reopen = False
534
+ # Resolved call site (filename, line) of where this widget was invoked,
535
+ # computed ONCE in the inline render pass of first frame its context menu
536
+ # is opened (see core_render) and cached until app restart. We cache the
537
+ # resolved tuple - NOT the raw frames - so the inspect-arg lens / jump
538
+ # button read a stable value; re-walking the live stack each frame would
539
+ # flip mid-drag (the "skip parents" optimization changes the call stack).
540
+ # Underscore-prefixed → not serialized.
541
+ self._call_site = None
542
+ # The whole UNfiltered call stack (innermost-first list of (filename,
543
+ # lineno, func_name)), captured alongside _call_site. draw_context_menu
544
+ # renders every frame, filtering per-frame: a real call site shows an
545
+ # editable code_file_io, a machinery/ignored frame just a label.
546
+ self._call_stack = []
547
+ # Queue-time call stack for a DEFERRED layer (a view drawn at end of frame).
548
+ # At end-of-frame dispatch the live stack is just the layer-loop machinery,
549
+ # so a view rendered inside a deferred layer has a _call_stack that bottoms
550
+ # out there. The original caller chain that QUEUED the layer is captured
551
+ # preemptively at queue-time (core_render, deferred pass) into this field;
552
+ # draw_context_menu appends it to complete a descendant's stack.
553
+ # _is_deferred_layer marks a queued view; _deferred_stack_requested is the
554
+ # lazy capture gate, set by a descendant's open menu and consumed next frame.
555
+ self._deferred_call_stack = []
556
+ # The same queue-time stack with LOCALS - (path, lineno, func_name,
557
+ # locals) outermost first, the debug tab's format (_call_stack_frames)
558
+ # - so a descendant's stack trace can be spliced onto the chain that
559
+ # queued this layer and rendered as one logical call. None until captured.
560
+ self._deferred_call_stack_frames = None
561
+ self._is_deferred_layer = False
562
+ self._deferred_stack_requested = False
563
+ self._call_site_captured = False
564
+ # Set by the context menu when it walks UP to this draw state via the
565
+ # up-arrow offset; this view never had its own menu open, so the normal
566
+ # context_menu_open capture gate never fired. The flag causes the next
567
+ # inline render will capture the call site anyway (lazy, one-shot).
568
+ self._call_site_requested = False
569
+ # Per-frame caches for the position/clip chain. abs_left/abs_top/abs_clip_rect/
570
+ # pin_rect/clip_anchor_base are pure functions of the draw_state tree but were
571
+ # recomputed many times per frame (the left property is hit by parent walks,
572
+ # pin clamping, clip checks, and BVH lookups), with each computation re-walking
573
+ # the parent chain. With pin_clamp the recursion compounded - ~600K _abs_left
574
+ # calls per ~25 draw_tint_context frames in practice. Cache by frame_count;
575
+ # the recursive internal walks all go through the cached property so an
576
+ # ancestor's value is computed at most once per frame.
577
+ # abs_left/abs_top are keyed by (frame_count, left_offset, top_offset,
578
+ # window_pos) - NOT frame alone. The wrapper sets left_offset/top_offset
579
+ # mid-frame (core_render.py:393, again in the columns branch :1235) to
580
+ # position the widget at the current imgui cursor; a frame-only key
581
+ # cached the first set_offset value and the column re-set never landed,
582
+ # which is what broke column rendering with the earlier cache.
583
+ self._abs_left_key = None
584
+ self._abs_left_cache = 0
585
+ self._abs_top_key = None
586
+ self._abs_top_true_key = None
587
+
588
+ self._abs_top_cache = 0
589
+ self._abs_top_true_cache = 0
590
+ self._abs_content_height_cache = 0
591
+ self._abs_content_height_key = None
592
+ # pin_rect/clip_anchor_base keys widen over frame_count too: the wrapper
593
+ # writes pin_target/pin_clip_rect/pin_clamp/parent_anchor_pos mid-frame
594
+ # (core_render.py:995–1041), AFTER earlier hover/clip code may have already
595
+ # cached a pre-anchor value. Frame-only caching made nested-window anchors
596
+ # take a frame to settle.
597
+ self._pin_rect_key = None
598
+ self._pin_rect_cache = None
599
+ self._clip_anchor_base_key = None
600
+ self._clip_anchor_base_cache = None
601
+
602
+ self.relative_pos = None
603
+
604
+ self._first_draw_state = None
605
+ self.melty_window = False
606
+ self.persistent = True
607
+ self.child_selected = None
608
+ self.just_shadow = False
609
+ self._event_names = set()
610
+ # Body-level on_action registrations from the last real render, replayed
611
+ # by the wrapper for blit-cache hits: (frame_count, [entries]). See
612
+ # on_action / replay_body_actions.
613
+ self._body_actions = None
614
+ # Rect-scoped event PARAMS from the last real render (event_rect):
615
+ # {param_name: [rect relative to (abs_left, abs_top), ...]}. The
616
+ # wrapper consumes and clears it before each body run.
617
+ self._event_rects = None
618
+
619
+ self._queued_windows = []
620
+ self.drag_window_pos_x = None
621
+ self.drag_window_pos_y = None
622
+ self._bounding_hovered = False
623
+ # Imgui state mirror
624
+ self.is_active = False
625
+ self.is_focused = False
626
+ self.scroll_offset = (0, 0)
627
+ # Authoritative max scroll_offset.y, published by core_render's scroll
628
+ # handler each frame. Descendants (text editor drag-auto-scroll) read it
629
+ # so their clamp matches exactly & the two don't fight (bottom flicker).
630
+ self._max_scroll_y = None
631
+ self._imgui_is_active = False
632
+ self._imgui_is_activated = False
633
+ self._imgui_is_focused = False
634
+ self._imgui_is_hovered = False
635
+ self._imgui_is_item_hovered = False
636
+
637
+ self._imgui_block_hovered = False
638
+ self._imgui_is_edited = False
639
+ self._imgui_popover_open = False
640
+ self.imgui_is_item_activated = False
641
+ self.inside_clip = True
642
+ self.fully_clipped = True
643
+
644
+ self._one_full_draw = False
645
+ self.scroll_visible = False
646
+ self._unmanaged_window = False
647
+ self.live = False
648
+ self._shadow_depth = 0
649
+ self.shadow = False
650
+ self.cst = None
651
+ self.window_pos = (0,0)
652
+ self.window_size = None
653
+ self._initial_window_pos = None
654
+ self._initial_window_pos_resize = None
655
+ self._initial_window_size = (300, 300)
656
+
657
+ self.drag_mode = DragMode.NONE
658
+ self.use_child = False
659
+ self.z_pos = 0
660
+ self.depth_and_layer = (0, 0)
661
+ self.content_height = 0
662
+ self.invalid_content_height = True
663
+ # True while this view is holding a placeholder height from the
664
+ # core_render.content_height fast path (never fully rendered yet). Cleared
665
+ # the frame it renders for real.
666
+ self._skipped_render = False
667
+ self.auto_resize = True
668
+ self._tile_id = None
669
+ self.imgui_is_toggled_open = False
670
+
671
+ self._previous_hash = None
672
+ self.tint = (0.11, 0.12, 0.14)
673
+ self.current_tint = None
674
+
675
+ self.unique = None # stable UI ID
676
+ self.expanded = True
677
+ self.name = ""
678
+ self.height = 0
679
+ self.expanded_height = None
680
+ self.width = 0
681
+ self.min_width = 0
682
+ self.min_height = 0
683
+ # The wrapper's max_height kwarg when the caller passes
684
+ # enforce_max_height=True, mirrored like min_height so the resize
685
+ # handlers (corner drag, the frame-edge solve) cap a closable window's
686
+ # height in realtime; None = uncapped (a bare max_height only bounds
687
+ # what the wrapper sizes - the resize handle stays free from it).
688
+ self.max_height = None
689
+ self.drag_window = False
690
+ self._left_rel = None
691
+ self._top_rel = None
692
+ self._min_width = None
693
+ self.top = None
694
+ self.left = None
695
+ self.left_offset = 0
696
+ self.top_offset = 0
697
+
698
+ self.left_offset_true = 0
699
+ self.top_offset_true = 0
700
+
701
+
702
+ self._draggable = False
703
+ self.search_text = ""
704
+ self.search_active = False
705
+ self._search_was_active = False
706
+ self._flow_spacing = 0.0
707
+ self.enabled = True
708
+ self._end_header_size = (0, 0)
709
+ self._header_height = 0
710
+ self._max_indent = 0
711
+ self._name_edit = False
712
+ self._screen_pos = (0, 0)
713
+ self._did_use_cache = False
714
+ self.track_mouse = False
715
+ self._scroll_child = None
716
+ self._collection_draw_state = None
717
+ self._mouse_up = False
718
+ self.mouse_down = False
719
+ self.drag_released = False
720
+ self._hovered = False
721
+ self.hotkey_receiver = False
722
+ self._scrolled = False
723
+ self._clicked = False
724
+ self.dragged = False
725
+ self._screen_pos = (0, 0)
726
+ self.drag_delta = (0, 0)
727
+ self._input_value = UNSET_VALUE
728
+ self._raw_input_value = UNSET_VALUE
729
+ self._bypass_cache = False
730
+
731
+ self._input_cache = {"external_state": (UNSET_VALUE, 0, -1), # value, frame, hash
732
+ "internal_state":(UNSET_VALUE, 0)} # value, frame
733
+
734
+ self._collection = None
735
+ self._has_popup = False
736
+ self.is_hovered_last = False
737
+ self.parent_window = None
738
+ self.result = None
739
+ self.params = {}
740
+ self.wrapped_top = 0
741
+ self.wrapped_left = 0
742
+ self.header_height = 0
743
+ self.header_top = 0
744
+ self.header_left = 0
745
+ self.header_width = 0
746
+ self.header_end_width = 0
747
+ # Natural (pre-pad) width of this window's own header.
748
+ self.header_natural_width = 0
749
+ # Multi-window header widths: max_header_width is the stable value
750
+ # read while padding headers this frame. _max_header_width_acc is the
751
+ # running max accumulated as children render, committed at window push.
752
+ self.max_header_width = 0
753
+ self.clip_rect = None
754
+ # Parent window's abs_pos when clip_rect was captured. abs_clip_rect
755
+ # shifts the (absolute, not-based) clip_rect by the window's movement
756
+ # since then, so the clip tracks a window drag without a re-render.
757
+ self._clip_win_anchor = None
758
+ self.dlt_count = Core.melty.save_draw_state_for
759
+ self.premature_break = False
760
+ self.header_left_delta = 0
761
+ self.header_top_delta = 0
762
+ self.multi_line = False
763
+ self.footer_width = 0
764
+ self.footer_height = 0
765
+
766
+ self.content_width = 0
767
+ self.content_height = 0
768
+
769
+ # Profiling
770
+ self.render_time = 0.0
771
+ self.overhead_time = 0.0
772
+
773
+ ### Columns
774
+ self.final_max_column = 0
775
+ self._current_max_column = 0
776
+ # Per-divider pixel nudges applied left-to-right on top of the basic
777
+ # content_width/#columns split. offsets[c-1] shifts interior boundary
778
+ # c; values are never clamped (see column_boundary in core_render).
779
+ self.column_offsets = []
780
+ self._column_cursor = defaultdict(lambda: [0, 0])
781
+ self._melty_cursor = (0,0)
782
+ self._melty_content_height = 0
783
+
784
+ self._outside_column_height = 0
785
+ self._inner_cursor = 0 # column -> (x, y)
786
+ self._columns_top = None
787
+ self._max_column_height = 0
788
+ self._max_column_index = 0
789
+ self._columns_bottom = 0
790
+ ### End Columns
791
+ self._is_nested = False
792
+ self.anchor_pos = Anchor.TOP_LEFT
793
+ # Which point on the parent the child anchors to. Independent of
794
+ # anchor_pos (the child's own origin). Defaults to TOP_LEFT so parent
795
+ # anchoring is opt-in and legacy top-left layout is preserved.
796
+ self.parent_anchor_pos = Anchor.TOP_LEFT
797
+ # Floating views pin to a clip - resolved live from this Pin enum
798
+ # (PARENT / GRANDPARENT / WINDOW / CLIP) off .parent/parent_window at
799
+ # compute time, so the float tracks the target as it scrolls/moves
800
+ # without snapshotting anything. None = not pinned. Truthy when pinned.
801
+ # See pin_rect / _pin_rect.
802
+ self.pin_to_clip = None
803
+ self._kwargs = {}
804
+ self.kwargs = AttrDict({})
805
+ # Auto-state params (see the auto-state block in core_render.py).
806
+ # auto_params holds only DIVERGED values - what view code wrote via
807
+ # draw_state.<param> = x - keyed by param name. They inject into the
808
+ # kwargs gauntlet above the default layers (defaults and caller-passed
809
+ # kwargs still win). Non-underscore on purpose: it serializes whenever
810
+ # non-empty, so diverged state persists across sessions while
811
+ # at-default params save nothing. _auto_baseline mirrors last frame's
812
+ # RESOLVED value per param (what the caller handed the view); the
813
+ # divergence scan compares the live attr against it to detect writes.
814
+ # The the draw_state.<param> attrs themselves are dynamic (not
815
+ # declared here), so to_dict never serializes them directly.
816
+ self.auto_params = {}
817
+ self._auto_baseline = {}
818
+ self.hover_reported = True
819
+ self._start_z_pos = 3
820
+ self._column_width = 0
821
+ self._front_layer = False
822
+
823
+ # Used to cache abs_clip_rect
824
+ self.clipped_by_rect = None
825
+
826
+ # Set by Melty's end_frame tree rebuild: True while this nested
827
+ # window's spawning view is currently scrolled/clipped out of sight, so
828
+ # the window is skipped (not closed) by the layer dispatch and its
829
+ # subtree filtered out of bvh_query hits. Read via getattr (live
830
+ # instances predating the field won't have it).
831
+ self._hidden_offscreen = False
832
+
833
+ self._show_load = False
834
+ self._show_save = False
835
+ self._read_only = False
836
+ self._internal_pending = None
837
+ self._apply_load = None
838
+
839
+ self._save_pending = None
840
+ self._apply_save = None
841
+ self._pending_convert = False
842
+ self._save_pending_for = 0
843
+ self._save_pending_obj = None
844
+ self._all_pending = {"save_pending":None, "load_pending":None}
845
+ self._load_pending_for = 0
846
+ self._content_rect = (100,30)
847
+ # PERSISTED (non-underscore, @exclude'd): the wrapper's post-body
848
+ # observation. Serialized so a restored view's abs_content_height -
849
+ # and with it needs_scroll and the persisted scroll state - is right
850
+ # on a session's FIRST frame, before the view (or its loading
851
+ # stand-in) has rendered once.
852
+ self.observed_content_height = 0
853
+ self._last_expanded = None
854
+ self.expanded_rect = (0, 0, 0, 0)
855
+ self._collapsed_rect = (0, 0, 0, 0)
856
+ self._load_pending = False
857
+ self._save_pending = None
858
+
859
+ self.explain_convert = None
860
+
861
+ # Text input state
862
+ self.text_cursor_pos = 0
863
+ self.text_selection_start = 0
864
+ self.text_selection_end = 0
865
+ self.text_is_focused = False
866
+ self.text_cursor_blink_time = 0.0
867
+ self.text_double_click_time = 0.0
868
+ self.text_last_click_pos = -1
869
+ self.text_click_count = 0
870
+ self.text_h_scroll = 0.0
871
+ self.text_prev_cursor_pos = 0
872
+ self._name_color = (1,1,1)
873
+ # Drag granularity latched on mouse-down ('char' | 'word' | 'line') plus
874
+ # the anchor pos of the anchor word/line, so a double/triple-click drag
875
+ # extends by whole words/lines and keeps the anchor selected (IntelliJ
876
+ # style) instead of collapsing to a single caret.
877
+ self.text_drag_mode = 'char'
878
+ self.text_drag_anchor_lo = 0
879
+ self.text_drag_anchor_hi = 0
880
+
881
+ # Find-in-text search state. text_search_count/current are populated by
882
+ # draw_text each frame and consumed by the header's find UI (count +
883
+ # nav arrows). The underscore fields are private bookkeeping.
884
+ self.text_search_current = 0
885
+ self.text_search_count = 0
886
+ self._text_search_last_term = None
887
+ self._text_search_scroll_to = False
888
+
889
+ # Cross-view aggregation (search owner side). _search_session is a
890
+ # SearchTerm pushed onto Melty.search_stack each frame; the owner's
891
+ # pre-body search_walk pushes its term here and writes the combined
892
+ # result count into text_search_count for the find UI.
893
+ self._search_session = None
894
+ self._search_last_term = None
895
+ self._search_nav_pending = False
896
+ # Temporary state: set when search is opened (Ctrl+F) so the find box grabs
897
+ # text focus on the activating frame even if the underlying searchable
898
+ # view (the owner) currently holds melty's text focus. Consumed (cleared)
899
+ # by render_search after the box requests focus, so it won't keep
900
+ # yanking focus away from a later deliberate click into the editor.
901
+ self._search_focus_pending = False
902
+ # Set by the owner's search_walk to the local index of the global-current
903
+ # match when it lands in THIS view (else None). The view reads it while
904
+ # drawing to highlight/scroll to that match - so the count and the
905
+ # selection come from one source and can't disagree.
906
+ self._search_active_local = None
907
+ # draw_collection's copy of the key holding the global-current match.
908
+ self._search_current_key = None
909
+ # Closure set each render: (term, session) -> claims this view's own
910
+ # matches into the session without imgui. Driven by meltygui.search_walk.
911
+ self._search_matcher = None
912
+
913
+ self._print_last_invalid = False
914
+ self._last_invalidate = None
915
+ self._hover_eligible = 0
916
+
917
+ # BVH spatial index
918
+ self._bvh_id = None
919
+ self._bvh_bbox = None # cached bbox for delete operations
920
+
921
+
922
+ # File watch state (replaces _WatchState for convert_in/convert_out)
923
+ self._address = None
924
+ self._file_mtime = 0.0
925
+ self._file_size = 0
926
+ self._original_load_data = None
927
+ self._original_input_ref = None
928
+
929
+ self._stack_trace = None
930
+ self._nested_index = 0
931
+
932
+ self.selected = False
933
+ self._cache = None
934
+ self._return_value = None
935
+
936
+ def invalidate_up(self, max_depth=4, frame_delta=0, note=None, force=False):
937
+ Core.melty.cache.invalidate_up(self._tile_id, frame_delta=frame_delta,
938
+ max_depth=max_depth, note=note, force=force)
939
+
940
+ def invalidate(self, frame_delta=0, note=None):
941
+ Core.melty.cache.invalidate(self._tile_id, frame_delta=frame_delta, note=note)
942
+
943
+ def invalidate_by_obj(self, obj=None, frame_delta=0, note=None):
944
+ Core.melty.cache.invalidate_by_obj(obj=obj, frame_delta=frame_delta, note=note)
945
+
946
+ def invalidate_up_by_obj(self, obj=None, max_depth=3, frame_delta=0, note=None):
947
+ Core.melty.cache.invalidate_up_by_obj(obj, max_depth=max_depth, frame_delta=frame_delta, note=note)
948
+
949
+ @property
950
+ def cursor_screen_pos(self):
951
+ cursor_x = self.abs_left + self._melty_cursor[0]
952
+ cursor_y = self.abs_top + self._melty_cursor[1]
953
+ return cursor_x, cursor_y
954
+
955
+ @property
956
+ def clip_size(self):
957
+ # if self.clip_rect is None:
958
+ # return (self.width or self.min_width, self.height or self.min_height)
959
+ left, top, right, bottom = self.abs_clip_rect
960
+ return (right - left, bottom - top)
961
+
962
+ @property
963
+ def bbox(self):
964
+ l, t, w, h = self.abs_left, self.abs_top, self.width, self.height
965
+ if l is None or t is None or not w or not h:
966
+ return None
967
+ return (l, t, l + w, t + h - 2)
968
+
969
+ def is_file_stale(self):
970
+ if self._address is None:
971
+ return False
972
+ try:
973
+ s = self._address.path.stat()
974
+ return s.st_mtime != self._file_mtime or s.st_size != self._file_size
975
+ except OSError:
976
+ return True
977
+
978
+ def mark_file_current(self):
979
+ if self._address is None:
980
+ return
981
+ try:
982
+ s = self._address.path.stat()
983
+ self._file_mtime = s.st_mtime
984
+ self._file_size = s.st_size
985
+ except OSError:
986
+ pass
987
+
988
+
989
+ # ─── locate_* : the set-anywhere accessors ────────────────────────────
990
+ # `locate_<param>` access any param of the view, read and write:
991
+ #
992
+ # tilt = draw_state.locate_tilt # framework-resolved value
993
+ # draw_state.locate_x_dim = 0 # -> the DRIVING source
994
+ #
995
+ # ASYMMETRY on purpose: reading is ONLY the resolved value (_kwargs, with
996
+ # the in-flight set cache and the ds fallback anywhere_value applies) -
997
+ # cheap enough per frame. Only the WRITE walks the source registry, picks
998
+ # the driving source, and drives its deferred save/hotswap.
999
+ #
1000
+ # Declared HERE, in the class, so they work on every draw_state from the
1001
+ # first frame - independent of whether anything's imported the view
1002
+ # module yet. The implementation stays in view/core_views/anywhere.py and
1003
+ # is imported lazily per access (this module is model-layer; a top-level
1004
+ # import of the view module would cycle) - a sys.modules hit after the
1005
+ # first call.
1006
+ #
1007
+ # NAME ROUTING on read, the two directions are wired differently:
1008
+ # READ __getattr__ only runs on a MISS (no instance dict entry, no
1009
+ # class property), so every normal attribute read pays nothing.
1010
+ # WRITE there is no miss-only hook for setattr, and a DrawState
1011
+ # __setattr__ would add a Python frame for EVERY attribute write
1012
+ # (~100 per render_func call, per the wrapper's own bookkeeping).
1013
+ # So write routing rides the __setattr__ that already exists: the
1014
+ # @live wrapper calls `_locate_set` when a name starts with
1015
+ # LOCATE_PREFIX (invalidating_decoration ensures the prefix is a single
1016
+ # char compare for every other name).
1017
+ #
1018
+ # A locate write does NOT invalidate this draw_state - it's exactly the
1019
+ # `set_anywhere(...)` call it replaces, and the value only gets there
1020
+ # through the source save + hotswap, which invalidates on its own.
1021
+ LOCATE_PREFIX = "locate_"
1022
+ # Class-level default lets draw_states alive from before a hotswap (whose
1023
+ # __init__ never saw the field) read None instead of raising.
1024
+ _body_actions = None
1025
+ _event_rects = None
1026
+
1027
+ def __getattr__(self, name):
1028
+ # Reached only when normal lookup failed. `locate_params` and any
1029
+ # future explicit property resolve BEFORE this and never arrive here.
1030
+ if name.startswith("locate_"):
1031
+ from meltygui.core.rendering.parameter_core import anywhere_value
1032
+ return anywhere_value(name[7:], self)
1033
+ raise AttributeError(name)
1034
+
1035
+ def _locate_set(self, name, value):
1036
+ """The write half, called by @live's __setattr__ for `locate_*` names.
1037
+ Writes skip the SET_ANYWHERE_PARAMS whitelist (allow_any) — the point
1038
+ of the generic accessor is that any param of the view is settable —
1039
+ and fall back to storing the value ON THIS DRAW_STATE (ds_fallback)
1040
+ when no source in code sets the param, rather than rewriting the
1041
+ view function's signature default."""
1042
+ if isinstance(getattr(type(self), name, None), property):
1043
+ raise AttributeError(f"{name} is read-only")
1044
+ from meltygui.core.rendering.parameter_core import set_anywhere
1045
+ set_anywhere(name[7:], value, self, allow_any=True, ds_fallback=True)
1046
+
1047
+ @property
1048
+ def locate_params(self):
1049
+ """Dict-shaped live view over this view's input parameters: reads
1050
+ resolve from _kwargs, item-writes go through set_anywhere.
1051
+
1052
+ ds.locate_params["x_dim"] = 0
1053
+ for param, value in ds.locate_params: ...
1054
+
1055
+ The whole-signature companion to `locate_<param>` above — same reads
1056
+ and writes, enumerable, and a real dict subclass so it renders and
1057
+ routes like any other dict. Read-only as an attribute; mutate it by
1058
+ item, which is where the anywhere write happens.
1059
+
1060
+ ONE proxy per draw_state, re-snapshotted on each access: views key
1061
+ caches and dirty checks off the identity of the value they were
1062
+ handed, so a fresh object per frame would look like a new value every
1063
+ frame. Stored via object.__setattr__ — like _anc_scroll_key above, it
1064
+ never appears on the default instance, so it isn't serialized."""
1065
+ from meltygui.core.rendering.parameter_core import ParamProxy
1066
+ proxy = self.__dict__.get('_locate_proxy')
1067
+ if proxy is None:
1068
+ proxy = ParamProxy(self)
1069
+ object.__setattr__(self, '_locate_proxy', proxy)
1070
+ return proxy
1071
+ return proxy.refresh()
1072
+
1073
+ @property
1074
+ def locate_all_params(self):
1075
+ """`locate_params` plus the HEADER's params, GROUPED: two nested
1076
+ dicts — {'params': <view params>, 'header': <header-only params>} —
1077
+ each a live ParamProxy (reads resolve, item-writes go through
1078
+ set_anywhere). A separate cached instance, same identity rules as
1079
+ locate_params above."""
1080
+ from meltygui.core.rendering.parameter_core import GroupedParamProxy
1081
+ proxy = self.__dict__.get('_locate_all_proxy')
1082
+ if proxy is None or not isinstance(proxy, GroupedParamProxy):
1083
+ proxy = GroupedParamProxy(self)
1084
+ object.__setattr__(self, '_locate_all_proxy', proxy)
1085
+ return proxy
1086
+ return proxy.refresh()
1087
+
1088
+ def pos_changed(self):
1089
+ """Reconcile this view's BVH box with its current geometry/visibility.
1090
+
1091
+ Called at the end of every render. bvh_sync is the single source of
1092
+ truth — it no-ops unless `bbox` (abs_left/abs_top/width/height) or
1093
+ visibility (inside_clip/closed/abs_closed) actually changed, and owns
1094
+ `_bvh_id`/`_bvh_bbox` so they stay in lockstep with the rtree."""
1095
+ Core.melty.bvh_sync(self)
1096
+
1097
+ # @property
1098
+ # def inner_cursor(self):
1099
+ # if self._columns_top is None:
1100
+ # self._columns_top = imgui.get_cursor_pos()[1]
1101
+ #
1102
+ # return self._columns_top - self.abs_top
1103
+
1104
+ #
1105
+ # def __getattr__(self, name):
1106
+ # if name.startswith("kw_"):
1107
+ # key = name[2:] # strip "arg_" prefix
1108
+ # try:
1109
+ # return self._kwargs[key]
1110
+ # except KeyError:
1111
+ # print(f"Warning: Attempted to access missing argument '{key}' in DrawState.")
1112
+ # raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
1113
+ #
1114
+ # def __setattr__(self, name, value):
1115
+ # if name.startswith("kw_"):
1116
+ # self._args[name[3:]] = value
1117
+ # else:
1118
+ # super().__setattr__(name, value)
1119
+
1120
+ @property
1121
+ def abs_clipped_height(self):
1122
+ clip_rect = self.abs_clip_rect
1123
+
1124
+ clipped_bottom = min(clip_rect[3], self.abs_top + self.height)
1125
+ clipped_top = max(clip_rect[1], self.abs_top)
1126
+ clipped_height = clipped_bottom - clipped_top
1127
+ return max(0, clipped_height)
1128
+
1129
+ @property
1130
+ def abs_content_height(self):
1131
+ if self.frame_count < 2:
1132
+ # Serve the PERSISTED measurement while this session hasn't
1133
+ # re-measured yet: a restored view then scrolls to its persisted
1134
+ # scroll_offset on the very first frame, before its content (or
1135
+ # a loading stand-in) renders. Returns 0 only for never-measured
1136
+ # views, matching the old behavior for genuinely new ones.
1137
+ return int(self.observed_content_height)
1138
+
1139
+ content_height = 0
1140
+ f = Core.melty.frame_count
1141
+ max_bottom = 0
1142
+ min_top = float('inf')
1143
+ key = (f, self.height, self._parent.abs_clip_rect, self.observed_content_height)
1144
+ is_scroll_view = not self._kwargs.get("disable_scroll", True)
1145
+ if self._abs_content_height_key == key:
1146
+ return self._abs_content_height_cache
1147
+ for child in self._view_children.values():
1148
+ if child._kwargs.get("column", 0) != 0:
1149
+ continue
1150
+
1151
+ if child.closable:
1152
+ continue
1153
+ if is_scroll_view:
1154
+ clipped_bottom = child.abs_top + child.height
1155
+ clipped_top = child.abs_top
1156
+ else:
1157
+ clip_rect = self._parent.abs_clip_rect
1158
+ clipped_bottom = min(clip_rect[3], child.abs_top + child.height)
1159
+ clipped_top = max(clip_rect[1], child.abs_top)
1160
+
1161
+ clipped_height = clipped_bottom - clipped_top
1162
+ content_height += max(0, clipped_height)
1163
+
1164
+ min_top = min(min_top, child.abs_top)
1165
+ max_bottom = max(max_bottom, child.abs_top + child.height)
1166
+
1167
+ content_height = max_bottom - min_top if max_bottom > min_top else 0
1168
+
1169
+ # Special case where view wants to scroll but has no children for which to infer its height
1170
+ if "determines_height" in self._kwargs:
1171
+ content_height = self._content_rect[1]
1172
+
1173
+
1174
+ self._abs_content_height_cache = content_height
1175
+
1176
+ # if is_scroll_view:
1177
+ # content_height = max(content_height, self.height)
1178
+
1179
+ return int(self.observed_content_height)
1180
+
1181
+ @property
1182
+ def abs_layer(self):
1183
+
1184
+ if self.parent_window is not None and self.closable:
1185
+ # Nested closable windows live in the dedicated nested layer band
1186
+ # when their parent's root is the front window; otherwise they stay
1187
+ # parent-relative in the parent band. See Melty.nested_window_layer.
1188
+ return Core.melty.nested_window_layer(
1189
+ self.parent_window.abs_layer, self._kwargs.get("layer_offset", 4),
1190
+ ds=self)
1191
+ elif self.parent_window is not None:
1192
+ return self.parent_window.abs_layer
1193
+ else:
1194
+ return self.layer
1195
+
1196
+ @property
1197
+ def abs_closed(self):
1198
+
1199
+ if self.closed and self.closable:
1200
+ return True
1201
+
1202
+ if not self.expanded:
1203
+ return True
1204
+
1205
+ if self.parent_window is not None:
1206
+ return self.parent_window.abs_closed
1207
+
1208
+ return False
1209
+
1210
+ @property
1211
+ def root_window(self):
1212
+ if self.parent_window is None:
1213
+ return self
1214
+ else:
1215
+ return self.parent_window.root_window
1216
+
1217
+ @property
1218
+ def anchor_offset(self):
1219
+ anchor_margin = 2
1220
+
1221
+ if self.anchor_pos in LEFT_ANCHORS:
1222
+ offset_x = 0
1223
+ elif self.anchor_pos in RIGHT_ANCHORS:
1224
+ offset_x = -self.width
1225
+ else: # horizontally centered
1226
+ offset_x = int(-self.width / 2)
1227
+
1228
+ if self.anchor_pos in TOP_ANCHORS:
1229
+ offset_y = 0
1230
+ elif self.anchor_pos in BOTTOM_ANCHORS:
1231
+ offset_y = -self.height + -anchor_margin
1232
+ else: # vertically centered
1233
+ offset_y = int(-self.height / 2)
1234
+
1235
+ return (offset_x, offset_y)
1236
+
1237
+ @property
1238
+ def parent_anchor_offset(self):
1239
+ """Offset from the parent window's top-left to the anchor point on the
1240
+ parent selected by ``anchor_pos``.
1241
+
1242
+ This is the parent-side counterpart to ``anchor_offset`` (which selects
1243
+ the child's own corner). The two are independent: ``parent_anchor_pos``
1244
+ picks the point on the parent, ``anchor_pos`` picks the child's corner
1245
+ that lands on it. Returns (0, 0) for a TOP_LEFT parent anchor (the
1246
+ default), preserving the legacy top-left-relative layout.
1247
+ """
1248
+ parent = self.parent_window
1249
+ if parent is None or parent is self:
1250
+ return (0, 0)
1251
+
1252
+ if self.parent_anchor_pos in LEFT_ANCHORS:
1253
+ offset_x = 0
1254
+ elif self.parent_anchor_pos in RIGHT_ANCHORS:
1255
+ offset_x = parent.width
1256
+ else: # horizontally centered
1257
+ offset_x = int(parent.width / 2)
1258
+
1259
+ if self.parent_anchor_pos in TOP_ANCHORS:
1260
+ offset_y = 0
1261
+ elif self.parent_anchor_pos in BOTTOM_ANCHORS:
1262
+ offset_y = parent.height
1263
+ else: # vertically centered
1264
+ offset_y = int(parent.height / 2)
1265
+
1266
+ return (offset_x, offset_y)
1267
+
1268
+ @property
1269
+ def _pin_target(self):
1270
+ """The view this pins to, resolved live from the Pin mode off
1271
+ _parent/parent_window at call time, so the float tracks it as it
1272
+ scrolls/moves. None when not pinned or the relative is missing.
1273
+
1274
+ Pinning only ever targets an ancestor (parent / grandparent / window),
1275
+ and ancestors never depend on a descendant's position — so reading the
1276
+ target's abs_clip_rect from here can't cycle, and needs no cache."""
1277
+ mode = self.pin_to_clip
1278
+ if mode is Pin.WINDOW:
1279
+ t = self.parent_window
1280
+ elif mode is Pin.GRANDPARENT:
1281
+ p = self._parent
1282
+ t = p._parent if (p is not None and p is not self) else None
1283
+ elif mode is Pin.PARENT or mode is Pin.CLIP:
1284
+ t = self._parent
1285
+ else:
1286
+ return None
1287
+ return t if (t is not None and t is not self) else None
1288
+
1289
+ @property
1290
+ def pin_rect(self):
1291
+ """Reference rect ``(left, top, right, bottom)`` the pin anchors to, in
1292
+ absolute coords — the live clip rect of the resolved relative
1293
+ (``_pin_target``). None when there's nothing to pin to.
1294
+
1295
+ For ``Pin.CLIP`` the rect is intersected with the parent window's box so
1296
+ each corner clamps to the visible edge (e.g. bottom = min(parent_bottom,
1297
+ clip_bottom)) instead of running past it.
1298
+
1299
+ The target's box is read from its LIVE abs (``_abs_left``/``_abs_top``),
1300
+ not its per-frame-cached ``abs_left``/``abs_top``. A pinned float needs
1301
+ the target's current position on frames the target itself isn't
1302
+ re-rendering — e.g. while its window is dragged: the window moves (its
1303
+ own abs cache invalidates on window_pos), but the target's cache key
1304
+ doesn't change, so cached ``target.abs_left`` would return last frame's
1305
+ anchor. Computing live re-reads the moved window through the parent
1306
+ chain, so the anchor tracks the drag instead of lagging a frame."""
1307
+ target = self._pin_target
1308
+ if target is None:
1309
+ return None
1310
+ # Anchor to the target's RAW box (live abs), so the float tracks the
1311
+ # target's actual position. PARENT/WINDOW/GRANDPARENT do NOT clamp here:
1312
+ # intersecting with the target's clip rect makes the anchor corner snap
1313
+ # to the clip edge whenever the target is clipped, so the window tracks
1314
+ # the *clip* instead of the target (the "only moves when the clamp moves
1315
+ # it" bug). Clamping is Pin.CLIP's job - see below.
1316
+ tl, tt = target._abs_left(), target._abs_top()
1317
+ rect = (tl, tt, tl + target.width, tt + target.height)
1318
+ if self.pin_to_clip is Pin.CLIP:
1319
+ # Pin.CLIP clamps the float to the parent window's visible box so
1320
+ # corners track the visible edge. Use the window's LIVE box
1321
+ # (win._abs_left()/_abs_top()), not its cached abs_left or the
1322
+ # target's captured clip_rect: both are fixed in screen space and so
1323
+ # are stale when the window itself is dragged, which left Pin.CLIP
1324
+ # floats clamped to the old position. The live window box is correct
1325
+ # in both cases - it's constant while content scrolls and moves with
1326
+ # a window window.
1327
+ win = self.parent_window
1328
+ if win is None and len(Core.melty.melty_windows) > 0:
1329
+ win = Core.melty.melty_windows[-1]
1330
+ if win is not None and win is not self:
1331
+ wl, wt = win._abs_left(), win._abs_top()
1332
+ c = (wl, wt, wl + win.width, wt + win.height)
1333
+ rect = (max(rect[0], c[0]), max(rect[1], c[1]),
1334
+ min(rect[2], c[2]), min(rect[3], c[3]))
1335
+ return rect
1336
+
1337
+ @property
1338
+ def clip_anchor_base(self):
1339
+ """Anchor reference point on the pinned rect (see ``pin_rect``).
1340
+
1341
+ Picks the corner of the pin rect selected by ``parent_anchor_pos`` — the
1342
+ point *on the target* the window attaches to. ``anchor_offset`` (driven
1343
+ by ``anchor_pos``) then shifts the window so its own corner lands there,
1344
+ so the two are independent: e.g. parent_anchor=BOTTOM_RIGHT /
1345
+ anchor=TOP_RIGHT hangs the window off the target's bottom-right corner.
1346
+ With both at the TOP_LEFT default the window's top-left sits on the
1347
+ target's top-left. Computed live (pin_rect already is), so it tracks the
1348
+ target regardless of how it scrolls.
1349
+ """
1350
+ clip = self.pin_rect
1351
+ if clip is None:
1352
+ return None
1353
+ clip_left, clip_top, clip_right, clip_bottom = clip
1354
+
1355
+ if self.parent_anchor_pos in LEFT_ANCHORS:
1356
+ base_x = clip_left
1357
+ elif self.parent_anchor_pos in RIGHT_ANCHORS:
1358
+ base_x = clip_right
1359
+ else: # horizontally centered
1360
+ base_x = (clip_left + clip_right) / 2
1361
+
1362
+ if self.parent_anchor_pos in TOP_ANCHORS:
1363
+ base_y = clip_top
1364
+ elif self.parent_anchor_pos in BOTTOM_ANCHORS:
1365
+ base_y = clip_bottom
1366
+ else: # vertically centered
1367
+ base_y = (clip_top + clip_bottom) / 2
1368
+
1369
+ return (base_x, base_y)
1370
+
1371
+ def _ancestor_scroll(self):
1372
+ """Sum the scroll_offsets of intermediate ancestors between self and
1373
+ ``parent_window`` (exclusive at both ends) by walking the ``_parent``
1374
+ chain. Used so abs_left/abs_top respond to ancestor scroll deltas
1375
+ without waiting for the descendant to re-render — left_offset is
1376
+ stored as the *unscrolled* content position and this delta subtracted
1377
+ on demand. Returns (sx, sy).
1378
+
1379
+ Memoized per (frame_count, Melty.scroll_version): the walk used to run
1380
+ on EVERY abs_left/abs_top access just to build their cache keys (~57
1381
+ walks per render_func call). Any real scroll_offset change anywhere
1382
+ bumps scroll_version (see new_setattr in invalidation_decoration), so
1383
+ mid-frame scroll deltas still invalidate exactly like the uncached
1384
+ walk did."""
1385
+ meltygui = Core.melty
1386
+ key = (meltygui.frame_count, getattr(meltygui, 'scroll_version', 0))
1387
+ if self._anc_scroll_key == key:
1388
+ return self._anc_scroll_cache
1389
+ sx = sy = 0
1390
+ node = self._parent
1391
+ stop = self.parent_window
1392
+ # Cycle guard mirroring the abs_left guard: bail if we revisit self
1393
+ # or walk longer than the deepest realistic chain.
1394
+ for _ in range(64):
1395
+ if node is None or node is self or node is stop:
1396
+ break
1397
+ so = node.scroll_offset
1398
+ if so is not None:
1399
+ ny = so[1]
1400
+ # Enforce the scroll bound here, at the source, so writers (a
1401
+ # text editor's drag-auto-scroll, pans) don't each have to clamp.
1402
+ # node._max_scroll_y is the authoritative max core_render
1403
+ # published this frame. Clamp any contribution AND write it back
1404
+ # so the stored offset can't run past the content ends - that
1405
+ # runaway is what made auto-scroll overshoot EOF and lag on the
1406
+ # way back. None = the node hasn't published a max yet.
1407
+ mx = node._max_scroll_y
1408
+ if mx is not None:
1409
+ if ny < 0:
1410
+ ny = 0.0
1411
+ elif ny > mx:
1412
+ ny = mx
1413
+ if ny != so[1]:
1414
+ node.scroll_offset = (so[0], ny)
1415
+
1416
+ sx += so[0]
1417
+ sy += ny
1418
+ nxt = node._parent
1419
+ if nxt is node:
1420
+ break
1421
+ node = nxt
1422
+ # Raw writes so the memo must not re-enter @live setattr tracking.
1423
+ # Cache before key, so a matching key always sees a written cache.
1424
+ # (The clamp writeback above may have bumped scroll_version, making
1425
+ # this key already anyway - the next access just recomputes.)
1426
+ object.__setattr__(self, '_anc_scroll_cache', (sx, sy))
1427
+ object.__setattr__(self, '_anc_scroll_key', key)
1428
+ return sx, sy
1429
+
1430
+ def _pinned_base_y(self, base_y, anchor_y):
1431
+ """Bound a pinned nested window's vertical ANCHOR to its parent window:
1432
+ the float's TOP can ride at most 200px above the window's top, and its
1433
+ TOP can't drop below the window's bottom. (Display top/height when there is no
1434
+ parent window.)
1435
+
1436
+ Pinned floats (context menus, live-value windows — pin_to_clip set)
1437
+ follow their spawner: pin_rect reads the target's live abs, so a tall
1438
+ spawner (e.g. a big scrolling draw_text) that scrolls drags the float
1439
+ clean out of the window with it — the spawning VIEW routinely sits far
1440
+ above/below the window's own box. Bounding against the WINDOW (not the
1441
+ display, not the view) keeps the float within a window-height of travel
1442
+ past each edge, so it's always adjacent to the window it belongs to.
1443
+
1444
+ Clamps the ANCHOR (base_y), NOT the final abs top: window_pos_y (the
1445
+ user's drag) is deliberately left out of the bound, so a click-drag
1446
+ still moves the window freely — clamping the sum ate vertical drag and
1447
+ left a dead zone on the way back. Same shape as the swoosh, which
1448
+ clamps its parent anchor to the window edge."""
1449
+ if not self.closable:
1450
+ return base_y
1451
+ win = self.parent_window
1452
+ if win is not None and win is not self:
1453
+ win_top, win_h = win._abs_top(), win.height or 0
1454
+ else:
1455
+ disp = Core.melty.display_size
1456
+ win_top, win_h = 0, (disp[1] if disp is not None else 0)
1457
+ floor_y = win_top - 200 - anchor_y
1458
+ ceil_y = win_top + win_h - anchor_y
1459
+ if floor_y <= base_y <= ceil_y:
1460
+ return base_y
1461
+ try:
1462
+ # Exempt the floating DnD window: glue_window_to_cursor assumes abs
1463
+ # is linear in window_pos, and a clamp makes its per-frame
1464
+ # correction accumulate without bound (see _cap_to_display).
1465
+ from meltygui.core.input.drag_drop_core import DragDrop
1466
+ if DragDrop.is_dragged_item(self):
1467
+ return base_y
1468
+ except Exception:
1469
+ pass
1470
+ return floor_y if base_y < floor_y else ceil_y
1471
+
1472
+ def _cap_to_display(self, pos, axis):
1473
+ """Cap a nested window's computed abs position so at least a sliver of
1474
+ its box stays on the display. Nested windows follow their spawning
1475
+ view's scroll (the ancestor-scroll subtraction in _abs_left/_abs_top),
1476
+ which otherwise carries them right off screen. Only the COMPUTED
1477
+ position is capped — left_offset / window_pos stay untouched — so
1478
+ scrolling back returns the window to its natural spot. A sliver cap
1479
+ (not full containment) so a deliberate drag can still tuck a window
1480
+ mostly off screen without ever losing its grab handle. The floating
1481
+ DnD window is exempt: glue_window_to_cursor assumes abs is linear in
1482
+ window_pos, and a clamp would make its per-frame correction
1483
+ accumulate without bound."""
1484
+ if (not self.closable or self.parent_window is None
1485
+ or self.parent_window is self):
1486
+ return pos
1487
+ size = self.width if axis == 0 else self.height
1488
+ disp = Core.melty.display_size
1489
+ if size is None or disp is None:
1490
+ return pos
1491
+ keep = min(48, size)
1492
+ if self._kwargs.get("keep_in_view", False):
1493
+ # Popup menus stay wholly visible. Ordinary movable windows retain
1494
+ # their sliver allowance so you can deliberately tuck them away.
1495
+ lo, hi = 0, max(0, disp[axis] - size)
1496
+ else:
1497
+ lo, hi = keep - size, disp[axis] - keep
1498
+ # whether the cap is holding this axis: the OS-edge physics
1499
+ # (os_frame._root_extent) pull a scrolled-out nested window out of
1500
+ # its parent's collision extent
1501
+ capped = not (lo <= pos <= hi)
1502
+ if axis == 0:
1503
+ self._capped_x = capped
1504
+ else:
1505
+ self._capped_y = capped
1506
+ if not capped:
1507
+ return pos
1508
+ try:
1509
+ from meltygui.core.input.drag_drop_core import DragDrop
1510
+ if DragDrop.is_dragged_item(self):
1511
+ return pos
1512
+ except Exception:
1513
+ pass
1514
+ return max(min(pos, hi), lo)
1515
+
1516
+ def _abs_left(self):
1517
+ # Parent-relative placement reads the dependency-checked absolute
1518
+ # origin. The cache marks evaluation before recursing, so a malformed
1519
+ # parent cycle returns the last position instead of recursing forever.
1520
+ parent_left = 0
1521
+ if self.parent_window is not None and self.parent_window is not self:
1522
+ parent_left = self.parent_window.abs_left
1523
+
1524
+ window_pos_x = self.window_pos[0] if self.window_pos is not None else 0
1525
+ anchor = self.anchor_offset
1526
+
1527
+ base = self.clip_anchor_base if self.pin_to_clip else None
1528
+ if base is not None:
1529
+ # Pin to the clip rect corner rather than the scrolled position of
1530
+ # the declaring view (which left_offset tracks).
1531
+ this_left = window_pos_x + base[0] + anchor[0]
1532
+ if (self.pin_to_clip is Pin.CLIP and self.width is not None
1533
+ and self.parent_window is not None and self.parent_window is not self):
1534
+ # Box-level clamp: the rect clamp only repositions the anchor
1535
+ # point, so if anchor_offset shifts the box its far edge can
1536
+ # still hang off the window. Pull the box back so it stays
1537
+ # inside the window horizontally (favoring the left edge if the
1538
+ # box is wider than the window).
1539
+ win = self.parent_window
1540
+ win_left = win.abs_left
1541
+ this_left = min(this_left, win_left + win.width - self.width)
1542
+ this_left = max(this_left, win_left)
1543
+ else:
1544
+ # left_offset is stored as the *unscrolled* position in parent_window's
1545
+ # content (capture sites add the ancestor scroll). Subtracting the
1546
+ # live ancestor scroll here makes abs_left immediately reflect any
1547
+ # mid-frame scroll change on an ancestor - what previously had to
1548
+ # wait for the descendant to re-render with a new cursor pos.
1549
+ sx, _ = self._ancestor_scroll()
1550
+ this_left = (window_pos_x + parent_left + self.left_offset - sx
1551
+ + self.parent_anchor_offset[0] + anchor[0])
1552
+ this_left = self._cap_to_display(this_left, 0)
1553
+ return int(this_left)
1554
+
1555
+ def _abs_top_true(self):
1556
+ parent_top = 0
1557
+ if self._parent is not None and self._parent is not self:
1558
+ parent_top = self._parent._abs_top_true()
1559
+ parent_scroll = self._parent.scroll_offset[1] if self._parent.scroll_offset is not None else 0
1560
+ parent_top -= parent_scroll
1561
+
1562
+ anchor = self.anchor_offset
1563
+ window_pos_y = self.window_pos[1] if self.window_pos is not None else 0
1564
+
1565
+ base = self.clip_anchor_base if self.pin_to_clip else None
1566
+ if base is not None:
1567
+ # Pin to the clip rect corner rather than the scrolled position of
1568
+ # the declaring view (which top_offset tracks).
1569
+ this_top = window_pos_y + self._pinned_base_y(base[1], anchor[1]) + anchor[1]
1570
+ if (self.pin_to_clip is Pin.CLIP and self.height is not None
1571
+ and self.parent_window is not None and self.parent_window is not self):
1572
+ # Box-level clamp: see _abs_left. Keeps the bottom edge from
1573
+ # hanging below the window (favoring the top edge if the box
1574
+ # is taller than the window).
1575
+ win = self.parent_window
1576
+ win_top = win.abs_top
1577
+ this_top = min(this_top, win_top + win.height - self.height)
1578
+ this_top = max(this_top, win_top)
1579
+ else:
1580
+ # See _abs_left for why this subtracts the live ancestor scroll.
1581
+ this_top = (window_pos_y + parent_top + self.top_offset_true
1582
+ + self.parent_anchor_offset[1] + anchor[1])
1583
+ return int(this_top)
1584
+
1585
+
1586
+ def _abs_top(self):
1587
+ parent_top = 0
1588
+ if self.parent_window is not None and self.parent_window is not self:
1589
+ parent_top = self.parent_window.abs_top
1590
+
1591
+ anchor = self.anchor_offset
1592
+ window_pos_y = self.window_pos[1] if self.window_pos is not None else 0
1593
+
1594
+ base = self.clip_anchor_base if self.pin_to_clip else None
1595
+ if base is not None:
1596
+ # Pin to the clip rect corner rather than the scrolled position of
1597
+ # the declaring view (which top_offset tracks).
1598
+ this_top = window_pos_y + self._pinned_base_y(base[1], anchor[1]) + anchor[1]
1599
+ if (self.pin_to_clip is Pin.CLIP and self.height is not None
1600
+ and self.parent_window is not None and self.parent_window is not self):
1601
+ # Box-level clamp: see _abs_left. Keeps the bottom edge from
1602
+ # hanging below the window (favoring the top edge if the box
1603
+ # is taller than the window).
1604
+ win = self.parent_window
1605
+ win_top = win.abs_top
1606
+ this_top = min(this_top, win_top + win.height - self.height)
1607
+ this_top = max(this_top, win_top)
1608
+ else:
1609
+ # See _abs_left for why this subtracts the live ancestor scroll.
1610
+ _, sy = self._ancestor_scroll()
1611
+ this_top = (window_pos_y + parent_top + self.top_offset - sy
1612
+ + self.parent_anchor_offset[1] + anchor[1])
1613
+ this_top = self._cap_to_display(this_top, 1)
1614
+ return int(this_top)
1615
+
1616
+ @property
1617
+ def abs_clip_rect(self):
1618
+ abs_left = self.abs_left
1619
+ abs_top = self.abs_top
1620
+ box_right = abs_left + self.width
1621
+ box_bottom = abs_top + self.height
1622
+ clip = self.clip_rect
1623
+ if clip is None:
1624
+ return (abs_left, abs_top, box_right, box_bottom)
1625
+
1626
+ # clip_rect is stored in absolute screen coords, so it's correct while
1627
+ # content scrolls (the clip region is fixed in screen space) but stale
1628
+ # when the parent window moves. Shift it by however far the parent window
1629
+ # has moved since capture - zero during scroll, the mouse delta during a
1630
+ # window drag - so the clamp tracks the window without a re-render. The
1631
+ # clip region moves rigidly with the window, so a uniform shift is exact.
1632
+ pw = self.parent_window
1633
+ anchor = self._clip_win_anchor
1634
+ if pw is not None and pw is not self and anchor is not None:
1635
+ dx = pw._abs_left() - anchor[0]
1636
+ dy = pw._abs_top() - anchor[1]
1637
+ clip = (clip[0] + dx, clip[1] + dy, clip[2] + dx, clip[3] + dy)
1638
+
1639
+ # Intersect the LIVE box with the (shifted) clip rect. Both move with the
1640
+ # window now, so the clamped edges stay pinned to the unclamped corner.
1641
+ return (int(max(abs_left, clip[0])),
1642
+ int(max(abs_top, clip[1])),
1643
+ int(min(box_right, clip[2])),
1644
+ int(min(box_bottom, clip[3])))
1645
+
1646
+ @property
1647
+ def abs_clip_rect_local(self):
1648
+ abs_left = self.abs_left
1649
+ abs_top = self.abs_top
1650
+ clipped_by = self.clipped_by_rect
1651
+ if clipped_by is None:
1652
+ clip = (abs_left, abs_top, abs_left + self.width, abs_top + self.height)
1653
+ else:
1654
+ clip = (abs_left + clipped_by[0],
1655
+ abs_top + clipped_by[1],
1656
+ abs_left + self.width - clipped_by[2],
1657
+ abs_top + self.height - clipped_by[3])
1658
+ box_right = abs_left + self.width
1659
+ box_bottom = abs_top + self.height
1660
+ # clip_rect is captured in absolute screen coords, so it's correct while
1661
+ # content scrolls (the clip region is fixed in screen space) but stale
1662
+ # when the whole window moves. Shift it by however far the parent window
1663
+ # has moved since capture - zero during scroll, the drag delta during a
1664
+ # window drag - so the clamp tracks the window without a re-render. The
1665
+ # clip region moves rigidly with the window, so a uniform shift is exact.
1666
+ pw = self.parent_window
1667
+ anchor = self._clip_win_anchor
1668
+ if pw is not None and pw is not self and anchor is not None:
1669
+ dx = pw._abs_left() - anchor[0]
1670
+ dy = pw._abs_top() - anchor[1]
1671
+ clip = (clip[0] + dx, clip[1] + dy, clip[2] + dx, clip[3] + dy)
1672
+
1673
+ # Intersect the LIVE box with the (shifted) clip rect. Both move with the
1674
+ # window now, so the clamped edges stay locked to the unclamped corner.
1675
+ return (int(max(abs_left, clip[0])),
1676
+ int(max(abs_top, clip[1])),
1677
+ int(min(box_right, clip[2])),
1678
+ int(min(box_bottom, clip[3])))
1679
+
1680
+ def children_in_clip(self, clip=None, max_depth=1, include_windows=True):
1681
+ """Descendants whose vertical span overlaps `clip` (default: this view's
1682
+ abs_clip_rect), top-to-bottom.
1683
+
1684
+ `max_depth` bounds the recursion: 1 (default) returns only direct
1685
+ children; higher values also descend into them, including any descendant
1686
+ that overlaps `clip`, down at most `max_depth` levels. Pass
1687
+ `max_depth=None` for the whole subtree. The same `clip` is carried down
1688
+ — positions are absolute (screen space), so a grandchild is kept iff it
1689
+ overlaps the original viewport, not its immediate parent's box.
1690
+
1691
+ `include_windows=False` prunes nested windows (closable) and their
1692
+ whole subtree — same contract as get_child_keys' include_windows: a
1693
+ window owns its own composition and doesn't move with this view, so
1694
+ callers reacting to this view's layout (e.g. the scroll-in sweep)
1695
+ shouldn't touch it.
1696
+
1697
+ Results are pre-order (each child immediately followed by its own
1698
+ in-clip subtree) and deduped by identity across the whole walk."""
1699
+ if clip is None:
1700
+ clip = self.abs_clip_rect
1701
+ if clip is None:
1702
+ return []
1703
+
1704
+ direct = self._direct_children_in_clip(clip)
1705
+ if not include_windows:
1706
+ direct = [c for c in direct if not c.closable]
1707
+ if max_depth is not None and max_depth <= 1:
1708
+ return direct
1709
+
1710
+ next_depth = None if max_depth is None else max_depth - 1
1711
+ seen = set()
1712
+ result = []
1713
+ for c in direct:
1714
+ if id(c) not in seen:
1715
+ seen.add(id(c))
1716
+ result.append(c)
1717
+ for gc in c.children_in_clip(clip, next_depth, include_windows):
1718
+ if id(gc) not in seen:
1719
+ seen.add(id(gc))
1720
+ result.append(gc)
1721
+ return result
1722
+
1723
+ def _direct_children_in_clip(self, clip):
1724
+ """Direct children overlapping `clip`, ordered top-to-bottom by live
1725
+ abs_top.
1726
+
1727
+ Children self-register into _view_children (core_render, where _parent
1728
+ is set) keyed by id. abs_left/abs_top are read live, so this reflects
1729
+ the current scroll immediately — unlike a BVH clip query, whose boxes
1730
+ only catch up when each child re-renders, so it would miss exactly the
1731
+ children that just scrolled in.
1732
+
1733
+ A plain 1-D overlap filter, NOT a binary search: rows can have different
1734
+ heights, so ordering by abs_top does not order by bottom edge
1735
+ (abs_top + height), and a binary search keyed on either edge skips a
1736
+ band of visible rows. We filter (so dict iteration order is irrelevant),
1737
+ then sort the survivors top-to-bottom. A child can appear under more
1738
+ than one key, and one that has since re-rendered elsewhere leaves a
1739
+ stale entry behind — so we dedupe by identity and drop any whose _parent
1740
+ is no longer this view."""
1741
+ children = self._view_children
1742
+ if not children:
1743
+ return []
1744
+ clip_top, clip_bottom = clip[1], clip[3]
1745
+
1746
+ seen = set()
1747
+ result = []
1748
+ for c in children.values():
1749
+ if c is None or c is self or c._parent is not self:
1750
+ continue
1751
+ if id(c) in seen:
1752
+ continue
1753
+ seen.add(id(c))
1754
+ top = c.abs_top
1755
+ if top < clip_bottom and top + (c.height or 0) > clip_top:
1756
+ result.append(c)
1757
+ result.sort(key=lambda c: c.abs_top)
1758
+ return result
1759
+
1760
+ def descendants(self, max_depth=None):
1761
+ """Every descendant draw_state via _view_children, pre-order and deduped
1762
+ by identity, bounded by `max_depth` (None = whole subtree).
1763
+
1764
+ The un-clipped sibling of children_in_clip: no viewport filter, so it
1765
+ returns rows that have scrolled off-screen too — as long as they
1766
+ rendered at least once and are still parented here. Used to walk the
1767
+ whole tree without drawing, e.g. to recount search matches by invoking
1768
+ each view's _search_matcher (meltygui.search_walk), instead of force-
1769
+ rendering off-screen rows just so they re-register their counts."""
1770
+ seen = set()
1771
+ result = []
1772
+
1773
+ def _walk(node, depth):
1774
+ if depth is not None and depth <= 0:
1775
+ return
1776
+ children = node._view_children
1777
+ if not children:
1778
+ return
1779
+ for c in list(children.values()):
1780
+ if c is None or c is node or c._parent is not node or id(c) in seen:
1781
+ continue
1782
+ seen.add(id(c))
1783
+ result.append(c)
1784
+ _walk(c, None if depth is None else depth - 1)
1785
+
1786
+ _walk(self, max_depth)
1787
+ return result
1788
+
1789
+ @property
1790
+ def size_change(self):
1791
+ if self._tile_id is None or self.width is None or self.height is None:
1792
+ return True
1793
+ #
1794
+ # if (not imgui.is_mouse_down(0)
1795
+ # and not imgui.is_mouse_down(1)
1796
+ # and not imgui.is_mouse_down(2)):
1797
+ # return True
1798
+
1799
+
1800
+
1801
+ cache = Core.melty.cache
1802
+ if cache is None:
1803
+ return False
1804
+ t = cache._tiles.get(self._tile_id)
1805
+ if t is None:
1806
+ return False
1807
+ return t.size != (int(self.width), int(self.height))
1808
+
1809
+ @property
1810
+ def tile_fully_filled(self) -> bool:
1811
+ """Debug helper: True when this view's blit tile has had every pixel
1812
+ written from the main framebuffer at least once. Used by the filled-
1813
+ tile overlay (Toggles.show_filled_tiles)."""
1814
+ if self._tile_id is None:
1815
+ return False
1816
+ cache = Core.melty.cache
1817
+ if cache is None:
1818
+ return False
1819
+ return cache._tile_fully_filled(cache._tiles.get(self._tile_id))
1820
+
1821
+ def _cached_absolute_position(self, axis):
1822
+ # Pins already resolve their target live. For ordinary placement,
1823
+ # validate the parent origin and selected corner before accepting a
1824
+ # cached value: solve() may have read us before a parent hand resize
1825
+ # or native rebase later in this same frame.
1826
+ #
1827
+ # This is read thousands of times a frame (every on_action, clip rect
1828
+ # and hover test), so a cache hit must stay cheap at any nesting depth:
1829
+ # the key and cache writes are raw, as in _ancestor_scroll, because the
1830
+ # memo must not re-enter @live setattr tracking, and only this axis of
1831
+ # parent_anchor_offset is worked out.
1832
+ if self.pin_to_clip:
1833
+ return self._abs_left() if axis == 0 else self._abs_top()
1834
+ if axis == 0:
1835
+ key_name, previous, cached = '_abs_left_key', self._abs_left_key, self._abs_left_cache
1836
+ else:
1837
+ key_name, previous, cached = '_abs_top_key', self._abs_top_key, self._abs_top_cache
1838
+ if previous is False:
1839
+ return cached # malformed parent cycles retain the last position
1840
+ parent = self.parent_window
1841
+ if parent is None or parent is self:
1842
+ # A root reads no other draw_state, so it needs no cycle mark.
1843
+ parent = None
1844
+ parent_origin = parent_extent = 0
1845
+ else:
1846
+ # Mark evaluation before reading the parent: a parent cycle gets
1847
+ # the last position back instead of recursing forever.
1848
+ object.__setattr__(self, key_name, False)
1849
+ key = None
1850
+ try:
1851
+ if parent is not None:
1852
+ parent_anchor_pos = self.parent_anchor_pos
1853
+ if axis == 0:
1854
+ parent_origin = parent.abs_left
1855
+ parent_extent = 0 if parent_anchor_pos in LEFT_ANCHORS else parent.width
1856
+ else:
1857
+ parent_origin = parent.abs_top
1858
+ parent_extent = 0 if parent_anchor_pos in TOP_ANCHORS else parent.height
1859
+ scroll = self._ancestor_scroll()[axis]
1860
+ # parent_extent with parent_anchor_pos decides parent_anchor_offset.
1861
+ key = (Core.melty.frame_count,
1862
+ self.left_offset if axis == 0 else self.top_offset,
1863
+ self.window_pos, self.anchor_pos, self.parent_anchor_pos,
1864
+ self.width if axis == 0 else self.height, scroll,
1865
+ parent_origin, parent_extent)
1866
+ if previous == key:
1867
+ key = previous
1868
+ return cached
1869
+ value = self._abs_left() if axis == 0 else self._abs_top()
1870
+ object.__setattr__(self, '_abs_left_cache' if axis == 0 else '_abs_top_cache', value)
1871
+ return value
1872
+ finally:
1873
+ if parent is not None or key is not previous:
1874
+ object.__setattr__(self, key_name, key)
1875
+
1876
+ @property
1877
+ def abs_left(self):
1878
+ return self._cached_absolute_position(0)
1879
+
1880
+ @property
1881
+ def abs_top(self):
1882
+ return self._cached_absolute_position(1)
1883
+
1884
+ @property
1885
+ def abs_top_true(self):
1886
+ # if self.pin_to_clip:
1887
+ # return self._abs_top()
1888
+ f = Core.melty.frame_count
1889
+ key = (f, self.top_offset_true, self.window_pos,
1890
+ self.anchor_pos, self.parent_anchor_pos, self.height)
1891
+ if self._abs_top_true_key == key:
1892
+ return self._abs_top_true_cache
1893
+ self._abs_top_true_key = key
1894
+ val = self._abs_top_true()
1895
+ self._abs_top_true_cache = val
1896
+ return val
1897
+
1898
+ def mark_column(self, column):
1899
+ self.max_column = max(self.max_column, column)
1900
+
1901
+ def shadow_depth_at(self, depth, active_layer):
1902
+ divisor = max(0.5, depth - 20.0)
1903
+ depth_and_layer = active_layer * Core.melty.max_depth + (depth * (20.0 / (divisor)))
1904
+ depth_and_layer *= Core.melty.layer_inc
1905
+ return depth_and_layer
1906
+
1907
+ @property
1908
+ def shadow_depth(self):
1909
+ depth, active_layer = self.depth_and_layer
1910
+ return shadow_depth_at(depth, active_layer)
1911
+
1912
+
1913
+ @property
1914
+ def seen(self):
1915
+ debounce = 1
1916
+ return self.last_seen is not None and Core.melty.frame_count - self.last_seen < debounce
1917
+
1918
+ @property
1919
+ def window_index(self):
1920
+ nested_offset = self._nested_index if self.parent_window is not None else 0
1921
+ return self.abs_layer + nested_offset
1922
+
1923
+ # if self.closable:
1924
+ # layer_index = min(Melty.max_layer - 1, self.abs_layer + self._nested_index - 2)
1925
+ # elif self.parent_window is not None:
1926
+ # layer_index = min(Melty.max_layer - 1, self.abs_layer + self.parent_window._nested_index)
1927
+ # else:
1928
+ # layer_index = min(Melty.max_layer - 1, self.abs_layer)
1929
+
1930
+ # return layer_index
1931
+
1932
+ def init_cst_state(self, node, module_id: str):
1933
+ import libcst as cst # lazy: libcst takes ~80 ms to import
1934
+ self.cst = None
1935
+ self.cst.path_key = Core.melty.current_path()
1936
+ self.cst.module_id = module_id
1937
+ self.cst.root_gen = Core.melty.current_gen(module_id)
1938
+ # super light anchor for re-attachment later (customize as you like)
1939
+ if isinstance(node, cst.Name):
1940
+ self.cst.anchor = ("Name", node.value)
1941
+ elif isinstance(node, cst.Attribute):
1942
+ self.cst.anchor = ("Attr", node.attr.value)
1943
+ else:
1944
+ self.cst.anchor = (type(node).__name__,)
1945
+
1946
+ def get_resize_handle(self):
1947
+ if self.left is None or self.width is None or self.top is None or self.height is None:
1948
+ return (0, 0, 0, 0)
1949
+ left = self.left
1950
+ top = self.top
1951
+ right = left + self.width
1952
+ bottom = top + self.height + 2
1953
+
1954
+ margin = 20
1955
+ return (right - margin, bottom - margin, right, bottom)
1956
+
1957
+ def get_drag_mode(self):
1958
+ if self.auto_resize:
1959
+ return DragMode.WINDOW
1960
+
1961
+ mx, my = imgui.get_mouse_pos()
1962
+ rect_br = self.get_resize_handle()
1963
+ inside_br = (rect_br[0] <= mx <= rect_br[2] and rect_br[1] <= my <= rect_br[3])
1964
+
1965
+ # Bottom right
1966
+ if inside_br:
1967
+ return DragMode.RESIZE_BR
1968
+
1969
+ return DragMode.WINDOW
1970
+
1971
+ def is_glfw_mouse_hovering_rect(self, x1, y1, x2, y2):
1972
+
1973
+ global_mouse = glfw.get_cursor_pos(Core.melty.glfw_window)
1974
+ ox, oy = getattr(Core.melty, "frame_origin", None) or (0, 0) # shadow margin (+ shift) → content coords
1975
+ mx, my = global_mouse[0] - ox, global_mouse[1] - oy
1976
+ # basic collision check
1977
+ if x1 <= mx <= x2 and y1 <= my <= y2:
1978
+ return True
1979
+ return False
1980
+
1981
+ def get_rect(self):
1982
+ width = self.width or 0
1983
+ height = self.height or 0
1984
+
1985
+ top = self.top or 0
1986
+ left = self.left or 0
1987
+
1988
+ right = left + width
1989
+ bottom = top + height
1990
+
1991
+ is_outside = left >= right or top >= bottom
1992
+ if is_outside:
1993
+ return (0, 0, 0, 0)
1994
+
1995
+ return (left, top, right - 1, bottom - 1)
1996
+
1997
+ def draw_rect(self, rounding=0, tint=None, rect=None):
1998
+ if Core.melty.channels_split:
1999
+ draw_list = imgui.get_window_draw_list()
2000
+ draw_list.channels_set_current(Core.melty.get_channel() + 1)
2001
+
2002
+ if tint is None:
2003
+ tint = getattr(self._input_value, 'tint', None)
2004
+
2005
+ draw_list = imgui.get_overlay_draw_list()
2006
+ draw_list.add_rect(self.left, self.top, self.left + self.width, self.top + self.height,
2007
+ pack_color(*tint[:3], 1.0) if tint is not None else
2008
+ pack_color(1, 1, 1, 1),
2009
+ rounding=rounding, thickness=1)
2010
+
2011
+ if Core.melty.channels_split:
2012
+ draw_list = imgui.get_window_draw_list()
2013
+ draw_list.channels_set_current(Core.melty.get_channel())
2014
+
2015
+ def is_inside_clip(self, child_draw_state=None):
2016
+ clip_rect = self.abs_clip_rect
2017
+
2018
+ if child_draw_state is None:
2019
+ child_draw_state = self
2020
+
2021
+ if clip_rect is None:
2022
+ return True, False, False
2023
+
2024
+ clip_left, clip_top, clip_right, clip_bottom = clip_rect
2025
+
2026
+ if child_draw_state is not None:
2027
+ left = child_draw_state.abs_left
2028
+ top = child_draw_state.abs_top
2029
+ width = child_draw_state.width
2030
+ height = child_draw_state.height
2031
+
2032
+ if top is None or left is None:
2033
+ return True, False, False
2034
+
2035
+ if width is None or height is None:
2036
+ return True, False, False
2037
+
2038
+ if (top + height < clip_top or top > clip_bottom):
2039
+ if top > clip_bottom:
2040
+ return False, True, False
2041
+ else:
2042
+ return False, False, False
2043
+ return True, False, False
2044
+
2045
+ @property
2046
+ def abs_clamped_rect(self):
2047
+ clip_rect = self.abs_clip_rect
2048
+ if clip_rect is None:
2049
+ return (self.abs_left, self.abs_top, self.abs_left + self.width, self.abs_top + self.height)
2050
+
2051
+ clip_left, clip_top, clip_right, clip_bottom = clip_rect
2052
+
2053
+ left = max(self.abs_left, clip_left)
2054
+ top = max(self.abs_top, clip_top)
2055
+ right = min(self.abs_left + self.width, clip_right)
2056
+ bottom = min(self.abs_top + self.height, clip_bottom)
2057
+
2058
+ return (left, top, right, bottom)
2059
+
2060
+ def hover_eligible(self, rect=None, ignore_reports=True, clip_rect=None):
2061
+ # `clip_rect`: a caller that already read abs_clamped_rect this call
2062
+ # passes it in - on_action's registration computes it for the cursor
2063
+ # rect too, ~240 registrations a frame on the code editor window.
2064
+ if self.just_shadow:
2065
+ return False
2066
+
2067
+ if self.closed or not Core.melty.imgui_main_window_hovered:
2068
+ return False
2069
+
2070
+ if not self.inside_clip:
2071
+ return False
2072
+
2073
+ if rect is None:
2074
+ # Lean on the BVH: begin_frame already point-tested each view against
2075
+ # the cursor (Melty.bvh_hover_ids), O(1) membership replaces
2076
+ # imgui.is_mouse_hovering_rect on this view's own bbox.
2077
+ if id(self) not in Core.melty.bvh_hover_ids:
2078
+ return False
2079
+ # The BVH stores raw bboxes, not clipped ones, so still check the
2080
+ # cursor inside the active clip (scrolled-away views aren't hovered).
2081
+ if clip_rect is None:
2082
+ clip_rect = self.abs_clamped_rect
2083
+ if clip_rect is not None:
2084
+ mx, my = imgui.get_mouse_pos()
2085
+ if not (clip_rect[0] <= mx <= clip_rect[2] and clip_rect[1] <= my <= clip_rect[3]):
2086
+ return False
2087
+ else:
2088
+ # Custom sub-region: clip it and point-test the cursor directly.
2089
+ # Clamp to abs_clamped_rect (bbox ∩ abs clip), not just the bbox -
2090
+ # a view scrolled under its parent's header still has the header
2091
+ # band inside its bbox, and clipping only to the bbox let it report
2092
+ # hover there and steal the header's drag (window_move) event.
2093
+ if clip_rect is None:
2094
+ clip_rect = self.abs_clamped_rect
2095
+ if clip_rect is not None:
2096
+ rect = (max(rect[0], clip_rect[0]), max(rect[1], clip_rect[1]),
2097
+ min(rect[2], clip_rect[2]), min(rect[3], clip_rect[3]))
2098
+ mx, my = imgui.get_mouse_pos()
2099
+ if not (rect[0] <= mx <= rect[2] and rect[1] <= my <= rect[3]):
2100
+ return False
2101
+
2102
+ if not (self.hover_reported is None or self.hover_reported or ignore_reports):
2103
+ return False
2104
+ return True
2105
+
2106
+ def _action_base_priority(self, z_pos=None):
2107
+ """on_action's default priority: the wrapper's z ordering inverted
2108
+ (0 = topmost). `z_pos=None` reads Melty's live layer/depth — what a
2109
+ call from inside this view's body sees."""
2110
+ max_layer_depth = Core.melty.max_depth * Core.melty.max_depth + Core.melty.max_depth
2111
+ if z_pos is None:
2112
+ z_pos = Core.melty.paint_rank * Core.melty.max_depth + Core.melty.depth
2113
+ return max_layer_depth - z_pos
2114
+
2115
+ def _register_action(self, view_id, event_names, registered_priority, rect, cursor,
2116
+ debug_priority=0, debug_delta=0, cursor_gate=None):
2117
+ """The registration half of on_action: hover-test `rect` (None = this
2118
+ view's bbox) and subscribe. Shared by the live call and the cache-hit
2119
+ replay so both obey the same z-order / blocker rules."""
2120
+ if rect is not None:
2121
+ # Cheapest test first: ~330 registrations a frame on the code
2122
+ # editor window (frame edges, scrollbars, buttons, dials), and the
2123
+ # pointer is inside a handful of them. hover_eligible clips the
2124
+ # rect before calling, and the clip can only shrink it, so a
2125
+ # pointer outside the input rect is outside the clipped one too -
2126
+ # skip the priority / clamped-rect work on those.
2127
+ mx, my = imgui.get_mouse_pos()
2128
+ if not (rect[0] <= mx <= rect[2] and rect[1] <= my <= rect[3]):
2129
+ return
2130
+ if not self.hover_eligible(rect):
2131
+ return
2132
+ cursor_rect = None
2133
+ if cursor is not None:
2134
+ # The input handler re-tests this rect against the LATEST pointer
2135
+ # position when it changes the shape (gl_gui/mouse_cursor.py), so
2136
+ # a slow frame can't hold the I-beam after the pointer has left.
2137
+ cursor_rect = self.abs_clamped_rect
2138
+ if rect is not None and cursor_rect is not None:
2139
+ cursor_rect = (max(rect[0], cursor_rect[0]), max(rect[1], cursor_rect[1]),
2140
+ min(rect[2], cursor_rect[2]), min(rect[3], cursor_rect[3]))
2141
+ elif rect is not None:
2142
+ cursor_rect = tuple(rect)
2143
+ Core.melty.event_handler.register_hovered(view_id, event_names,
2144
+ priority=registered_priority,
2145
+ tile_id=self._tile_id, cursor=cursor,
2146
+ cursor_rect=cursor_rect, cursor_gate=cursor_gate)
2147
+
2148
+ overlay = imgui.get_overlay_draw_list()
2149
+ overlay.channels_set_current(Core.melty.max_layer - 1)
2150
+ if Toggles.InputHandlerToggles.show_debug:
2151
+ for name in event_names:
2152
+ self._event_names.add(name)
2153
+
2154
+ ds = self
2155
+ color = (1,1,1)
2156
+ text = f"zpos:{self.layer} priority:{debug_priority} priority_delta:{debug_delta} | {str(self._event_names)}"
2157
+ invalidation_rect = (ds.abs_left, ds.abs_top,
2158
+ ds.abs_left + (ds.width or 0),
2159
+ ds.abs_top + (ds.height or 0))
2160
+ text_size = imgui.calc_text_size(text)
2161
+ overlay.add_rect_filled(invalidation_rect[0] + ds.width - text_size.x, invalidation_rect[1],
2162
+ invalidation_rect[0] + ds.width,
2163
+ invalidation_rect[1] + text_size.y,
2164
+ pack_color(*color[:3],
2165
+ 1))
2166
+
2167
+ overlay.add_text(invalidation_rect[0] + ds.width - text_size.x, invalidation_rect[1],
2168
+ pack_color(*(0, 0, 0),
2169
+ 1),
2170
+ text)
2171
+
2172
+ overlay.add_rect(invalidation_rect[0], invalidation_rect[1], invalidation_rect[2],
2173
+ invalidation_rect[3],
2174
+ pack_color(*color[:3],
2175
+ 1),
2176
+ thickness=1.0)
2177
+
2178
+ def replay_body_actions(self):
2179
+ """Blit-cache hit: the body did not run, so re-issue every on_action
2180
+ it made on its last real render (see on_action). Rects re-anchor to
2181
+ the LIVE abs position (the cached property lags a blit-served drag)
2182
+ and priorities to the current z_pos, then go through the normal
2183
+ hover / blocker gauntlet — a covered or un-hovered rect registers
2184
+ nothing, exactly as the live call would."""
2185
+ record = self._body_actions
2186
+ if not record or not record[1]:
2187
+ return
2188
+ if self.just_shadow or not Core.melty.inside_clip(draw_state=self):
2189
+ return
2190
+ base = self._action_base_priority(z_pos=self.z_pos)
2191
+ left, top = self._abs_left(), self._abs_top()
2192
+ for view_suffix, event_names, offset, relative_rect, cursor, cursor_gate in record[1]:
2193
+ view_id = self._tile_id if view_suffix is None else str(self._tile_id) + "_" + str(view_suffix)
2194
+ rect = None
2195
+ if relative_rect is not None:
2196
+ rect = (left + relative_rect[0], top + relative_rect[1],
2197
+ left + relative_rect[2], top + relative_rect[3])
2198
+ self._register_action(view_id, list(event_names), base + offset, rect, cursor,
2199
+ debug_priority=base + offset, cursor_gate=cursor_gate)
2200
+
2201
+ @property
2202
+ def priority(self):
2203
+ max_layer_depth = (Core.melty.max_depth *
2204
+ Core.melty.max_layer + Core.melty.max_depth)
2205
+ layer_and_depth = (Core.melty.paint_rank *
2206
+ Core.melty.max_depth + Core.melty.depth)
2207
+ return max_layer_depth - layer_and_depth
2208
+
2209
+ def get_content_rect(self):
2210
+ # An inline header shares the value row; it consumes horizontal space.
2211
+ header_height = 0 if self._kwargs.get('header_same_line', False) else self.header_height
2212
+ return (self.abs_left, self.abs_top + header_height,
2213
+ self.abs_left + self.width, self.abs_top + self.height - self.footer_height)
2214
+
2215
+ def get_header_rect(self):
2216
+ # header_width is the measured width of the drawn header, so the drag
2217
+ # handle covers only the label band; 0 (no header drawn / not yet
2218
+ # measured) falls back to the full view width.
2219
+ right = self.abs_left + (self.header_width if self.header_width else self.width)
2220
+ return (self.abs_left, self.abs_top, right, self.abs_top + self.header_height)
2221
+
2222
+ # Transient per-draw_state UI state that should travel with a value undo, so
2223
+ # undoing a text edit also restores the caret/selection/scroll to where they
2224
+ # were before the edit. Snapshotting is generic (just attribute names): a
2225
+ # widget that wants more state restored on undo adds its field names, with no
2226
+ # changes to core_render or the renderer itself.
2227
+ UNDO_STATE_FIELDS = (
2228
+ "text_cursor_pos", "text_prev_cursor_pos",
2229
+ "text_selection_start", "text_selection_end", "text_h_scroll",
2230
+ )
2231
+
2232
+ def capture_undo_state(self):
2233
+ """Snapshot the undo-tracked transient fields as a name->value dict."""
2234
+ return {f: getattr(self, f) for f in self.UNDO_STATE_FIELDS}
2235
+
2236
+ def apply_undo_state(self, snapshot):
2237
+ """Restore a snapshot produced by capture_undo_state()."""
2238
+ if not snapshot:
2239
+ return
2240
+ for f, v in snapshot.items():
2241
+ setattr(self, f, v)
2242
+
2243
+ def get_action(self, event_name, view_id=None):
2244
+ """Read an already-delivered event without registering a hit region.
2245
+
2246
+ Geometry owners consume input, resolve their rectangle, then subscribe
2247
+ with on_action using that final rectangle for the next input dispatch.
2248
+ """
2249
+ identity = self._tile_id if view_id is None else f"{self._tile_id}_{view_id}"
2250
+ return Core.melty.events.get(identity, {}).get(event_name)
2251
+
2252
+ def on_action(self, event_names, view_id=None, priority=None, priority_delta=0, rect=None, cursor=None,
2253
+ cursor_gate=None):
2254
+ """Subscribe this view to `event_names` (or, with an empty list, just
2255
+ tag `rect` with a pointer `cursor` shape) for this frame.
2256
+ `cursor_gate="left_mouse_dragged"` shows the shape only where this
2257
+ subscription is the one that event would be captured by
2258
+ (InputHandler.register_hovered) — a drag handle's shape.
2259
+
2260
+ Called from a view BODY it is recorded on the draw_state as well
2261
+ (`_body_actions`), because the wrapper skips the body on a blit-cache
2262
+ hit and InputHandler forgets every subscription each frame — without
2263
+ the record a cached tile could not latch a drag or keep its I-beam
2264
+ (`replay_body_actions` re-issues the record on a hit). Recording only
2265
+ happens for the frame the wrapper opened the record in, so pre-gate
2266
+ calls on cached frames never pile up."""
2267
+ if self.parent_window is None and not self.closable:
2268
+ priority_delta -= 1
2269
+
2270
+ if not Core.melty.inside_clip(draw_state=self):
2271
+ return None
2272
+ if self.just_shadow:
2273
+ return None
2274
+ view_suffix = view_id
2275
+ if view_id is None:
2276
+ view_id = self._tile_id
2277
+ else:
2278
+ view_id = str(self._tile_id) + "_" + str(view_id)
2279
+
2280
+ single_event = False
2281
+ if isinstance(event_names, str):
2282
+ event_names = [event_names]
2283
+ single_event = True
2284
+
2285
+ if priority is None:
2286
+ priority = self._action_base_priority()
2287
+ registered_priority = priority - priority_delta
2288
+
2289
+ record = self._body_actions
2290
+ if record is not None and record[0] == Core.melty.frame_count:
2291
+ # Priority is kept as an offset from this view's z_pos and the rect
2292
+ # relative to its abs position: both are re-derived on replay, so a
2293
+ # view that moved (blit-served drag) or changed layer replays right.
2294
+ if rect is None:
2295
+ relative_rect = None
2296
+ else:
2297
+ left, top = self.abs_left, self.abs_top
2298
+ relative_rect = (rect[0] - left, rect[1] - top, rect[2] - left, rect[3] - top)
2299
+ record[1].append((view_suffix, tuple(event_names),
2300
+ registered_priority - self._action_base_priority(z_pos=self.z_pos),
2301
+ relative_rect, cursor, cursor_gate))
2302
+
2303
+ self._register_action(view_id, event_names, registered_priority, rect, cursor,
2304
+ debug_priority=priority, debug_delta=priority_delta,
2305
+ cursor_gate=cursor_gate)
2306
+
2307
+ if single_event:
2308
+ return self.get_action(event_names[0], view_id=view_suffix)
2309
+
2310
+ return_events = {}
2311
+ if view_id in Core.melty.events:
2312
+ for event_name in Core.melty.events[view_id]:
2313
+ return_events[event_name] = Core.melty.events[view_id][event_name]
2314
+ return return_events
2315
+
2316
+ def event_rect(self, event_names, rect):
2317
+ """Narrow a declared event PARAM to `rect` (screen coords) — the hit
2318
+ area the wrapper registers it over, instead of the whole content
2319
+ rect. Call it from the view body every run, next to the geometry
2320
+ that defines the rect; several calls for the same name union their
2321
+ rects. Outside the rects the param is simply not subscribed, so a
2322
+ press/drag there falls through to whatever sits behind this view in
2323
+ z-order — the enclosing window's move handle, a parent's scroll —
2324
+ with no forwarding code. draw_text scopes `left_mouse_drag` to its
2325
+ text rows this way so the gutter and the space below the last line
2326
+ drag the window. Rects are stored relative to the view origin (a
2327
+ blit-served window drag moves the view without re-running the body)
2328
+ and re-anchored at registration; the wrapper reads the record of the
2329
+ LAST body run when it registers the params for this one, then
2330
+ clears it, so a run that stops declaring lifts the scope. Not the
2331
+ tool for an EXTRA sub-rect the body wants events from — that is
2332
+ `on_action(event, view_id=…, rect=…)`."""
2333
+ if isinstance(event_names, str):
2334
+ event_names = (event_names,)
2335
+ left, top = self.abs_left, self.abs_top
2336
+ if left is None or top is None:
2337
+ return
2338
+ relative_rect = (rect[0] - left, rect[1] - top, rect[2] - left, rect[3] - top)
2339
+ record = self._event_rects
2340
+ if record is None:
2341
+ record = self._event_rects = {}
2342
+ for name in event_names:
2343
+ record.setdefault(name, []).append(relative_rect)
2344
+
2345
+ def is_bounding_hovered(self):
2346
+ if (self._imgui_is_active or self._imgui_is_edited or self._imgui_is_item_hovered or self._imgui_popover_open):
2347
+ return True
2348
+
2349
+ if self.top is None or self.left is None or self.width is None or self.height is None:
2350
+ return False
2351
+
2352
+ mouse_x, mouse_y = imgui.get_mouse_pos()
2353
+ if not Core.melty.inside_clip(rect=(mouse_x, mouse_y, 1, 1)):
2354
+ return False
2355
+
2356
+ if not (Core.melty.imgui_main_window_hovered or Core.melty.imgui_popup_open):
2357
+ return False
2358
+
2359
+ # Cursor-over-this-view comes from the BVH hit test, not is_mouse_hovering_rect.
2360
+ return id(self) in Core.melty.bvh_hover_ids
2361
+
2362
+
2363
+ class KeyMod(Enum):
2364
+ CTRL = 'ctrl'
2365
+ ALT = 'alt'
2366
+ SHIFT = 'shift'
2367
+
2368
+
2369
+ class Hotkey:
2370
+ def __init__(self, key=None, name="", mod=None, scoped=True):
2371
+ self.name = name
2372
+ self.key = key
2373
+ self.mod = mod
2374
+ self.scoped = scoped
2375
+
2376
+ # hashing and equality based on key and mod only
2377
+ def __hash__(self):
2378
+ return hash((self.key, self.mod))
2379
+
2380
+ def __eq__(self, other):
2381
+ if not isinstance(other, Hotkey):
2382
+ return False
2383
+ return self.key == other.key and self.mod == other.mod
2384
+
2385
+ def mod_active(self):
2386
+ if self.mod == KeyMod.CTRL:
2387
+ return imgui.get_io().key_ctrl
2388
+ elif self.mod == KeyMod.ALT:
2389
+ return imgui.get_io().key_alt
2390
+ elif self.mod == KeyMod.SHIFT:
2391
+ return imgui.get_io().key_shift
2392
+
2393
+ if self.mod is None:
2394
+ return True