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,180 @@
1
+ """The fp16 scene target and the presentation pass.
2
+
3
+ Every frame renders into ONE offscreen target instead of the window's
4
+ default framebuffer: RGBA16F colour (linear scRGB, negatives and values
5
+ above 1 kept — see hdr_color.py) plus a D24S8 renderbuffer (the split
6
+ renderer's per-window stencil masks need it). Everything that used to say
7
+ "framebuffer 0" — the studio's clear, imgui, the tile-cache snapshot, the
8
+ filters, the corner pass — asks `Melty.default_framebuffer()` and gets the
9
+ scene while a frame is open.
10
+
11
+ `present()` is the last GL work before the swap: it binds the real default
12
+ framebuffer and encodes the scene for the swapchain — `Toggles.HDR.output`
13
+ "srgb" for an untagged SDR window, "pq" (BT.2020 + ST 2084) for a surface
14
+ tagged PQ. The swapchain is 8-bit on this driver (NVIDIA egl-wayland offers
15
+ no 10-bit / fp16 window configs), so the encode is where HDR is quantized;
16
+ an fp16 dmabuf swapchain replaces this pass with a copy.
17
+
18
+ Module-level GL state survives hotswap (`globals().get`), like titlebar.py.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import OpenGL.GL as gl
23
+
24
+ from meltygui.core.graphics.gl_state import GLState
25
+ from meltygui.core.graphics.gl_state import GLTexture
26
+ from meltygui.core.graphics.gl_state import is_gl_thread
27
+ from meltygui.hdr_color import GLSL_ENCODE
28
+ from meltygui.core.graphics.shader_func import shader_func
29
+
30
+ _STATE = globals().get("_STATE", {
31
+ "fbo": 0, "tex": None, "rbo": None, "size": (0, 0), "active": False, "gl": None,
32
+ })
33
+
34
+
35
+ def active() -> bool:
36
+ return bool(_STATE["active"])
37
+
38
+
39
+ def framebuffer() -> int:
40
+ """The framebuffer a frame's draws go to: the scene while one is open, else 0."""
41
+ return int(_STATE["fbo"]) if _STATE["active"] else 0
42
+
43
+
44
+ def texture() -> GLTexture | None:
45
+ return _STATE["tex"]
46
+
47
+
48
+ def _release():
49
+ if _STATE["fbo"]:
50
+ gl.glDeleteFramebuffers(1, [int(_STATE["fbo"])])
51
+ if _STATE["rbo"]:
52
+ gl.glDeleteRenderbuffers(1, [int(_STATE["rbo"])])
53
+ if _STATE["tex"] is not None:
54
+ gl.glDeleteTextures([_STATE["tex"].texture_id])
55
+ _STATE.update(fbo=0, tex=None, rbo=None, size=(0, 0))
56
+
57
+
58
+ def _ensure(width: int, height: int) -> None:
59
+ width, height = max(1, int(width)), max(1, int(height))
60
+ if _STATE["fbo"] and _STATE["size"] == (width, height):
61
+ return
62
+ _release()
63
+ tex = int(gl.glGenTextures(1))
64
+ gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
65
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA16F, width, height, 0,
66
+ gl.GL_RGBA, gl.GL_HALF_FLOAT, None)
67
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
68
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
69
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
70
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
71
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
72
+ rbo = int(gl.glGenRenderbuffers(1))
73
+ gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, rbo)
74
+ gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, gl.GL_DEPTH24_STENCIL8, width, height)
75
+ gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, 0)
76
+ fbo = int(gl.glGenFramebuffers(1))
77
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
78
+ gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, tex, 0)
79
+ gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, gl.GL_DEPTH_STENCIL_ATTACHMENT, gl.GL_RENDERBUFFER, rbo)
80
+ status = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER)
81
+ if status != gl.GL_FRAMEBUFFER_COMPLETE:
82
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
83
+ gl.glDeleteFramebuffers(1, [fbo])
84
+ gl.glDeleteRenderbuffers(1, [rbo])
85
+ gl.glDeleteTextures([tex])
86
+ raise RuntimeError(f"scene target incomplete: 0x{status:04X} ({width}x{height})")
87
+ _STATE.update(fbo=fbo, rbo=rbo, size=(width, height),
88
+ tex=GLTexture(tex, gl.GL_TEXTURE_2D, (height, width), gl.GL_RGBA16F))
89
+
90
+
91
+ def begin(width: int, height: int) -> int:
92
+ """Frame start (LSDStudio.render, before the clear): size the scene to
93
+ the REAL framebuffer and bind it. Returns the framebuffer bound."""
94
+ if not is_gl_thread():
95
+ return 0
96
+ _ensure(width, height)
97
+ _STATE["active"] = True
98
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, int(_STATE["fbo"]))
99
+ return int(_STATE["fbo"])
100
+
101
+
102
+ _PRESENT_FRAG = """
103
+ #version 330 core
104
+ uniform sampler2D scene;
105
+ in vec2 uv;
106
+ out vec4 FragColor;
107
+ """ + GLSL_ENCODE + """
108
+ void main() {
109
+ vec4 c = texture(scene, uv);
110
+ // The scene is premultiplied in LINEAR light; the swapchain is
111
+ // premultiplied in ENCODED (electrical) values — what Wayland assumes
112
+ // without a wp_color_representation alpha mode, so the compositor
113
+ // divides the stored value by alpha before decoding. Encoding a*c as
114
+ // it stood read back as pq(a*c)/a: far above pq(c) at the dark end
115
+ // (PQ and sRGB are steepest there), so every translucent pixel — the
116
+ // shadow margin, the rounded corners' anti-aliased edge — came out
117
+ // much brighter than the scene held, the shadow beside the content a
118
+ // bright blue rim over a black desktop (09-09). Un-premultiply, encode
119
+ // the straight colour, multiply back. Opaque pixels are unchanged.
120
+ float a = clamp(c.a, 0.0, 1.0);
121
+ vec3 rgb = a > 0.0 ? c.rgb / a : vec3(0.0);
122
+ vec3 encoded;
123
+ if (pq_output == 1) {
124
+ // Linear scRGB -> BT.2020 -> PQ. reference_nits is what 1.0 shows
125
+ // as (the desktop's SDR reference), so SDR content matches an
126
+ // untagged window and white(4) lands at 4x that.
127
+ // Convert primaries FIRST, clamp after: a P3 / wide colour is
128
+ // scRGB with NEGATIVE components (p3(1,0,0) = (1.22, -0.04, -0.02)
129
+ // linear) and clamping them before the matrix collapses it back to
130
+ // the sRGB gamut. BT.2020 contains P3, so the result is non-negative.
131
+ vec3 nits = max(MELTY_SRGB_TO_BT2020 * rgb, 0.0) * reference_nits;
132
+ encoded = melty_pq_encode(nits);
133
+ } else {
134
+ encoded = melty_linear_to_srgb(rgb);
135
+ }
136
+ FragColor = vec4(encoded * a, a);
137
+ }
138
+ """
139
+
140
+
141
+ @shader_func(fragment=_PRESENT_FRAG)
142
+ def _present_pass(gl_state: GLState = None, scene=None, pq_output=0, reference_nits=250.0, **kwargs):
143
+ gl.glBindVertexArray(gl_state.vao("fs_triangle"))
144
+ gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
145
+
146
+
147
+ def present(width: int, height: int) -> bool:
148
+ """Frame end (Melty.post_frame, after the corner pass): encode the scene
149
+ into the default framebuffer. The scene stays intact for readbacks."""
150
+ if not _STATE["active"] or not is_gl_thread():
151
+ return False
152
+ from meltygui.core.runtime.toggles import Toggles
153
+ _STATE["active"] = False
154
+ if _STATE["gl"] is None:
155
+ _STATE["gl"] = GLState()
156
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
157
+ gl.glViewport(0, 0, int(width), int(height))
158
+ gl.glDisable(gl.GL_SCISSOR_TEST)
159
+ gl.glDisable(gl.GL_DEPTH_TEST)
160
+ gl.glDisable(gl.GL_STENCIL_TEST)
161
+ gl.glDisable(gl.GL_BLEND)
162
+ gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
163
+ # PQ only once the surface actually carries the tag (wayland_color.sync):
164
+ # an untagged app is sRGB to the compositor, whatever the toggle says.
165
+ import meltygui.core.graphics.wayland_color as wayland_color
166
+ pq = wayland_color.resolved_output() == "pq" and (wayland_color.applied() == "pq" or not wayland_color.available())
167
+ # 1.0 = the desktop's SDR white: the reference the applied PQ tag carries
168
+ # (wayland_color.desired_reference - the compositor's preferred
169
+ # description, else Toggles.HDR.pq_reference_nits).
170
+ reference = wayland_color.reference_nits() or wayland_color.desired_reference()
171
+ _present_pass(_STATE["gl"], scene=_STATE["tex"], pq_output=1 if pq else 0,
172
+ reference_nits=float(reference))
173
+ gl.glBindVertexArray(0)
174
+ return True
175
+
176
+
177
+ def shutdown() -> None:
178
+ if is_gl_thread():
179
+ _release()
180
+ _STATE["active"] = False
@@ -0,0 +1,439 @@
1
+ """Per-window screenshots of the live Melty studio.
2
+
3
+ The launcher MCP server runs in the same process as the studio (the studio
4
+ renders on a worker thread), but the GL context is only current on that render
5
+ thread. So capture is a hand-off:
6
+
7
+ * request_capture(name) — called from the MCP thread; enqueues a request,
8
+ wakes the render loop, and blocks until it's fulfilled.
9
+ * process_captures(window) — called once per frame from Melty.post_frame on
10
+ the render thread (just before swap, so GL_BACK holds the finished frame);
11
+ reads the named window's sub-rectangle and writes a PNG.
12
+
13
+ Capturing one window keeps the image small and legible (the real display can be
14
+ an ultra-wide spanning many windows).
15
+ """
16
+
17
+ import threading
18
+ import time
19
+ from pathlib import Path
20
+
21
+ from meltygui.core.runtime.paths import cache_root
22
+ SHOT_DIR = cache_root() / "screenshots"
23
+
24
+ _pending = []
25
+ _lock = threading.Lock()
26
+
27
+
28
+ def _shot_dir():
29
+ """Output directory for screenshots — `Toggles.screenshots` (expanded),
30
+ falling back to the in-repo `.melty/screenshots` if it can't be read."""
31
+ try:
32
+ from meltygui.core.runtime.toggles import Toggles
33
+ configured = Toggles.screenshots
34
+ if configured:
35
+ return Path(configured).expanduser()
36
+ except Exception:
37
+ pass
38
+ return SHOT_DIR
39
+
40
+
41
+ def list_window_names():
42
+ """Names of currently-registered top-level windows (for error messages)."""
43
+ from meltygui.core.melty import Melty
44
+ return [mw.name for mw in Melty.registered_windows.values()
45
+ if getattr(mw, "name", None) and getattr(mw, "draw_state", None) is not None]
46
+
47
+
48
+ # Frames to wait after fronting a window before reading pixels. apply_move_to_front
49
+ # runs in end_frame, and the bumped layer only affects the *next* frame's draw, so
50
+ # a couple of frames must render before the window is actually composited on top.
51
+ _SETTLE_FRAMES = 3
52
+
53
+
54
+ def _submit(req, timeout):
55
+ """Enqueue `req`, wake the render loop, and block until it's fulfilled or
56
+ `timeout` elapses. Returns (path, error); one is None."""
57
+ with _lock:
58
+ _pending.append(req)
59
+ _nudge()
60
+ if not req["event"].wait(timeout):
61
+ with _lock:
62
+ if req in _pending:
63
+ _pending.remove(req)
64
+ return None, (f"timed out after {timeout:.0f}s — is a studio session "
65
+ f"running and rendering? windows: {list_window_names()}")
66
+ return req["path"], req["error"]
67
+
68
+
69
+ def request_capture(name, timeout=8.0):
70
+ """Request a capture of window `name`. Returns (path, error); one is None.
71
+
72
+ Safe to call off the render thread — blocks until the render thread fronts
73
+ the window, lets it composite, captures it, or `timeout` elapses.
74
+ """
75
+ req = {"name": name, "stage": "front", "frames_left": 0,
76
+ "path": None, "error": None, "event": threading.Event()}
77
+ return _submit(req, timeout)
78
+
79
+
80
+ def request_tile_capture(name, timeout=8.0):
81
+ """Like `request_capture`, but reads the window's cached offscreen blit tile
82
+ straight from its FBO instead of the on-screen framebuffer.
83
+
84
+ The tile holds the window's own composited pixels, so the capture is
85
+ immune to whatever else is stacked on top of the window on screen (no
86
+ fronting/settling needed). Falls back to `request_capture`'s
87
+ front -> settle -> framebuffer-read flow if the window has no usable tile.
88
+
89
+ Safe to call off the render thread.
90
+ """
91
+ req = {"name": name, "stage": "tile", "frames_left": 0,
92
+ "path": None, "error": None, "event": threading.Event()}
93
+ return _submit(req, timeout)
94
+
95
+
96
+ def _nudge():
97
+ try:
98
+ from meltygui.core.windowing.glfw_utils import request_render
99
+ request_render() # force the loop to render the next frame
100
+ except Exception:
101
+ pass
102
+
103
+
104
+ def process_captures(window):
105
+ """Advance pending captures one frame. Call from the render thread
106
+ (post_frame), before swap_buffers, with the GL context current.
107
+
108
+ Each request walks: front (raise the window) -> settle (let it composite)
109
+ -> capture. We pump one frame per step via request_render.
110
+ """
111
+ with _lock:
112
+ if not _pending:
113
+ return
114
+ reqs = _pending[:]
115
+ _pending.clear()
116
+
117
+ from meltygui.core.melty import Melty
118
+ unfinished = []
119
+ for req in reqs:
120
+ try:
121
+ if req["stage"] == "tile":
122
+ path = _capture_tile(req["name"])
123
+ if path is not None:
124
+ req["path"] = path
125
+ req["event"].set()
126
+ else:
127
+ # No cached tile for this window -> fall back to the on-screen
128
+ # front -> settle -> framebuffer-read flow.
129
+ req["stage"] = "front"
130
+ unfinished.append(req)
131
+ continue
132
+ if req["stage"] == "front":
133
+ mw = _find_window(req["name"])
134
+ if mw is None:
135
+ req["error"] = (f"no window named {req['name']!r}; "
136
+ f"available: {list_window_names()}")
137
+ req["event"].set()
138
+ continue
139
+ # A closed window early-returns from its render (draw None),
140
+ # so open it. Clear both the draw_state flag and the stored
141
+ # window_args, since the renderer re-applies kwargs["closed"]
142
+ # each frame.
143
+ mw.draw_state.closed = False
144
+ if isinstance(mw.window_args, dict):
145
+ mw.window_args["closed"] = False
146
+ Melty.move_window_to_front(mw.draw_state)
147
+ req["stage"] = "settle"
148
+ req["frames_left"] = _SETTLE_FRAMES
149
+ unfinished.append(req)
150
+ elif req["stage"] == "settle":
151
+ req["frames_left"] -= 1
152
+ if req["frames_left"] <= 0:
153
+ req["stage"] = "capture"
154
+ unfinished.append(req)
155
+ elif req["stage"] == "capture":
156
+ req["path"] = _capture(window, req["name"])
157
+ req["event"].set()
158
+ except Exception as e:
159
+ req["error"] = str(e)
160
+ req["event"].set()
161
+
162
+ if unfinished:
163
+ with _lock:
164
+ _pending.extend(unfinished)
165
+ _nudge() # keep frames coming while staged requests advance
166
+
167
+
168
+ _view_pending = []
169
+
170
+
171
+ def request_view_capture(draw_state, requested_frame, reopen_menu_ds=None,
172
+ on_captured=None):
173
+ """Queue a deferred framebuffer capture of a single VIEW (any draw_state's
174
+ on-screen rect), not a whole window. Serviced by process_take_screenshot_flags
175
+ after `_SETTLE_FRAMES` -- enough frames for the window front-move to composite
176
+ and the closed context menu to clear from the framebuffer.
177
+
178
+ `requested_frame` is Melty.frame_count at request time. `reopen_menu_ds`, if
179
+ given, is the context menu's draw_state to reopen once the shot lands.
180
+ `on_captured`, if given, is called with the saved PNG path on the render
181
+ thread once the shot lands (not called if the capture fails).
182
+ Call on the render thread (from the menu's render).
183
+ """
184
+ _view_pending.append({
185
+ "draw_state": draw_state,
186
+ "requested_frame": requested_frame,
187
+ "reopen_menu_ds": reopen_menu_ds,
188
+ "on_captured": on_captured,
189
+ })
190
+ _nudge()
191
+
192
+
193
+ _region_pending = []
194
+
195
+
196
+ def request_region_capture(left, top, w, h, requested_frame, name="screenshot",
197
+ on_captured=None):
198
+ """Queue a deferred framebuffer capture of an arbitrary on-screen box
199
+ (window points) — the region screenshot tool's drag rectangle. Serviced
200
+ by process_take_screenshot_flags after `_SETTLE_FRAMES`, so the frame the
201
+ box was drawn on (crosshair / selection overlay) has cleared before the
202
+ pixels are read. `on_captured(path)` runs on the render thread once the
203
+ PNG is written. Call on the render thread."""
204
+ _region_pending.append({
205
+ "rect": (left, top, w, h), "requested_frame": requested_frame,
206
+ "name": name, "on_captured": on_captured,
207
+ })
208
+ _nudge()
209
+
210
+
211
+ def _process_region_captures(window):
212
+ if not _region_pending:
213
+ return
214
+ from meltygui.core.melty import Melty
215
+ still = []
216
+ for req in _region_pending:
217
+ if Melty.frame_count - int(req["requested_frame"]) < _SETTLE_FRAMES:
218
+ still.append(req)
219
+ continue
220
+ path = None
221
+ try:
222
+ path = _capture_points(window, *req["rect"], req["name"])
223
+ except Exception as e:
224
+ print(f"Screenshot (region) failed: {e}")
225
+ if path is not None and req.get("on_captured") is not None:
226
+ try:
227
+ req["on_captured"](path)
228
+ except Exception as e:
229
+ print(f"Screenshot on_captured callback failed: {e}")
230
+ _region_pending[:] = still
231
+ if still:
232
+ _nudge()
233
+
234
+
235
+ def process_take_screenshot_flags(window):
236
+ """Service deferred per-view screenshots queued from the context menu (and
237
+ the region tool's drag boxes — _process_region_captures). Call from the
238
+ render thread (post_frame), with the GL context current.
239
+
240
+ The context-menu screenshot button fronts the window, queues the view via
241
+ request_view_capture, and closes the menu -- all during one frame's draw. We
242
+ then wait `_SETTLE_FRAMES` so the front-move composites the window on top and
243
+ the closed menu has cleared, then grab the view's rect and reopen the menu.
244
+ Pumps frames while waiting.
245
+ """
246
+ _process_region_captures(window)
247
+ if not _view_pending:
248
+ return
249
+ from meltygui.core.melty import Melty
250
+ still = []
251
+ for req in _view_pending:
252
+ if Melty.frame_count - int(req["requested_frame"]) < _SETTLE_FRAMES:
253
+ still.append(req) # not settled yet
254
+ continue
255
+ ds = req["draw_state"]
256
+ path = None
257
+ try:
258
+ path = _capture_view(window, ds)
259
+ except Exception as e:
260
+ name = getattr(ds, "name", None) or "view"
261
+ print(f"Screenshot (view) failed for {name!r}: {e}")
262
+ if path is not None and req.get("on_captured") is not None:
263
+ try:
264
+ req["on_captured"](path)
265
+ except Exception as e:
266
+ print(f"Screenshot on_captured callback failed: {e}")
267
+ _reopen_context_menu_ds(req.get("reopen_menu_ds"))
268
+ _view_pending[:] = still
269
+ if still:
270
+ _nudge() # keep frames coming until the window is fronted & menu gone
271
+
272
+
273
+ def _reopen_context_menu_ds(menu_ds):
274
+ """Reopen a context menu after its screenshot lands. The menu set `_reopen`
275
+ on its own draw_state. Reopening means flipping the owning view's
276
+ `context_menu_open` back on (its source of truth) and clearing the menu's
277
+ `closed`, then re-rendering the owner."""
278
+ if menu_ds is None or not getattr(menu_ds, "_reopen", False):
279
+ return
280
+ menu_ds._reopen = False
281
+ menu_ds.closed = False
282
+ owner = getattr(menu_ds, "_raw_input_value", None) # the view that owns the menu
283
+ if owner is not None:
284
+ owner.context_menu_open = True
285
+ try:
286
+ owner.invalidate_up(max_depth=5)
287
+ except Exception:
288
+ pass
289
+ _nudge()
290
+
291
+
292
+ def _find_window(name):
293
+ from meltygui.core.melty import Melty
294
+ wins = [mw for mw in Melty.registered_windows.values()
295
+ if getattr(mw, "name", None) and getattr(mw, "draw_state", None) is not None]
296
+ # Prefer an exact name; fall back to case-insensitive substring. In both
297
+ # cases the last match is the most-recently-fronted window.
298
+ exact = [mw for mw in wins if mw.name == name]
299
+ if exact:
300
+ return exact[-1]
301
+ sub = [mw for mw in wins if name.lower() in mw.name.lower()]
302
+ if sub:
303
+ return sub[-1]
304
+ return None
305
+
306
+
307
+ def _capture(window, name):
308
+ """Grab a whole window's on-screen rect to a PNG (the MCP/window path)."""
309
+ mw = _find_window(name)
310
+ if mw is None:
311
+ raise ValueError(f"no window named {name!r}; available: {list_window_names()}")
312
+ return _capture_rect(window, mw.draw_state, mw.name)
313
+
314
+
315
+ def _capture_view(window, draw_state):
316
+ """Grab a single VIEW's on-screen rect to a PNG. Same framebuffer read as
317
+ `_capture`, but bounded to the given draw_state's rect rather than a whole
318
+ top-level window -- so the context menu can shoot just the view it's for."""
319
+ name = getattr(draw_state, "name", None) or "view"
320
+ return _capture_rect(window, draw_state, name)
321
+
322
+
323
+ def _capture_rect(window, ds, name):
324
+ """Read draw_state `ds`'s on-screen rect from GL_BACK and save it as a PNG
325
+ named after `name`. The draw_state supplies the rect (left/top/width/height
326
+ in window points); shared by the window and per-view capture paths."""
327
+ left, top, w, h = ds.left, ds.top, ds.width, ds.height
328
+ if not w or not h:
329
+ raise ValueError(f"{name!r} has no size ({w}x{h}); is it open?")
330
+ return _capture_points(window, left, top, w, h, name)
331
+
332
+
333
+ def _capture_points(window, left, top, w, h, name):
334
+ """Read the on-screen rect (left/top/width/height in WINDOW POINTS) from
335
+ GL_BACK and save it as a PNG named after `name`. The one framebuffer
336
+ reader: window / view captures pass a draw_state's box, the region tool
337
+ passes the user's drag box."""
338
+ import meltygui.core.windowing.window_api as glfw
339
+ import numpy as np
340
+ import OpenGL.GL as gl
341
+ from PIL import Image
342
+
343
+ from meltygui.core.melty import Melty
344
+
345
+ fb_w, fb_h = glfw.get_framebuffer_size(window)
346
+ win_w, win_h = glfw.get_window_size(window)
347
+ scale = (fb_w / win_w) if win_w else 1.0
348
+ # Window points are CONTENT coordinates: in the transparent frameless
349
+ # window the content sits inset by the shadow margin inside the
350
+ # framebuffer (Melty.frame_origin, framebuffer px - the same offset the
351
+ # masks / tile captures apply), so a box at the top of the screen read
352
+ # the transparent margin above the content without this.
353
+ origin_x, origin_y = (int(v) for v in (Melty.frame_origin or (0, 0)))
354
+
355
+ # Rect in framebuffer pixels, clamped to the framebuffer.
356
+ x0 = max(0, min(int(round(left * scale)) + origin_x, fb_w))
357
+ y_top = max(0, min(int(round(top * scale)) + origin_y, fb_h))
358
+ pw = max(1, min(int(round(w * scale)), fb_w - x0))
359
+ ph = max(1, min(int(round(h * scale)), fb_h - y_top))
360
+ y_bottom = fb_h - (y_top + ph) # GL origin is bottom-left
361
+
362
+ gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
363
+ gl.glReadBuffer(gl.GL_BACK)
364
+ data = gl.glReadPixels(x0, y_bottom, pw, ph, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
365
+
366
+ if isinstance(data, bytes):
367
+ arr = np.frombuffer(data, dtype=np.uint8)
368
+ else:
369
+ arr = np.asarray(data, dtype=np.uint8).ravel()
370
+ arr = arr.reshape(ph, pw, 4)
371
+ arr = np.flipud(arr) # bottom-left -> top-left origin
372
+
373
+ path = _shot_path(name)
374
+ Image.fromarray(arr, "RGBA").save(path)
375
+ return str(path)
376
+
377
+
378
+ def _shot_path(window_name):
379
+ """A fresh timestamped PNG path under the configured shot dir (created)."""
380
+ shot_dir = _shot_dir()
381
+ shot_dir.mkdir(parents=True, exist_ok=True)
382
+ safe = "".join(c if c.isalnum() else "_" for c in window_name)[:40] or "window"
383
+ return shot_dir / f"{safe}_{int(time.time())}.png"
384
+
385
+
386
+ def _capture_tile(name):
387
+ """Read window `name`'s cached offscreen blit tile straight from its FBO and
388
+ save a PNG. Returns the path, or None if the window has no usable tile (the
389
+ caller then falls back to the framebuffer capture).
390
+
391
+ Reading the tile sidesteps window-overlay artifacts: the tile holds the
392
+ window's own composited pixels regardless of what's stacked on top of it on
393
+ screen. Must run on the render thread with the GL context current.
394
+ """
395
+ import numpy as np
396
+ import OpenGL.GL as gl
397
+ from PIL import Image
398
+ from meltygui.core.melty import Melty
399
+
400
+ mw = _find_window(name)
401
+ if mw is None:
402
+ return None
403
+ ds = getattr(mw, "draw_state", None)
404
+ cache = getattr(Melty, "cache", None)
405
+ tile_id = getattr(ds, "_tile_id", None) if ds is not None else None
406
+ tiles = getattr(cache, "_tiles", None) if cache is not None else None
407
+ if tile_id is None or tiles is None:
408
+ return None
409
+ tile = tiles.get(tile_id)
410
+ if tile is None:
411
+ return None
412
+ w, h = tile.size
413
+ if not w or not h or tile.fbo in (None, -1):
414
+ return None
415
+
416
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, tile.fbo)
417
+ gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0)
418
+ gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
419
+ # Content is top-anchored in a possibly bottom-padded texture: the logical
420
+ # w x h pixels live in the texture rows [alloc_h - h, alloc_h), not at y=0.
421
+ alloc_h = (getattr(tile, "alloc_size", None) or tile.size)[1]
422
+ # Tiles hold LINEAR scRGB (fp16, hdr_color.py): read floats and sRGB-encode
423
+ # for the PNG the way the presentation pass does for the screen.
424
+ data = gl.glReadPixels(0, int(alloc_h) - int(h), int(w), int(h), gl.GL_RGBA, gl.GL_FLOAT)
425
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, Melty.default_framebuffer()) # restore main frame's target
426
+
427
+ lin = np.asarray(data, dtype=np.float32).reshape(-1, 4)
428
+ rgb = np.clip(lin[:, :3], 0.0, 1.0)
429
+ enc = np.where(rgb <= 0.0031308, rgb * 12.92, 1.055 * np.power(rgb, 1.0 / 2.4) - 0.055)
430
+ arr = np.empty((lin.shape[0], 4), dtype=np.uint8)
431
+ arr[:, :3] = np.round(enc * 255.0)
432
+ arr[:, 3] = np.round(np.clip(lin[:, 3], 0.0, 1.0) * 255.0)
433
+ arr = arr.ravel()
434
+ arr = arr.reshape(int(h), int(w), 4)
435
+ arr = np.flipud(arr) # GL origin is bottom-left -> top-left
436
+
437
+ path = _shot_path(mw.name)
438
+ Image.fromarray(arr, "RGBA").save(path)
439
+ return str(path)