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,281 @@
1
+ """Timeline logging for diagnosing cross-thread slowness (symbol index / code
2
+ host load). NOT a profiler: many interacting threads plus GIL-bound parsing make
3
+ sampling misleading, so instead every meaningful unit of work writes one line
4
+ with wall-clock time, frame count, and thread label — the log reads as a single
5
+ interleaved timeline. A gap in frame numbers while a worker line is open = the
6
+ GIL was held; overlapping spans show which threads stacked up.
7
+
8
+ 12:34:56.789 f001234 [render ] ensure_index: spawn recompute file=toggles.py
9
+
10
+ Usage:
11
+ from meltygui.core.diagnostics.perf_trace import trace, trace_rl, span, once
12
+
13
+ trace("warmer build start", files=150)
14
+ with span("cold compute", file=name): # logs "... took 812.4ms" on exit
15
+ ...
16
+ with span("probe", min_ms=2.0): ... # silent unless >= 2ms
17
+ trace_rl("slow-probe", "probe slow", ...) # at most 1 line/sec per key
18
+ if once(("host", name)): trace(...) # once per key per session
19
+
20
+ Gate: Toggles.symbol_perf_log (missing Toggles => enabled). Sink is LOG_PATH,
21
+ append mode with a session header — append (not truncate) so a jedi-pool child
22
+ process importing this module can't wipe the parent's log mid-run. Never raises:
23
+ a logging failure must not take down the render loop.
24
+
25
+ Everything here is stdlib-only; project state is read via sys.modules (no
26
+ imports) so this module is importable from anywhere without cycles.
27
+ """
28
+ import os
29
+ import sys
30
+ import threading
31
+ import time
32
+ from meltygui.core.runtime.paths import debug_log_path
33
+
34
+ LOG_PATH = debug_log_path("lsd_symbol_perf.log")
35
+ _MAX_CARRYOVER_BYTES = 5 * 1024 * 1024 # start fresh when the file grows past this
36
+
37
+ _lock = threading.Lock()
38
+ _rl_last: dict = {} # rate-limit key -> last emit monotonic
39
+ _once_keys: set = set() # keys already emitted via once()
40
+
41
+
42
+ def _enabled() -> bool:
43
+ try:
44
+ from meltygui.core.runtime.toggles import Toggles
45
+ return bool(Toggles.symbol_perf_log)
46
+ except Exception:
47
+ return True
48
+
49
+
50
+ def enabled() -> bool:
51
+ """Public gate for callers that do per-frame work BEYOND logging (e.g.
52
+ the GPU frame timer's query objects) — same toggle as trace()."""
53
+ return _enabled()
54
+
55
+
56
+ def _open_log():
57
+ """One line-buffered append handle per process, adopted across hotswap /
58
+ dual-import via sys (the established sharing pattern for process singletons)."""
59
+ fh = getattr(sys, "_symbol_perf_fh", None)
60
+ if fh is not None:
61
+ return fh
62
+ try:
63
+ mode = "a"
64
+ try:
65
+ if os.path.getsize(LOG_PATH) > _MAX_CARRYOVER_BYTES:
66
+ mode = "w"
67
+ except OSError:
68
+ pass
69
+ fh = open(LOG_PATH, mode, buffering=1)
70
+ fh.write(f"\n=== session start pid={os.getpid()} "
71
+ f"{time.strftime('%Y-%m-%d %X')} ===\n")
72
+ except Exception:
73
+ fh = False # sentinel: don't retry every call
74
+ sys._symbol_perf_fh = fh
75
+ return fh
76
+
77
+
78
+ def _frame() -> int:
79
+ mel = (sys.modules.get("meltygui.core.melty")
80
+ or sys.modules.get("lsd.gl_gui.melty"))
81
+ try:
82
+ return mel.Melty.frame_count if mel is not None else -1
83
+ except Exception:
84
+ return -1
85
+
86
+
87
+ def _thread_label() -> str:
88
+ t = threading.current_thread()
89
+ try:
90
+ gs = (sys.modules.get("meltygui.core.graphics.gl_state")
91
+ or sys.modules.get("lsd.gl_gui.gl_state"))
92
+ if gs is not None and getattr(gs, "_gl_thread", None) is t:
93
+ return "render"
94
+ except Exception:
95
+ pass
96
+ name = t.name
97
+ if name == "MainThread":
98
+ return "main"
99
+ # ThreadPoolExecutor-0_3 -> pool_3 (Background pool workers)
100
+ if name.startswith("ThreadPoolExecutor"):
101
+ return "pool_" + name.rsplit("_", 1)[-1]
102
+ return name
103
+
104
+
105
+ def _fmt_fields(fields: dict) -> str:
106
+ if not fields:
107
+ return ""
108
+ parts = []
109
+ for k, v in fields.items():
110
+ if isinstance(v, float):
111
+ v = f"{v:.1f}"
112
+ parts.append(f"{k}={v}")
113
+ return " " + " ".join(parts)
114
+
115
+
116
+ def trace(msg: str, **fields):
117
+ """One timeline line. Swallows every failure."""
118
+ if not _enabled():
119
+ return
120
+ try:
121
+ fh = _open_log()
122
+ if not fh:
123
+ return
124
+ now = time.time()
125
+ ts = time.strftime("%H:%M:%S", time.localtime(now)) + f".{int(now % 1 * 1000):03d}"
126
+ line = (f"{ts} f{_frame():06d} [{_thread_label():<15}] "
127
+ f"{msg}{_fmt_fields(fields)}\n")
128
+ with _lock:
129
+ fh.write(line)
130
+ except Exception:
131
+ pass
132
+
133
+
134
+ def trace_rl(key, msg: str, min_interval: float = 1.0, **fields):
135
+ """Rate-limited trace: at most one line per `min_interval` seconds per key.
136
+ For per-frame paths that are only interesting when they stay slow."""
137
+ if not _enabled():
138
+ return
139
+ try:
140
+ now = time.monotonic()
141
+ last = _rl_last.get(key)
142
+ if last is not None and now - last < min_interval:
143
+ return
144
+ _rl_last[key] = now
145
+ except Exception:
146
+ return
147
+ trace(msg, **fields)
148
+
149
+
150
+ def once(key) -> bool:
151
+ """True the first time `key` is seen this session — for once-only lines."""
152
+ try:
153
+ if key in _once_keys:
154
+ return False
155
+ _once_keys.add(key)
156
+ return True
157
+ except Exception:
158
+ return False
159
+
160
+
161
+ class span:
162
+ """Context manager logging '<label> took Xms' on exit (only when >= min_ms).
163
+ Extra context can be attached mid-span via .add(k=v); an exception inside
164
+ the span is noted on the line and re-raised."""
165
+
166
+ __slots__ = ("label", "min_ms", "fields", "t0", "c0")
167
+
168
+ def __init__(self, label: str, min_ms: float = 0.0, **fields):
169
+ self.label = label
170
+ self.min_ms = min_ms
171
+ self.fields = fields
172
+ self.t0 = 0.0
173
+ self.c0 = 0.0
174
+
175
+ def add(self, **fields):
176
+ self.fields.update(fields)
177
+
178
+ def __enter__(self):
179
+ self.t0 = time.monotonic()
180
+ self.c0 = time.thread_time()
181
+ return self
182
+
183
+ def __exit__(self, exc_type, exc, tb):
184
+ try:
185
+ dt_ms = (time.monotonic() - self.t0) * 1000.0
186
+ if dt_ms >= self.min_ms:
187
+ # cpu ≪ wall on a slow span = this thread was GIL-starved, not
188
+ # doing the work - the span label is then the victim, not the
189
+ # culprit (see the 2026-07-31 stall hunts).
190
+ cpu_ms = (time.thread_time() - self.c0) * 1000.0
191
+ suffix = " EXC=" + exc_type.__name__ if exc_type is not None else ""
192
+ trace(f"{self.label} took {dt_ms:.1f}ms (cpu {cpu_ms:.1f}ms){suffix}",
193
+ **self.fields)
194
+ except Exception:
195
+ pass
196
+ return False
197
+
198
+
199
+ # ── Stall watchdog: what is the render thread blocked on? ──────────────────────
200
+ # Wall≫cpu slow frames (the post-boot 1000ms+ bursts) mean the render thread
201
+ # is WAITING - lock, GIL, GL/present backpressure - and the per-phase spans
202
+ # can't pin on what. This daemon samples Melty's clock; when it sits still
203
+ # past `threshold_s` while the render thread is mid-frame (NOT parked in
204
+ # glfw wait_events - an idle studio is not a stall), it dumps every thread's
205
+ # current call stack to the log. One dump per stall, re-armed when the frame
206
+ # counter moves; a second dump is forced if the stall passes 3x threshold.
207
+ # Cost when healthy: one attribute read per poll (20Hz). Same toggle as trace.
208
+
209
+ def _render_thread():
210
+ gs = (sys.modules.get("meltygui.core.graphics.gl_state")
211
+ or sys.modules.get("lsd.gl_gui.gl_state"))
212
+ return getattr(gs, "_gl_thread", None) if gs is not None else None
213
+
214
+
215
+ def _dump_all_stacks(reason: str):
216
+ import traceback
217
+ frames = sys._current_frames()
218
+ render_t = _render_thread()
219
+ for t in threading.enumerate():
220
+ frame = frames.get(t.ident)
221
+ if frame is None:
222
+ continue
223
+ stack = traceback.extract_stack(frame)[-10:]
224
+ chain = " <- ".join(
225
+ f"{fs.filename.rsplit('/', 1)[-1]}:{fs.lineno} {fs.name}"
226
+ for fs in reversed(stack))
227
+ label = "render" if t is render_t else t.name
228
+ trace(f"STALL {reason} [{label}] {chain}")
229
+
230
+
231
+ def _stall_watchdog(threshold_s: float, poll_s: float):
232
+ last_count = -1
233
+ still_since = time.monotonic()
234
+ dumped = 0
235
+ while True:
236
+ time.sleep(poll_s)
237
+ try:
238
+ if not _enabled():
239
+ continue
240
+ count = _frame()
241
+ now = time.monotonic()
242
+ if count != last_count:
243
+ last_count = count
244
+ still_since = now
245
+ dumped = 0
246
+ continue
247
+ stalled_s = now - still_since
248
+ want = 1 if stalled_s >= threshold_s else 0
249
+ if want and stalled_s >= threshold_s * 3:
250
+ want = 2
251
+ if dumped >= want:
252
+ continue
253
+ render_t = _render_thread()
254
+ frame = sys._current_frames().get(render_t.ident) if render_t else None
255
+ if frame is None:
256
+ continue
257
+ # Parked between frames = idle, not a stall. wait_events blocks
258
+ # there; poll_events/sleep cover some launcher-style loops.
259
+ names = set()
260
+ f = frame
261
+ while f is not None and len(names) < 12:
262
+ names.add(f.f_code.co_name)
263
+ f = f.f_back
264
+ if {"wait_events", "poll_events"} & names:
265
+ still_since = now
266
+ continue
267
+ dumped = want
268
+ _dump_all_stacks(f"{stalled_s:.2f}s frame={count}")
269
+ except Exception:
270
+ pass # the watchdog must never hurt the app
271
+
272
+
273
+ def ensure_stall_watchdog(threshold_s: float = 0.35, poll_s: float = 0.05):
274
+ """Idempotent, process-lifetime (sys-guarded like the log handle — an
275
+ in-process studio restart adopts the running one instead of stacking)."""
276
+ if getattr(sys, "_lsd_stall_watchdog", None) is not None:
277
+ return
278
+ t = threading.Thread(target=_stall_watchdog, args=(threshold_s, poll_s),
279
+ name="stall-watchdog", daemon=True)
280
+ sys._lsd_stall_watchdog = t
281
+ t.start()
@@ -0,0 +1,88 @@
1
+ import functools
2
+ from typing import Callable, Any
3
+ from collections import deque, defaultdict
4
+
5
+ from meltygui.core.melty import Melty
6
+
7
+
8
+ def profile(func: Callable) -> Callable:
9
+ """
10
+ Decorator that profiles a function line-by-line and stores results in Melty.profiles_results.
11
+
12
+ Usage:
13
+ @profile
14
+ def my_function():
15
+ # your code here
16
+ pass
17
+
18
+ The profiling results will be available at:
19
+ Melty.profiles_results['my_function'][-1] # Most recent call
20
+
21
+ Each result is a list of (line_of_code, time_ms) tuples sorted by time.
22
+
23
+ Note: Requires line_profiler package. Install with: pip install line_profiler
24
+ """
25
+ # Track if we're already profiling this function (for recursive calls)
26
+ _profiling = False
27
+
28
+ @functools.wraps(func)
29
+ def wrapper(*args, **kwargs):
30
+ nonlocal _profiling
31
+
32
+ # Only profile at the top level to avoid nested profiler conflicts
33
+ if _profiling:
34
+ return func(*args, **kwargs)
35
+
36
+ try:
37
+ from line_profiler import LineProfiler
38
+ except ImportError:
39
+ # Fallback: just run the function without profiling
40
+ print("Warning: line_profiler not installed. Install with: pip install line_profiler")
41
+ return func(*args, **kwargs)
42
+
43
+ if func.__name__ not in Melty.profiles_results:
44
+ # Create a line profiler
45
+ profiler = LineProfiler()
46
+ profiler.add_function(func)
47
+
48
+ # Profile the function execution
49
+ _profiling = True
50
+ try:
51
+ profiler.enable()
52
+ result = func(*args, **kwargs)
53
+ profiler.disable()
54
+ finally:
55
+ _profiling = False
56
+
57
+ # Extract the line-by-line stats
58
+ filtered_results = []
59
+ stats = profiler.get_stats()
60
+
61
+ # stats.timings is a dict: {(filename, line_start, func_name): [(lineno, nhits, time), ...]}
62
+ for key, timings in stats.timings.items():
63
+ filename, line_start, func_name_inner = key
64
+
65
+ # Read the source code
66
+ import linecache
67
+
68
+ for lineno, nhits, time in timings:
69
+ if nhits > 0: # Only include lines that were executed
70
+ source_line = linecache.getline(filename, lineno).strip()
71
+ if source_line:
72
+ # line_profiler time is in units (typically nanoseconds, 1e-09)
73
+ # Convert to milliseconds: time * unit * 1000 (to go from seconds to ms)
74
+ time_ms = time * stats.unit * 1000
75
+ filtered_results.append((source_line, time_ms, lineno))
76
+
77
+ # Sort by time (slowest first)
78
+ filtered_results.sort(key=lambda x: x[1], reverse=True)
79
+
80
+ # Store the profile results (deque automatically to max 5 items)
81
+ Melty.profiles_results[func.__name__] = filtered_results
82
+
83
+ return result
84
+ else:
85
+ return func(*args, **kwargs)
86
+
87
+ return wrapper
88
+
@@ -0,0 +1,62 @@
1
+ """Bounded, always-on resize diagnostics; records metadata, never view values.
2
+
3
+ Each process writes .melty/resize-<pid>.log (JSON lines, two 4 MiB backups).
4
+ No draw-state references are retained and logging failures cannot break a drag.
5
+ """
6
+ import json
7
+ import logging
8
+ from logging.handlers import RotatingFileHandler
9
+ import os
10
+ from pathlib import Path
11
+ import time
12
+ import traceback
13
+
14
+
15
+ def record(stage, window, event=None, error=False, **details):
16
+ try:
17
+ from meltygui.core.melty import Melty
18
+ logger = logging.getLogger(f"meltygui.resize.{os.getpid()}")
19
+ if not logger.handlers:
20
+ from meltygui.core.runtime.paths import cache_root
21
+ path = cache_root()
22
+ path.mkdir(parents=True, exist_ok=True)
23
+ handler = RotatingFileHandler(path / f"resize-{os.getpid()}.log",
24
+ maxBytes=4 * 1024 * 1024, backupCount=2)
25
+ logger.addHandler(handler)
26
+ logger.setLevel(logging.INFO)
27
+ logger.propagate = False
28
+ entry = dict(time=time.time(), frame=Melty.frame_count, stage=stage,
29
+ window=str(getattr(window, 'id', None)), identity=id(window),
30
+ name=str(getattr(window, 'name', ''))[:200])
31
+ for key in ('window_pos', 'width', 'height', 'abs_left', 'abs_top',
32
+ 'min_width', 'min_height', 'closed', 'expanded', 'use_cache',
33
+ 'freeze_resize', 'size_change', '_frame_pinned',
34
+ '_initial_window_size', '_initial_window_pos_resize',
35
+ '_resize_from_top_left', '_resize_target_edge_x0', '_resize_target_row_y0'):
36
+ entry[key] = getattr(window, key, None)
37
+ for key, axis in (('_resize_target_edge', 'x'), ('_resize_target_row', 'y')):
38
+ edge = getattr(window, key, None)
39
+ entry[key] = None if edge is None else (id(edge), edge.get(axis))
40
+ if event is not None:
41
+ entry['event'] = {key: getattr(event, key, None)
42
+ for key in ('x', 'y', 'dx', 'dy', 'total_dx', 'total_dy')}
43
+ if error:
44
+ entry['traceback'] = traceback.format_exc()
45
+ import meltygui.core.windowing.os_frame as os_frame
46
+ entry['geometry_mode'] = os_frame._STATE['mode']
47
+ entry['geometry_generation'] = os_frame._STATE['generation']
48
+ entry['os_expected'] = list(os_frame._STATE['expected'])
49
+ entry['os_unapplied'] = list(os_frame._STATE['unapplied'])
50
+ entry.update(details)
51
+ logger.info(json.dumps(entry, default=lambda value: f'<{type(value).__name__}>'))
52
+ except Exception:
53
+ # Diagnostics must never turn an otherwise valid frame into a failure.
54
+ pass
55
+
56
+
57
+ def edges(window, axis):
58
+ """Small identity/geometry snapshot; never serialize a draw_state or its data."""
59
+ registry = getattr(window, '_edge_views' if axis == 'x' else '_row_views', {})
60
+ return [{"owner": str(key), "closed": getattr(view, 'closed', False),
61
+ "edges": [(id(edge), edge.get(axis)) for edge in edge_list]}
62
+ for key, (view, edge_list) in registry.items()]
@@ -0,0 +1,247 @@
1
+ """Region screenshot tool — Ctrl+Shift+3 (draw_main's root hotkey) or
2
+ Actions.screenshot arms it; a crosshair follows the cursor on the overlay
3
+ draw list; click-drag a box; on release the pixels inside the box are read
4
+ from the main framebuffer (GL_BACK, after the frame is fully composited — the
5
+ deferred capture queue in screenshot.py), saved as a PNG under the configured
6
+ shot dir, added to the code editor as a tab (its ImageCodec view) WITHOUT
7
+ switching to it, and its path is put on the clipboard.
8
+
9
+ State is module-level (one tool, never more than one capture in flight);
10
+ `draw(draw_state)` runs from draw_main every frame and is a no-op unless
11
+ armed. While armed the tool owns the mouse: full-screen BLOCKING left-button
12
+ subscriptions at a priority above every window and blocker, so the press
13
+ that starts the box can't land on whatever sits under the cursor. Esc (or
14
+ the hotkey again) cancels.
15
+
16
+ The crosshair + camera are the CURSOR IMAGE while armed (glfw.create_cursor
17
+ from a PIL render, hotspot at the crosshair centre), not overlay drawing:
18
+ anything drawn in a frame uses the frame-start mouse sample and shows a
19
+ frame later, while the compositor moves the cursor plane with zero latency
20
+ — overlay crosshairs visibly trailed the pointer. The cursor plane is the
21
+ one thing that can sit exactly where the pointer is. Only the drag box
22
+ (anchored at the press) is drawn on the overlay, with a readout pill beside
23
+ it — top-left `x, y` and `w × h` in window points — so the tool doubles as
24
+ a measure tool (drag over a thing, read its rect off the label, Esc).
25
+ """
26
+
27
+ import meltygui.core.windowing.window_api as glfw
28
+ import meltygui_imgui as imgui
29
+ from meltygui.hdr_color import pack_color
30
+
31
+ import meltygui.core.input.mouse_cursor as mouse_cursor
32
+ from meltygui.core.melty import Melty
33
+ from meltygui.core.windowing.glfw_utils import request_render
34
+ from meltygui.core.rendering.core_decoration import Core
35
+
36
+ # Above every window / blocker (the live lab's blocking handlers use 1024).
37
+ _PRIORITY_DELTA = 4096
38
+ _MIN_BOX_PX = 3 # smaller than this on release = a click, not a box
39
+ _ICON = "\uf030" # FA camera (baked into the cursor image)
40
+ _BOX_COLOR = (0.35, 0.75, 1.0, 1.0)
41
+ _FILL_COLOR = (0.35, 0.75, 1.0, 0.12)
42
+ _CURSOR_SIZE, _CURSOR_HOT, _CURSOR_ARM, _CURSOR_GAP = 64, 20, 16, 3
43
+
44
+
45
+ class RegionScreenshot:
46
+ armed = False
47
+ start = None # (x, y) in points where the drag began; None = no box yet
48
+ cursor = None # *cursor* (built once, render thread)
49
+ cursor_set = False # our cursor is the window's current cursor
50
+
51
+
52
+ def _build_cursor_image():
53
+ """Crosshair (gap around the hotspot, dark halo under a white hairline)
54
+ with the FA camera glyph below-right — the same glyph Actions.screenshot
55
+ wears. Returns (PIL image, hotspot)."""
56
+ from PIL import Image, ImageDraw, ImageFont
57
+ from meltygui.core.styling.fonts import _RESOURCES
58
+ size, hot, arm, gap = _CURSOR_SIZE, _CURSOR_HOT, _CURSOR_ARM, _CURSOR_GAP
59
+ img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
60
+ d = ImageDraw.Draw(img)
61
+ for col, w in (((0, 0, 0, 140), 3), ((255, 255, 255, 230), 1)):
62
+ for seg in ((hot - arm, hot, hot - gap, hot), (hot + gap, hot, hot + arm, hot),
63
+ (hot, hot - arm, hot, hot - gap), (hot, hot + gap, hot, hot + arm)):
64
+ d.line(seg, fill=col, width=w)
65
+ font = ImageFont.truetype(str(_RESOURCES / "fontawesome-webfont.ttf"), 22)
66
+ gx, gy = hot + 9, hot + 7
67
+ d.text((gx + 1, gy + 1), _ICON, font=font, fill=(0, 0, 0, 150))
68
+ d.text((gx, gy), _ICON, font=font, fill=(255, 255, 255, 235))
69
+ return img, hot
70
+
71
+
72
+ def _set_tool_cursor(on):
73
+ """Swap the window cursor to the crosshair/camera image (on) or back to
74
+ the default (off). GLFW calls belong on the event-pumping thread — the
75
+ render thread — which is where draw() runs."""
76
+ if RegionScreenshot.cursor_set == on:
77
+ return
78
+ window = glfw.get_current_context()
79
+ if window is None:
80
+ return
81
+ try:
82
+ if on and RegionScreenshot.cursor is None:
83
+ img, hot = _build_cursor_image()
84
+ RegionScreenshot.cursor = glfw.create_cursor(img, hot, hot)
85
+ glfw.set_cursor(window, RegionScreenshot.cursor if on else None)
86
+ RegionScreenshot.cursor_set = on
87
+ # Keep the per-frame shape push (mouse_cursor.apply) from our image.
88
+ mouse_cursor.note_external_cursor(on)
89
+ except Exception as e:
90
+ print(f"region_screenshot: cursor swap failed: {e}")
91
+
92
+
93
+ def arm():
94
+ """Enter capture mode (idempotent)."""
95
+ RegionScreenshot.armed = True
96
+ RegionScreenshot.start = None
97
+ request_render()
98
+
99
+
100
+ def cancel():
101
+ RegionScreenshot.armed = False
102
+ RegionScreenshot.start = None
103
+ request_render()
104
+
105
+
106
+ def toggle():
107
+ cancel() if RegionScreenshot.armed else arm()
108
+
109
+
110
+ def _open_captured(path):
111
+ """screenshot.py's on_captured: runs on the render thread inside
112
+ post_frame — defer the editor work to between frames, like any other
113
+ external window mutation. The shot becomes an editor TAB without
114
+ stealing the selection (OpenFiles.open_file only — not open_in_editor,
115
+ whose jump_to_path the editor adopts as its active tab): the user is
116
+ mid-work in whatever they were screenshotting. The tab bars repaint so
117
+ the new tab shows. The saved file's PATH also goes on the clipboard
118
+ (text, via GLFW's clipboard — the studio is the focused Wayland client,
119
+ so this is the one clipboard write that always lands)."""
120
+ from meltygui.core.runtime.extensions import source_window as editor_window_draw_state
121
+
122
+ def _land():
123
+ open_files = getattr(getattr(Melty.vis, "root", None), "open_files", None)
124
+ if open_files is not None:
125
+ open_files.open_file(path)
126
+ # The tab list changed under the editors' bodies: force both
127
+ # instances through their blit cache so the new tab appears.
128
+ for inst in (0, 1):
129
+ win = editor_window_draw_state(inst)
130
+ if win is not None and Melty.cache is not None and win._tile_id is not None:
131
+ Melty.cache.invalidate_up(win._tile_id, force=True, max_depth=4)
132
+ imgui.set_clipboard_text(str(path))
133
+ request_render()
134
+ Melty.post_to_render(_land)
135
+
136
+
137
+ def _finish(x0, y0, x1, y1):
138
+ """Release: queue the framebuffer read of the box (settled a couple of
139
+ frames so this frame's overlay — crosshair, box — has cleared)."""
140
+ from meltygui.core.graphics.screenshot import request_region_capture
141
+ RegionScreenshot.armed = False
142
+ RegionScreenshot.start = None
143
+ left, top = min(x0, x1), min(y0, y1)
144
+ w, h = abs(x1 - x0), abs(y1 - y0)
145
+ if w < _MIN_BOX_PX or h < _MIN_BOX_PX:
146
+ # A click, not a box: stay armed so the user can try again.
147
+ RegionScreenshot.armed = True
148
+ request_render()
149
+ return
150
+ request_region_capture(left, top, w, h, Melty.frame_count,
151
+ name="screenshot", on_captured=_open_captured)
152
+
153
+
154
+ def box_label_lines(x0, y0, x1, y1):
155
+ """The readout for a normalized box (x0 <= x1, y0 <= y1), in WINDOW
156
+ POINTS — the coordinate system every draw_state rect and the capture
157
+ itself use, so a measurement read off the label maps straight onto
158
+ `draw_state.abs_left` / `width` values. Line 1 = top-left corner,
159
+ line 2 = size."""
160
+ x, y = int(round(x0)), int(round(y0))
161
+ w, h = int(round(x1 - x0)), int(round(y1 - y0))
162
+ return (f"{x}, {y}", f"{w} × {h}")
163
+
164
+
165
+ def label_rect(x0, y0, x1, y1, label_w, label_h, display_w, display_h, gap):
166
+ """Where the readout pill goes: below the box, right-aligned to its
167
+ right edge (outside, so the measured content stays visible). Off the
168
+ bottom of the display → inside the box's bottom-right corner; past the
169
+ right edge → slid left to fit; a box that fills the screen → inside,
170
+ clamped. Returns (left, top)."""
171
+ left = x1 - label_w
172
+ top = y1 + gap
173
+ if top + label_h > display_h:
174
+ top = y1 - gap - label_h
175
+ left = x1 - gap - label_w
176
+ left = max(0.0, min(left, display_w - label_w))
177
+ top = max(0.0, top)
178
+ return left, top
179
+
180
+
181
+ def _draw_box_label(overlay, x0, y0, x1, y1, display_w, display_h):
182
+ """Paint the coordinate/size readout next to the drag box (the "measure
183
+ tool" half of the feature): a dark pill, box-coloured text."""
184
+ # [tint=(0.35, 0.75, 1.0)]
185
+ padding = 5
186
+ # [tint=(0.95, 0.61, 0.07)]
187
+ gap = 6
188
+ # [tint=(0.36, 0.68, 0.89)]
189
+ label_background = (0.05, 0.05, 0.08, 0.85)
190
+ lines = box_label_lines(x0, y0, x1, y1)
191
+ sizes = [imgui.calc_text_size(line) for line in lines]
192
+ text_w = max(size.x for size in sizes)
193
+ line_h = sizes[0].y
194
+ label_w = text_w + padding * 2
195
+ label_h = line_h * len(lines) + padding * 2
196
+ left, top = label_rect(x0, y0, x1, y1, label_w, label_h, display_w, display_h, gap)
197
+ overlay.add_rect_filled(left, top, left + label_w, top + label_h,
198
+ pack_color(*label_background), rounding=4.0)
199
+ overlay.add_rect(left, top, left + label_w, top + label_h,
200
+ pack_color(*_BOX_COLOR), rounding=4.0, thickness=1.0)
201
+ text_color = pack_color(*_BOX_COLOR)
202
+ for index, line in enumerate(lines):
203
+ overlay.add_text(left + padding, top + padding + line_h * index, text_color, line)
204
+
205
+
206
+ def draw(draw_state):
207
+ """Per-frame body (from draw_main): claim the mouse, track the box, paint
208
+ the crosshair/box + its coordinate/size readout on the overlay, and fire
209
+ the capture on release."""
210
+ if not RegionScreenshot.armed:
211
+ _set_tool_cursor(False) # back to the default after cancel / esc
212
+ return
213
+ if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
214
+ cancel()
215
+ _set_tool_cursor(False)
216
+ return
217
+ _set_tool_cursor(True)
218
+
219
+ io = imgui.get_io()
220
+ full = (0.0, 0.0, float(io.display_size.x), float(io.display_size.y))
221
+ sub = dict(view_id="region_shot", rect=full, priority_delta=_PRIORITY_DELTA)
222
+ down = draw_state.on_action("left_mouse_down", **sub)
223
+ drag = draw_state.on_action("left_mouse_drag", **sub)
224
+ release = draw_state.on_action("left_mouse_drag_release", **sub)
225
+ draw_state.on_action("left_mouse_up", **sub) # claimed so nothing under us sees the click
226
+ draw_state.on_action("left_mouse_click", **sub)
227
+
228
+ mx, my = imgui.get_mouse_pos()
229
+ if down is not None:
230
+ RegionScreenshot.start = (float(down.x), float(down.y))
231
+ if release is not None and RegionScreenshot.start is not None:
232
+ sx, sy = RegionScreenshot.start
233
+ _finish(sx, sy, float(release.x), float(release.y))
234
+ return # nothing painted this frame - the capture reads a clean frame
235
+ if drag is None and RegionScreenshot.start is not None and not imgui.is_mouse_down(0):
236
+ RegionScreenshot.start = None # button went up without the drag activating
237
+
238
+ if RegionScreenshot.start is not None:
239
+ overlay = imgui.get_overlay_draw_list()
240
+ overlay.channels_set_current(Core.melty.max_layer - 1)
241
+ sx, sy = RegionScreenshot.start
242
+ x0, y0, x1, y1 = min(sx, mx), min(sy, my), max(sx, mx), max(sy, my)
243
+ overlay.add_rect_filled(x0, y0, x1, y1, pack_color(*_FILL_COLOR))
244
+ overlay.add_rect(x0, y0, x1, y1, pack_color(*_BOX_COLOR), 0.0, 0, 1.0)
245
+ _draw_box_label(overlay, x0, y0, x1, y1, full[2], full[3])
246
+ # The box's moving edge tracks the cursor: keep frames coming.
247
+ request_render()