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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- 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)
|