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,1560 @@
|
|
|
1
|
+
"""Custom application titlebar (Toggles.Melty.enhanced_titlebar).
|
|
2
|
+
|
|
3
|
+
Removes the server-side decoration so the UI extends to the top of the
|
|
4
|
+
display, draws the window controls as overlay-drawlist widgets in the top
|
|
5
|
+
corners — WHICH of minimize / maximize / close, and on which side, follows
|
|
6
|
+
the desktop's own title-bar button setting (titlebar_buttons.py, or
|
|
7
|
+
Toggles.Melty.titlebar_button_layout; an OS window whose root meltygui window
|
|
8
|
+
has a header hosts them inside that header, draw_header_controls) — and
|
|
9
|
+
hands drag / edge-resize back to the window manager via
|
|
10
|
+
_NET_WM_MOVERESIZE — so snapping, tiling and drag smoothness stay native.
|
|
11
|
+
|
|
12
|
+
Backend support. X11 (and XWayland): everything above. Native Wayland:
|
|
13
|
+
GLFW has no public route to xdg_toplevel.move()/resize(), so the OS window
|
|
14
|
+
KEEPS a frame there — GLFW's own fallback frame (a caption strip + borders,
|
|
15
|
+
compositor-driven move/resize) once Toggles.Melty.wayland_native_frame has
|
|
16
|
+
switched libdecor off at the first glfw.init (glfw_utils
|
|
17
|
+
.apply_wayland_frame_hint) — or, with Toggles.Melty.wayland_show_frame off
|
|
18
|
+
(the default), no frame at all. This module then contributes the buttons
|
|
19
|
+
(always on Wayland; Toggles.Melty.enhanced_titlebar is an X11 knob), the
|
|
20
|
+
right-drag resize (a cursor-driven drag of the OS window's own frame edges
|
|
21
|
+
through the edge physics, gl_gui/os_frame.py — bottom-right corner, or the
|
|
22
|
+
top-left on a DOUBLE right-drag, the meltygui windows' rule) and, through
|
|
23
|
+
gl_gui/wayland_move.py, the SAME strip / drag-anywhere move and edge resize
|
|
24
|
+
as X11: xdg_toplevel.move / .resize sent straight to the compositor (what
|
|
25
|
+
Super+drag and the caption strip do), the grab then driven by GNOME. With libdecor still on, its own
|
|
26
|
+
title bar carries the buttons and nothing here draws (backend_supported
|
|
27
|
+
is False).
|
|
28
|
+
|
|
29
|
+
Everything here runs on the visualization thread inside the imgui frame
|
|
30
|
+
(called from LSDStudio.render). The decoration attribute is synced live
|
|
31
|
+
each frame, so flipping the toggle takes effect without a restart.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import ctypes
|
|
35
|
+
import sys
|
|
36
|
+
import time
|
|
37
|
+
|
|
38
|
+
import meltygui.core.windowing.window_api as glfw
|
|
39
|
+
import meltygui_imgui as imgui
|
|
40
|
+
import OpenGL.GL as gl
|
|
41
|
+
|
|
42
|
+
import meltygui.core.input.mouse_cursor as mouse_cursor
|
|
43
|
+
import meltygui.core.windowing.titlebar_buttons as titlebar_buttons
|
|
44
|
+
import meltygui.core.windowing.wayland_move as wayland_move
|
|
45
|
+
import meltygui.core.input.hypr_left_drag as hypr_left_drag
|
|
46
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
47
|
+
from meltygui.core.graphics.gl_state import is_gl_thread
|
|
48
|
+
from meltygui.core.graphics.shader_func import shader_func
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# X11 backend: _NET_WM_MOVERESIZE via ctypes → libX11
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
# direction values from the EWMH spec
|
|
55
|
+
_SIZE_TOPLEFT = 0
|
|
56
|
+
_SIZE_TOP = 1
|
|
57
|
+
_SIZE_TOPRIGHT = 2
|
|
58
|
+
_SIZE_RIGHT = 3
|
|
59
|
+
_SIZE_BOTTOMRIGHT = 4
|
|
60
|
+
_SIZE_BOTTOM = 5
|
|
61
|
+
_SIZE_BOTTOMLEFT = 6
|
|
62
|
+
_SIZE_LEFT = 7
|
|
63
|
+
_MOVE = 8
|
|
64
|
+
|
|
65
|
+
_CLIENT_MESSAGE = 33
|
|
66
|
+
_BUTTON_RELEASE = 5
|
|
67
|
+
_BUTTON_RELEASE_MASK = 1 << 3
|
|
68
|
+
_SUBSTRUCTURE_MASK = (1 << 19) | (1 << 20) # SubstructureNotify | SubstructureRedirect
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _XClientMessageEvent(ctypes.Structure):
|
|
72
|
+
_fields_ = [
|
|
73
|
+
("type", ctypes.c_int),
|
|
74
|
+
("serial", ctypes.c_ulong),
|
|
75
|
+
("send_event", ctypes.c_int),
|
|
76
|
+
("display", ctypes.c_void_p),
|
|
77
|
+
("window", ctypes.c_ulong),
|
|
78
|
+
("message_type", ctypes.c_ulong),
|
|
79
|
+
("format", ctypes.c_int),
|
|
80
|
+
("data", ctypes.c_long * 5),
|
|
81
|
+
("_pad", ctypes.c_char * 96), # pad out to XEvent union size
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _XButtonEvent(ctypes.Structure):
|
|
86
|
+
_fields_ = [
|
|
87
|
+
("type", ctypes.c_int),
|
|
88
|
+
("serial", ctypes.c_ulong),
|
|
89
|
+
("send_event", ctypes.c_int),
|
|
90
|
+
("display", ctypes.c_void_p),
|
|
91
|
+
("window", ctypes.c_ulong),
|
|
92
|
+
("root", ctypes.c_ulong),
|
|
93
|
+
("subwindow", ctypes.c_ulong),
|
|
94
|
+
("time", ctypes.c_ulong),
|
|
95
|
+
("x", ctypes.c_int),
|
|
96
|
+
("y", ctypes.c_int),
|
|
97
|
+
("x_root", ctypes.c_int),
|
|
98
|
+
("y_root", ctypes.c_int),
|
|
99
|
+
("state", ctypes.c_uint),
|
|
100
|
+
("button", ctypes.c_uint),
|
|
101
|
+
("same_screen", ctypes.c_int),
|
|
102
|
+
("_pad", ctypes.c_char * 96),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_x11 = None
|
|
107
|
+
_moveresize_atom = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _lib():
|
|
111
|
+
global _x11
|
|
112
|
+
if _x11 is None:
|
|
113
|
+
_x11 = ctypes.CDLL("libX11.so.6")
|
|
114
|
+
_x11.XInternAtom.restype = ctypes.c_ulong
|
|
115
|
+
_x11.XInternAtom.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
|
116
|
+
_x11.XUngrabPointer.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
|
117
|
+
_x11.XSendEvent.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int,
|
|
118
|
+
ctypes.c_long, ctypes.c_void_p]
|
|
119
|
+
_x11.XFlush.argtypes = [ctypes.c_void_p]
|
|
120
|
+
_x11.XDefaultRootWindow.restype = ctypes.c_ulong
|
|
121
|
+
_x11.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
|
122
|
+
_x11.XQueryPointer.argtypes = [ctypes.c_void_p, ctypes.c_ulong] + \
|
|
123
|
+
[ctypes.c_void_p] * 7
|
|
124
|
+
return _x11
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _handles(window):
|
|
128
|
+
return glfw.get_x11_display(), glfw.get_x11_window(window)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _root_pointer(x11, dpy, root):
|
|
132
|
+
r = ctypes.c_ulong()
|
|
133
|
+
child = ctypes.c_ulong()
|
|
134
|
+
rx, ry, wx, wy = (ctypes.c_int() for _ in range(4))
|
|
135
|
+
mask = ctypes.c_uint()
|
|
136
|
+
x11.XQueryPointer(dpy, root, ctypes.byref(r), ctypes.byref(child),
|
|
137
|
+
ctypes.byref(rx), ctypes.byref(ry),
|
|
138
|
+
ctypes.byref(wx), ctypes.byref(wy), ctypes.byref(mask))
|
|
139
|
+
return rx.value, ry.value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _synthesize_button_release(x11, dpy, win, root, button=1):
|
|
143
|
+
# The WM's grab swallows the real ButtonRelease, so GLFW (and imgui's
|
|
144
|
+
# process_inputs) would think the button stayed down until the next real
|
|
145
|
+
# click. A release sent only to our own window fixes the client-side
|
|
146
|
+
# state without touching the server's physical button state the WM is
|
|
147
|
+
# tracking for the drag.
|
|
148
|
+
ev = _XButtonEvent()
|
|
149
|
+
ev.type = _BUTTON_RELEASE
|
|
150
|
+
ev.display = dpy
|
|
151
|
+
ev.window = win
|
|
152
|
+
ev.root = root
|
|
153
|
+
ev.time = 0 # CurrentTime
|
|
154
|
+
ev.x_root, ev.y_root = _root_pointer(x11, dpy, root)
|
|
155
|
+
ev.state = 1 << (7 + button) # Button<N>Mask (Button1Mask = 1<<8)
|
|
156
|
+
ev.button = button
|
|
157
|
+
ev.same_screen = 1
|
|
158
|
+
x11.XSendEvent(dpy, win, True, _BUTTON_RELEASE_MASK, ctypes.byref(ev))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _begin_moveresize(window, direction, button=1):
|
|
162
|
+
"""Hand the in-progress button press to the WM as a move/resize."""
|
|
163
|
+
global _moveresize_atom
|
|
164
|
+
try:
|
|
165
|
+
x11 = _lib()
|
|
166
|
+
dpy, win = _handles(window)
|
|
167
|
+
if not dpy or not win:
|
|
168
|
+
return False
|
|
169
|
+
root = x11.XDefaultRootWindow(dpy)
|
|
170
|
+
if _moveresize_atom is None:
|
|
171
|
+
_moveresize_atom = x11.XInternAtom(dpy, b"_NET_WM_MOVERESIZE", 0)
|
|
172
|
+
rx, ry = _root_pointer(x11, dpy, root)
|
|
173
|
+
|
|
174
|
+
x11.XUngrabPointer(dpy, 0) # release the implicit press grab so the WM can take it
|
|
175
|
+
ev = _XClientMessageEvent()
|
|
176
|
+
ev.type = _CLIENT_MESSAGE
|
|
177
|
+
ev.display = dpy
|
|
178
|
+
ev.window = win
|
|
179
|
+
ev.message_type = _moveresize_atom
|
|
180
|
+
ev.format = 32
|
|
181
|
+
ev.data[0] = rx
|
|
182
|
+
ev.data[1] = ry
|
|
183
|
+
ev.data[2] = direction
|
|
184
|
+
ev.data[3] = button
|
|
185
|
+
ev.data[4] = 1 # source = normal application
|
|
186
|
+
x11.XSendEvent(dpy, root, False, _SUBSTRUCTURE_MASK, ctypes.byref(ev))
|
|
187
|
+
_synthesize_button_release(x11, dpy, win, root, button)
|
|
188
|
+
x11.XFlush(dpy)
|
|
189
|
+
return True
|
|
190
|
+
except Exception:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Widget: window controls + drag strip + edge resize
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
_BTN_H = 30.0 # top_inset: chrome height to keep clear (buttons ≈ this)
|
|
199
|
+
_pressed_button = None # index armed by a press, fires on release inside
|
|
200
|
+
|
|
201
|
+
# The drag strip participates in the normal input-handler pipeline as the
|
|
202
|
+
# WORST-priority drag subscriber: on mouse-down the handler gives the drag
|
|
203
|
+
# to the best-priority hovered subscriber, so any view that actually uses
|
|
204
|
+
# click-and-drag (window headers, sliders, dnd, ...) captures it and the
|
|
205
|
+
# strip only receives the drag when nothing else under the cursor wants one.
|
|
206
|
+
_STRIP_ID = "enhanced_titlebar_strip"
|
|
207
|
+
_RESIZE_ID = "enhanced_titlebar_rdrag_resize"
|
|
208
|
+
_STRIP_PRIORITY = 10 ** 9
|
|
209
|
+
_wm_move_started = False # latch: dragged is per-frame, the WM move starts once
|
|
210
|
+
|
|
211
|
+
# Right-drag resize gesture: {"top_left": bool, "x": last total_dx, "y":
|
|
212
|
+
# last total_dy}, latched on the first dragged frame and dropped on release.
|
|
213
|
+
_rdrag = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def top_inset():
|
|
217
|
+
"""px of top-edge chrome the app should keep clear (the window-control
|
|
218
|
+
row) — 0 when the custom titlebar is off. For overlays that would
|
|
219
|
+
otherwise collide with the buttons (e.g. the GPU readout)."""
|
|
220
|
+
return _BTN_H if titlebar_enabled() else 0.0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _on_wayland():
|
|
224
|
+
try:
|
|
225
|
+
return glfw.get_platform() == glfw.PLATFORM_WAYLAND
|
|
226
|
+
except AttributeError:
|
|
227
|
+
return False # pre-3.4 glfw on linux = X11
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def backend_supported():
|
|
231
|
+
"""X11 always; native Wayland only once libdecor is out of the picture
|
|
232
|
+
(the fallback frame has no buttons — ours fill in), never beside
|
|
233
|
+
libdecor's own title bar. Never off Linux: the move / resize / frame
|
|
234
|
+
plumbing behind it is Xlib and Wayland."""
|
|
235
|
+
if _on_wayland():
|
|
236
|
+
from meltygui.core.windowing.glfw_utils import wayland_native_frame_active
|
|
237
|
+
return wayland_native_frame_active()
|
|
238
|
+
return sys.platform.startswith("linux")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def titlebar_enabled():
|
|
242
|
+
"""X11: Toggles.Melty.enhanced_titlebar (it replaces the WM frame, an
|
|
243
|
+
opt-in). Wayland: whenever the native frame is up — GLFW's fallback frame
|
|
244
|
+
carries no buttons, so ours are the only min/max/close the window gets,
|
|
245
|
+
and the toggle is not consulted."""
|
|
246
|
+
if _on_wayland():
|
|
247
|
+
return backend_supported()
|
|
248
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
249
|
+
return Toggles.Melty.enhanced_titlebar and backend_supported()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def wants_os_decoration():
|
|
253
|
+
"""DECORATED for the OS window. Wayland: with libdecor still up, always
|
|
254
|
+
(its frame is the window's chrome); on the native frame,
|
|
255
|
+
Toggles.Melty.wayland_show_frame — off = frameless, resize by right-drag,
|
|
256
|
+
move by the compositor. X11: only while the enhanced titlebar is off (it
|
|
257
|
+
replaces the WM's frame with _NET_WM_MOVERESIZE gestures). Boot hint and
|
|
258
|
+
per-frame sync both read this."""
|
|
259
|
+
if _on_wayland():
|
|
260
|
+
if not backend_supported():
|
|
261
|
+
return True
|
|
262
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
263
|
+
return bool(Toggles.Melty.wayland_show_frame)
|
|
264
|
+
return not titlebar_enabled()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def sync_decoration(window):
|
|
268
|
+
"""Apply the toggle live: called every frame on the viz thread."""
|
|
269
|
+
want_bar = titlebar_enabled()
|
|
270
|
+
decorated = bool(glfw.get_window_attrib(window, glfw.DECORATED))
|
|
271
|
+
want_decorated = wants_os_decoration()
|
|
272
|
+
if decorated != want_decorated:
|
|
273
|
+
glfw.set_window_attrib(window, glfw.DECORATED,
|
|
274
|
+
glfw.TRUE if want_decorated else glfw.FALSE)
|
|
275
|
+
return want_bar
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _edge_at(mx, my, w, h, border, corner):
|
|
279
|
+
"""EWMH resize direction for a pointer at (mx, my), or None. An
|
|
280
|
+
off-window pointer — the backend's (-1, -1) / -FLT_MAX sentinel — is
|
|
281
|
+
never on an edge (it used to read as the top-left corner)."""
|
|
282
|
+
if mx < 0 or my < 0:
|
|
283
|
+
return None
|
|
284
|
+
on_l, on_r = mx <= border, mx >= w - border
|
|
285
|
+
on_t, on_b = my <= border, my >= h - border
|
|
286
|
+
near_l, near_r = mx <= corner, mx >= w - corner
|
|
287
|
+
near_t, near_b = my <= corner, my >= h - corner
|
|
288
|
+
if (on_t and near_l) or (on_l and near_t):
|
|
289
|
+
return _SIZE_TOPLEFT
|
|
290
|
+
if (on_t and near_r) or (on_r and near_t):
|
|
291
|
+
return _SIZE_TOPRIGHT
|
|
292
|
+
if (on_b and near_l) or (on_l and near_b):
|
|
293
|
+
return _SIZE_BOTTOMLEFT
|
|
294
|
+
if (on_b and near_r) or (on_r and near_b):
|
|
295
|
+
return _SIZE_BOTTOMRIGHT
|
|
296
|
+
if on_t:
|
|
297
|
+
return _SIZE_TOP
|
|
298
|
+
if on_b:
|
|
299
|
+
return _SIZE_BOTTOM
|
|
300
|
+
if on_l:
|
|
301
|
+
return _SIZE_LEFT
|
|
302
|
+
if on_r:
|
|
303
|
+
return _SIZE_RIGHT
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# One directional shape per edge/corner (right edge |>, left edge <|, each
|
|
308
|
+
# corner its own), not the shared directional arrows. Pushed via
|
|
309
|
+
# mouse_cursor.request - these shapes are past imgui's control.
|
|
310
|
+
_EDGE_CURSOR = {
|
|
311
|
+
_SIZE_TOP: mouse_cursor.RESIZE_N, _SIZE_BOTTOM: mouse_cursor.RESIZE_S,
|
|
312
|
+
_SIZE_LEFT: mouse_cursor.RESIZE_W, _SIZE_RIGHT: mouse_cursor.RESIZE_E,
|
|
313
|
+
_SIZE_TOPLEFT: mouse_cursor.RESIZE_NW, _SIZE_BOTTOMRIGHT: mouse_cursor.RESIZE_SE,
|
|
314
|
+
_SIZE_TOPRIGHT: mouse_cursor.RESIZE_NE, _SIZE_BOTTOMLEFT: mouse_cursor.RESIZE_SW,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# EWMH direction → xdg_toplevel.resize_edge, for the Wayland edge zones.
|
|
319
|
+
_XDG_EDGE = {
|
|
320
|
+
_SIZE_TOP: wayland_move.EDGE_TOP, _SIZE_BOTTOM: wayland_move.EDGE_BOTTOM,
|
|
321
|
+
_SIZE_LEFT: wayland_move.EDGE_LEFT, _SIZE_RIGHT: wayland_move.EDGE_RIGHT,
|
|
322
|
+
_SIZE_TOPLEFT: wayland_move.EDGE_TOP_LEFT, _SIZE_TOPRIGHT: wayland_move.EDGE_TOP_RIGHT,
|
|
323
|
+
_SIZE_BOTTOMLEFT: wayland_move.EDGE_BOTTOM_LEFT, _SIZE_BOTTOMRIGHT: wayland_move.EDGE_BOTTOM_RIGHT,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _wm_gestures_available():
|
|
328
|
+
"""Can the strip / edge zones hand a gesture to the window manager?
|
|
329
|
+
X11: always (_NET_WM_MOVERESIZE). Wayland: once wayland_move found the
|
|
330
|
+
window's xdg_toplevel (LSDStudio attaches it after the input backend)."""
|
|
331
|
+
return wayland_move.available() if _on_wayland() else True
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _release_after_wayland_grab(window):
|
|
335
|
+
"""The compositor's grab swallows the button release. X11 gets a real
|
|
336
|
+
synthesized X event; on Wayland GLFW's own state can't be poked, so the
|
|
337
|
+
input handler is fed the release here and the polls read the button as
|
|
338
|
+
up through wayland_move.button_masked until GLFW's next real event."""
|
|
339
|
+
from meltygui.core.melty import Melty
|
|
340
|
+
backend = getattr(Melty, "backend", None)
|
|
341
|
+
if backend is None or not hasattr(backend, "_on_button"):
|
|
342
|
+
return
|
|
343
|
+
for button in wayland_move.masked_buttons():
|
|
344
|
+
backend._on_button(window, button, glfw.RELEASE, 0)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _begin_wm_move(window):
|
|
348
|
+
if _on_wayland():
|
|
349
|
+
if wayland_move.begin_move(window):
|
|
350
|
+
_release_after_wayland_grab(window)
|
|
351
|
+
return True
|
|
352
|
+
return False
|
|
353
|
+
return _begin_moveresize(window, _MOVE)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _begin_wm_resize(window, direction):
|
|
357
|
+
if _on_wayland():
|
|
358
|
+
if wayland_move.begin_resize(window, _XDG_EDGE[direction]):
|
|
359
|
+
_release_after_wayland_grab(window)
|
|
360
|
+
return True
|
|
361
|
+
return False
|
|
362
|
+
return _begin_moveresize(window, direction)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _toggle_maximize(window):
|
|
366
|
+
if _maximized(window):
|
|
367
|
+
glfw.restore_window(window)
|
|
368
|
+
else:
|
|
369
|
+
glfw.maximize_window(window)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _workarea_for(window):
|
|
373
|
+
"""(left, top, right, bottom) of the workarea of the monitor holding the
|
|
374
|
+
window's center — the resize bounds. Falls back to the primary monitor
|
|
375
|
+
when the center sits off every monitor (mid-drag between screens)."""
|
|
376
|
+
if _on_wayland():
|
|
377
|
+
# No window positions on Wayland: the bounds are the primary
|
|
378
|
+
# monitor's workarea SIZE anchored at the window's own top-left
|
|
379
|
+
# (which the compositor pins) - a cap on how far a resize may grow.
|
|
380
|
+
# The SURFACE may overhang the workarea by the shadow margin on
|
|
381
|
+
# every side (the window client is the content), so the cap is
|
|
382
|
+
# the workarea plus twice the margin.
|
|
383
|
+
_ax, _ay, aw, ah = glfw.get_monitor_workarea(glfw.get_primary_monitor())
|
|
384
|
+
ox, oy = content_origin()
|
|
385
|
+
inset = window_inset()
|
|
386
|
+
return 0.0, 0.0, float(aw + ox + inset), float(ah + oy + inset)
|
|
387
|
+
wx, wy = glfw.get_window_pos(window)
|
|
388
|
+
ww, wh = glfw.get_window_size(window)
|
|
389
|
+
cx, cy = wx + ww / 2, wy + wh / 2
|
|
390
|
+
target = None
|
|
391
|
+
for m in glfw.get_monitors():
|
|
392
|
+
mx, my = glfw.get_monitor_pos(m)
|
|
393
|
+
mode = glfw.get_video_mode(m)
|
|
394
|
+
if (mx <= cx < mx + mode.size.width
|
|
395
|
+
and my <= cy < my + mode.size.height):
|
|
396
|
+
target = m
|
|
397
|
+
break
|
|
398
|
+
if target is None:
|
|
399
|
+
target = glfw.get_primary_monitor()
|
|
400
|
+
ax, ay, aw, ah = glfw.get_monitor_workarea(target)
|
|
401
|
+
return float(ax), float(ay), float(ax + aw), float(ay + ah)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# FontAwesome glyphs (merged in the default UI font - see fonts._fa_merge),
|
|
405
|
+
# same icon set the rest of the app draws with.
|
|
406
|
+
_ICON_MINIMIZE = "\uf2d1" # fa window-minimize
|
|
407
|
+
_ICON_MAXIMIZE = "\uf2d0" # fa window-maximize
|
|
408
|
+
_ICON_RESTORE = "\uf2d2" # fa window-restore
|
|
409
|
+
_ICON_CLOSE = "\uf00d" # fa times
|
|
410
|
+
_ICON_MOVE = "\uf0b2" # fa arrows-alt - the desktop's left-drag move toggle (hypr_left_drag)
|
|
411
|
+
_ICONS = (_ICON_MINIMIZE, _ICON_MAXIMIZE, _ICON_RESTORE, _ICON_CLOSE, _ICON_MOVE)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _button_icon(kind, maximized):
|
|
415
|
+
if kind == "minimize":
|
|
416
|
+
return _ICON_MINIMIZE
|
|
417
|
+
if kind == "maximize":
|
|
418
|
+
return _ICON_RESTORE if maximized else _ICON_MAXIMIZE
|
|
419
|
+
if kind == "move":
|
|
420
|
+
return _ICON_MOVE
|
|
421
|
+
return _ICON_CLOSE
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def button_layout():
|
|
425
|
+
"""(left kinds, right kinds) the chrome shows — each a tuple over
|
|
426
|
+
"minimize" / "maximize" / "close", in on-screen order: Toggles.Melty
|
|
427
|
+
.titlebar_button_layout (GNOME syntax) when set, else the desktop's own
|
|
428
|
+
title-bar button setting (titlebar_buttons.system_layout: gsettings,
|
|
429
|
+
kwinrc, xfwm4, the settings portal)."""
|
|
430
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
431
|
+
pinned = Toggles.Melty.titlebar_button_layout
|
|
432
|
+
if pinned:
|
|
433
|
+
return titlebar_buttons.parse_gnome(pinned)
|
|
434
|
+
return titlebar_buttons.system_layout()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def drag_anywhere_enabled():
|
|
438
|
+
"""Whether an unclaimed left drag on bare background moves the OS window
|
|
439
|
+
through meltygui's OWN xdg_toplevel.move this frame:
|
|
440
|
+
Toggles.Melty.move_drag_anywhere, and — on the patched Hyprland that
|
|
441
|
+
carries the window-gesture toggle (hypr_left_drag) — only while the
|
|
442
|
+
compositor's own left-drag move does NOT apply to this app's class
|
|
443
|
+
(the chrome's "move" button faded). Lit, the compositor drives the
|
|
444
|
+
move itself and meltygui stands down; faded (the studio always, an app
|
|
445
|
+
after a click), meltygui moves and resizes the window itself. The drag
|
|
446
|
+
strip is the app's caption and keeps moving either way."""
|
|
447
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
448
|
+
if not Toggles.Melty.move_drag_anywhere:
|
|
449
|
+
return False
|
|
450
|
+
if hypr_left_drag.available():
|
|
451
|
+
return not hypr_left_drag.enabled()
|
|
452
|
+
return True
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def control_kinds():
|
|
456
|
+
"""button_layout() plus the desktop's own extra: on Lukas's patched
|
|
457
|
+
Hyprland (hypr_left_drag.available — the compositor has
|
|
458
|
+
general:left_drag_move) a "move" toggle, the hyprbars
|
|
459
|
+
`state = "left_drag_move"` button mirrored, unless
|
|
460
|
+
Toggles.Melty.titlebar_move_toggle hides it. It sits INNERMOST of the
|
|
461
|
+
right group like the hyprbars one (before minimize / maximize / close),
|
|
462
|
+
or ends the left group when everything is on the left."""
|
|
463
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
464
|
+
left_kinds, right_kinds = button_layout()
|
|
465
|
+
if not (Toggles.Melty.titlebar_move_toggle and hypr_left_drag.available()):
|
|
466
|
+
return left_kinds, right_kinds
|
|
467
|
+
if right_kinds or not left_kinds:
|
|
468
|
+
return tuple(left_kinds), ("move",) + tuple(right_kinds)
|
|
469
|
+
return tuple(left_kinds) + ("move",), tuple(right_kinds)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _button_metrics():
|
|
473
|
+
"""(margin, gap, height, {kind: width}) shared by both groups: the
|
|
474
|
+
header's close button sizing (flat_button: glyph + px(15) wide, + px(8)
|
|
475
|
+
tall — every kind its own glyph, like the header) inset by
|
|
476
|
+
button_margin from the corner, button_gap apart."""
|
|
477
|
+
from meltygui.core.melty import Melty
|
|
478
|
+
# [tint=(1.0, 0.55, 0.2)]
|
|
479
|
+
button_margin = Melty.px(4.0)
|
|
480
|
+
# [tint=(1.0, 0.55, 0.2)]
|
|
481
|
+
button_gap = Melty.px(3.0)
|
|
482
|
+
sizes = {icon: imgui.calc_text_size(icon) for icon in _ICONS}
|
|
483
|
+
height = max(s.y for s in sizes.values()) + Melty.px(8.0)
|
|
484
|
+
return button_margin, button_gap, height, {icon: s.x + Melty.px(15.0) for icon, s in sizes.items()}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _button_layout(disp_w, maximized):
|
|
488
|
+
"""[(kind, icon, rect)] of every control the layout asks for, in
|
|
489
|
+
on-screen order: the left group from the top-left corner, the right
|
|
490
|
+
group right-aligned at the top-right, each inset button_margin from
|
|
491
|
+
its corner and button_gap apart."""
|
|
492
|
+
left_kinds, right_kinds = control_kinds()
|
|
493
|
+
button_margin, button_gap, height, widths = _button_metrics()
|
|
494
|
+
buttons = []
|
|
495
|
+
x0 = button_margin
|
|
496
|
+
for kind in left_kinds:
|
|
497
|
+
icon = _button_icon(kind, maximized)
|
|
498
|
+
width = widths[icon]
|
|
499
|
+
buttons.append((kind, icon, (x0, button_margin, x0 + width, button_margin + height)))
|
|
500
|
+
x0 += width + button_gap
|
|
501
|
+
right = []
|
|
502
|
+
x1 = disp_w - button_margin
|
|
503
|
+
for kind in reversed(right_kinds):
|
|
504
|
+
icon = _button_icon(kind, maximized)
|
|
505
|
+
width = widths[icon]
|
|
506
|
+
right.append((kind, icon, (x1 - width, button_margin, x1, button_margin + height)))
|
|
507
|
+
x1 -= width + button_gap
|
|
508
|
+
right.reverse()
|
|
509
|
+
return buttons + right
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _button_bands(buttons, disp_w):
|
|
513
|
+
"""(left_edge, right_edge): the x range between the two control groups
|
|
514
|
+
— the drag strip's span (a corner group's whole column is the
|
|
515
|
+
buttons', not the strip's). The window's edges when a side is empty."""
|
|
516
|
+
left_edge = max((r[2] for kind, icon, r in buttons if r[0] < disp_w / 2), default=0.0)
|
|
517
|
+
right_edge = min((r[0] for kind, icon, r in buttons if r[0] >= disp_w / 2), default=disp_w)
|
|
518
|
+
return left_edge, right_edge
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def chrome_insets():
|
|
522
|
+
"""(left_px, right_px) the window controls take from the window's top
|
|
523
|
+
corners this frame — what a meltygui header drawn in the chrome row keeps
|
|
524
|
+
clear on each side (surface.root_view_kwargs: `header_indent` on the
|
|
525
|
+
left, a reserving `with_header_end` on the right). 0 for an empty side
|
|
526
|
+
or with the chrome off."""
|
|
527
|
+
if not titlebar_enabled():
|
|
528
|
+
return 0.0, 0.0
|
|
529
|
+
left_kinds, right_kinds = control_kinds()
|
|
530
|
+
button_margin, button_gap, height, widths = _button_metrics()
|
|
531
|
+
maximized = _maximized(_studio_window())
|
|
532
|
+
|
|
533
|
+
def group(kinds):
|
|
534
|
+
if not kinds:
|
|
535
|
+
return 0.0
|
|
536
|
+
return button_margin + sum(widths[_button_icon(k, maximized)] for k in kinds) \
|
|
537
|
+
+ button_gap * len(kinds)
|
|
538
|
+
|
|
539
|
+
return group(left_kinds), group(right_kinds)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# Melty.frame_count of the frame whose root view hosted the controls
|
|
543
|
+
# (draw_header_controls): paint_window_controls and draw_titlebar's button
|
|
544
|
+
# hits stand down for it - the header painted and clicked them.
|
|
545
|
+
_hosted_frame = -1
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def draw_header_controls(draw_state=None, **kwargs):
|
|
549
|
+
"""The `with_header_end` of an OS window's root view (surface
|
|
550
|
+
.root_view_kwargs): the window controls painted INSIDE the root's meltygui
|
|
551
|
+
header, where a header's close button sits — the same flat_button paint
|
|
552
|
+
as _paint_buttons, in the root's own tile (an overlay paint under a
|
|
553
|
+
cached root tile was blitted over — the glyphs vanished, 09-12), hover
|
|
554
|
+
through the wrapper's repaint of a bounding-hovered view, clicks through
|
|
555
|
+
draw_state.on_action. In the flow it claims the RIGHT group's width, so
|
|
556
|
+
the wrapper right-aligns it under those buttons and clips the header
|
|
557
|
+
before them (draw_state.header_end_width); both groups are painted at
|
|
558
|
+
their corner rects (the root fills the surface, so window and surface
|
|
559
|
+
coordinates agree). The overlay path stands down for the frame
|
|
560
|
+
(_hosted_frame)."""
|
|
561
|
+
global _hosted_frame
|
|
562
|
+
from meltygui.core.melty import Melty
|
|
563
|
+
imgui.dummy(chrome_insets()[1], 0)
|
|
564
|
+
if draw_state is None or not titlebar_enabled():
|
|
565
|
+
return
|
|
566
|
+
window = _studio_window()
|
|
567
|
+
io = imgui.get_io()
|
|
568
|
+
buttons = _button_layout(io.display_size.x, _maximized(window))
|
|
569
|
+
mx, my = io.mouse_pos.x, io.mouse_pos.y
|
|
570
|
+
over_button = next((i for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons)
|
|
571
|
+
if x0 <= mx <= x1 and y0 <= my <= y1), None)
|
|
572
|
+
if not draw_state._bounding_hovered or Melty.on_drag:
|
|
573
|
+
over_button = None
|
|
574
|
+
_hosted_frame = Melty.frame_count
|
|
575
|
+
_paint_buttons(imgui.get_window_draw_list(), buttons, over_button)
|
|
576
|
+
for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons):
|
|
577
|
+
# Same delivery as flat_button's own click (priority_delta=4 over
|
|
578
|
+
# the body's registrations, the plain pointer over the button).
|
|
579
|
+
if draw_state.on_action("left_mouse_clicked", view_id=f"titlebar_button_{i}",
|
|
580
|
+
rect=(x0, y0, x1, y1), priority_delta=4,
|
|
581
|
+
cursor=mouse_cursor.ARROW) is not None:
|
|
582
|
+
_activate_button(kind, window)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _activate_button(kind, window):
|
|
586
|
+
"""What a control does: minimize / maximize / close (the close deferred
|
|
587
|
+
to the merge window while pending state would be lost) / the desktop's
|
|
588
|
+
left-drag-move toggle for this app (hypr_left_drag.toggle)."""
|
|
589
|
+
if kind == "minimize":
|
|
590
|
+
glfw.iconify_window(window)
|
|
591
|
+
elif kind == "maximize":
|
|
592
|
+
_toggle_maximize(window)
|
|
593
|
+
elif kind == "move":
|
|
594
|
+
hypr_left_drag.toggle()
|
|
595
|
+
elif _close_blocked_by_merge():
|
|
596
|
+
pass # merge window opened instead; app stays up
|
|
597
|
+
else:
|
|
598
|
+
_close_window(window)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _studio_window():
|
|
602
|
+
"""The active OS window, accepting both native and GLFW handles.
|
|
603
|
+
|
|
604
|
+
Keep mocked vis handles out: ctypes can coerce MagicMock to a garbage
|
|
605
|
+
pointer. Native windows are Python objects with a class-level marker.
|
|
606
|
+
"""
|
|
607
|
+
import ctypes
|
|
608
|
+
from meltygui.core.melty import Melty
|
|
609
|
+
window = Melty.glfw_window or getattr(Melty.vis, "window", None)
|
|
610
|
+
return window if glfw.is_native_window(window) or isinstance(window, ctypes._Pointer) else None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _main_window_ds():
|
|
614
|
+
"""The Main Window's draw_state (draw_main) — the owner the controls'
|
|
615
|
+
flat-mask marks ride under. In an app (surface.py, no draw_main) the
|
|
616
|
+
surface's root meltygui window: with its header in the chrome row its
|
|
617
|
+
cached tile covers the corners, and an OWNERLESS mark never keeps a
|
|
618
|
+
blit from copying over the buttons. None before the root's first
|
|
619
|
+
frame."""
|
|
620
|
+
from meltygui.core.melty import Melty
|
|
621
|
+
registry = getattr(Melty, "draw_state_registry", None) or {}
|
|
622
|
+
main = next((d for d in registry.values() if getattr(d, "name", None) == "Main Window"), None)
|
|
623
|
+
if main is not None:
|
|
624
|
+
return main
|
|
625
|
+
roots = getattr(Melty, "root_draw_states", None) or {}
|
|
626
|
+
return next((d for entries in roots.values() for d in entries), None)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def paint_window_controls(draw_list):
|
|
630
|
+
"""Paint the min/max/close controls — from draw_melty_windows, on the
|
|
631
|
+
MAIN WINDOW draw list's top channel just before Melty.end_frame, i.e.
|
|
632
|
+
BEFORE the capture/shadow passes (draw_titlebar keeps the hit logic).
|
|
633
|
+
That placement is the whole point: the shadow composite paints each
|
|
634
|
+
depth mark's drop shadow and lit rim INTO the framebuffer and the
|
|
635
|
+
overlay list renders after it, so buttons drawn there covered their own
|
|
636
|
+
rim with their background (the "rect in front of it" look). Painted
|
|
637
|
+
here they get composited exactly like a header's close button.
|
|
638
|
+
|
|
639
|
+
Each button also marks the FLAT mask at the top paint rank (owner: the
|
|
640
|
+
Main Window's draw_state — an ownerless mask rect never terminates the
|
|
641
|
+
mask build's parent walk), which is what keeps a cached window blitted
|
|
642
|
+
under the corner from copying over them, and what gives them their
|
|
643
|
+
depth for the shadow pass — the same mark a window gets."""
|
|
644
|
+
global _pressed_button
|
|
645
|
+
from meltygui.core.melty import Melty
|
|
646
|
+
from meltygui.core.runtime.toggles import shadow_depth_at
|
|
647
|
+
if not titlebar_enabled() or _hosted_frame == Melty.frame_count:
|
|
648
|
+
return # off, or the root's header painted them this frame (draw_header_controls)
|
|
649
|
+
window = _studio_window()
|
|
650
|
+
io = imgui.get_io()
|
|
651
|
+
disp_w = io.display_size.x
|
|
652
|
+
mx, my = io.mouse_pos.x, io.mouse_pos.y
|
|
653
|
+
maximized = _maximized(window)
|
|
654
|
+
buttons = _button_layout(disp_w, maximized)
|
|
655
|
+
over_button = next((i for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons)
|
|
656
|
+
if x0 <= mx <= x1 and y0 <= my <= y1), None)
|
|
657
|
+
# Top-most of everything painted: the highest paint rank + a little depth.
|
|
658
|
+
layer = Melty.nested_layer_max - 1
|
|
659
|
+
rank = shadow_depth_at(2, layer)
|
|
660
|
+
owner = _main_window_ds()
|
|
661
|
+
if Melty.cache is not None and owner is not None:
|
|
662
|
+
for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons):
|
|
663
|
+
Melty.cache.mask_mark_rect(owner, layer, rank, x0, y0, x1 - x0, y1 - y0,
|
|
664
|
+
f"titlebar_button_{i}", corner_radius=Melty.px(6.0))
|
|
665
|
+
if Melty.channels_split:
|
|
666
|
+
draw_list.channels_set_current(Melty.max_depth - 1)
|
|
667
|
+
_paint_buttons(draw_list, buttons, over_button)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# Toggles.show_fps readout. Change the pill/text values to restyle it;
|
|
671
|
+
# FPS_GAP is its distance from the right control group (or the window edge).
|
|
672
|
+
FPS_GAP = 8.0
|
|
673
|
+
FPS_PAD_X = 6.0
|
|
674
|
+
FPS_PILL_COLOR = (0.0, 0.0, 0.0, 0.55)
|
|
675
|
+
FPS_TEXT_COLOR = (0.85, 0.95, 0.85)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# (label, chrome) a Surface measured for the frame being drawn; consumed by
|
|
679
|
+
# paint_fps. None: the studio's window, which falls back to imgui's rate.
|
|
680
|
+
_fps_note = None
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def note_fps_label(label, chrome):
|
|
684
|
+
global _fps_note
|
|
685
|
+
_fps_note = (label, chrome)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def paint_fps(overlay):
|
|
689
|
+
"""Toggles.show_fps: the OS window's frame-rate label, right-aligned in
|
|
690
|
+
the titlebar strip before the right control group (the top-right corner
|
|
691
|
+
of a window without Melty chrome). Called from Melty.end_frame with the
|
|
692
|
+
overlay list at its top channel: the label changes every frame, and the
|
|
693
|
+
overlay renders after the cached tiles' blits, so no view is
|
|
694
|
+
invalidated to keep it current."""
|
|
695
|
+
global _fps_note
|
|
696
|
+
from meltygui.core.melty import Melty
|
|
697
|
+
from meltygui.hdr_color import pack_color
|
|
698
|
+
io = imgui.get_io()
|
|
699
|
+
label, chrome = _fps_note or (f"{io.framerate:.0f} fps", True)
|
|
700
|
+
_fps_note = None
|
|
701
|
+
disp_w = io.display_size.x
|
|
702
|
+
right = disp_w
|
|
703
|
+
if chrome and titlebar_enabled():
|
|
704
|
+
buttons = _button_layout(disp_w, _maximized(_studio_window()))
|
|
705
|
+
right = _button_bands(buttons, disp_w)[1]
|
|
706
|
+
text = imgui.calc_text_size(label)
|
|
707
|
+
pad = Melty.px(FPS_PAD_X)
|
|
708
|
+
height = min(Melty.px(_BTN_H), text.y + Melty.px(6.0))
|
|
709
|
+
x1 = right - Melty.px(FPS_GAP)
|
|
710
|
+
x0 = x1 - text.x - 2 * pad
|
|
711
|
+
y0 = (Melty.px(_BTN_H) - height) * 0.5
|
|
712
|
+
overlay.add_rect_filled(x0, y0, x1, y0 + height, pack_color(*FPS_PILL_COLOR), rounding=Melty.px(6.0))
|
|
713
|
+
overlay.add_text(x0 + pad, y0 + (height - text.y) * 0.5, pack_color(*FPS_TEXT_COLOR, 1.0), label)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _paint_buttons(dl, buttons, over_button):
|
|
717
|
+
"""The controls (`buttons` = _button_layout's list), each painted EXACTLY like a window header's
|
|
718
|
+
close button (draw_header_end): the same flat_button call — colour
|
|
719
|
+
(9, 1, 1), glyph + px(15) by glyph + px(8), rounded, theme-mixed glyph,
|
|
720
|
+
hover brightening, a depth mark for the shadow pass (drop shadow + lit
|
|
721
|
+
rim) — run under Toggles.Melty.melty_window_tint set in the style
|
|
722
|
+
manager, the way a header runs under its window's tint (that tint is
|
|
723
|
+
what make_color_rgb mixes the colour against). The previous tint is
|
|
724
|
+
restored afterwards."""
|
|
725
|
+
from meltygui.view.header_view import flat_button
|
|
726
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
727
|
+
from meltygui.core.melty import Melty
|
|
728
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
729
|
+
# The header close button's colour (draw_header_end).
|
|
730
|
+
# [tint=(0.9, 0.15, 0.15)]
|
|
731
|
+
close_color = (9, 1, 1)
|
|
732
|
+
# The move toggle while left-drag move is OFF for this app: the
|
|
733
|
+
# hyprbars button's fade — the glyph this grey (text_color, the
|
|
734
|
+
# full-range bypass) on a disc this much darker (tint_value; 0.16 lit).
|
|
735
|
+
# [tint=(0.9, 0.15, 0.15)]
|
|
736
|
+
move_off_glyph = (0.4, 0.4, 0.4)
|
|
737
|
+
# [tint=(0.9, 0.15, 0.15)]
|
|
738
|
+
move_off_disc_value = 0.06
|
|
739
|
+
style_manager = Melty.style_manager
|
|
740
|
+
previous_tint = style_manager.get_tint() if style_manager is not None else None
|
|
741
|
+
chrome_tint = Toggles.Melty.melty_window_tint
|
|
742
|
+
if style_manager is not None and chrome_tint and len(chrome_tint) >= 3:
|
|
743
|
+
style_manager.set_imgui_tint(*chrome_tint[:4])
|
|
744
|
+
try:
|
|
745
|
+
for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons):
|
|
746
|
+
# The header close's own lift over its surface (+2 over the
|
|
747
|
+
# flat-mask mark paint_window_controls stamped for the button).
|
|
748
|
+
add_shadow((x0, y0, x1 - x0, y1 - y0), corner_radius=Melty.px(6.0), clip=False,
|
|
749
|
+
layer=Melty.nested_layer_max - 1, depth=2)
|
|
750
|
+
faded = kind == "move" and not hypr_left_drag.enabled()
|
|
751
|
+
flat_button(icon, None, view_id=f"titlebar_button_{i}",
|
|
752
|
+
width=x1 - x0, height=y1 - y0, pos=(x0, y0),
|
|
753
|
+
hovered=(over_button == i), layout=False, draw_list=dl,
|
|
754
|
+
color=close_color,
|
|
755
|
+
**({"text_color": move_off_glyph, "tint_value": move_off_disc_value}
|
|
756
|
+
if faded else {}))
|
|
757
|
+
finally:
|
|
758
|
+
if style_manager is not None and previous_tint is not None:
|
|
759
|
+
style_manager.set_imgui_tint(*previous_tint)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _close_blocked_by_merge():
|
|
763
|
+
"""True when quitting would lose pending state: some file has BOTH pending
|
|
764
|
+
edits and unmerged external drift (PendingSave.needs_merge). Instead of
|
|
765
|
+
closing, surface the merge window — the manual merge is the only way that
|
|
766
|
+
state resolves — with as much as possible already staged (its Auto-merge
|
|
767
|
+
run over every drifted file) and the conflicts that are left flashed;
|
|
768
|
+
Ctrl+M then applies. Errors never block the close."""
|
|
769
|
+
try:
|
|
770
|
+
from meltygui.editor.pending_save import PendingSave
|
|
771
|
+
if not PendingSave.needs_merge():
|
|
772
|
+
return False
|
|
773
|
+
from meltygui.core.runtime.extensions import get, call
|
|
774
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
775
|
+
if get('conflicts_open') is None:
|
|
776
|
+
notify('External changes conflict with pending edits; save or discard them before closing.', tint=(1.0, 0.7, 0.2))
|
|
777
|
+
return True
|
|
778
|
+
call('conflicts_open', auto_merge=True)
|
|
779
|
+
notify("Unmerged external changes — merge before closing "
|
|
780
|
+
"(Ctrl+M applies the staged merge)", tint=(1.0, 0.7, 0.2))
|
|
781
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
782
|
+
request_render()
|
|
783
|
+
return True
|
|
784
|
+
except Exception:
|
|
785
|
+
return False
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _close_window(window):
|
|
789
|
+
"""The close button's action. Flags the loop to exit and wakes it, so
|
|
790
|
+
the should_close check runs right after THIS frame instead of on the
|
|
791
|
+
next input event (the loop parks in glfw.wait_events). The surface
|
|
792
|
+
itself is hidden by the loop's exit path (LSDStudio._run_visualization's
|
|
793
|
+
finally) BEFORE the seconds-long teardown — never here, mid-frame: the
|
|
794
|
+
frame that fires this still swaps, and swapping onto a surface GLFW has
|
|
795
|
+
just unmapped is what the compositor-close callback path never does."""
|
|
796
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
797
|
+
glfw.set_window_should_close(window, True)
|
|
798
|
+
request_render() # posts the empty event that wakes wait_events
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def draw_titlebar(window):
|
|
802
|
+
"""Per-frame entry point — call inside the imgui frame on the viz thread.
|
|
803
|
+
|
|
804
|
+
Handles decoration sync, the top-right window controls' hit logic
|
|
805
|
+
(their paint is paint_window_controls, earlier in the frame), the top
|
|
806
|
+
drag strip (double-click = maximize unless
|
|
807
|
+
Toggles.Melty.disable_double_click_maximize; with Toggles.Melty.move_drag_anywhere
|
|
808
|
+
an unclaimed left-drag anywhere moves too), edge/corner resize and the
|
|
809
|
+
right-drag resize. The WM gestures go through _NET_WM_MOVERESIZE on X11
|
|
810
|
+
and wayland_move (xdg_toplevel.move/resize) on Wayland.
|
|
811
|
+
"""
|
|
812
|
+
global _pressed_button, _wm_move_started, _rdrag
|
|
813
|
+
if not sync_decoration(window):
|
|
814
|
+
_pressed_button = None
|
|
815
|
+
_wm_move_started = False
|
|
816
|
+
_rdrag = None
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
from meltygui.core.melty import Melty
|
|
820
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
821
|
+
|
|
822
|
+
io = imgui.get_io()
|
|
823
|
+
disp_w, disp_h = io.display_size.x, io.display_size.y
|
|
824
|
+
mx, my = io.mouse_pos.x, io.mouse_pos.y
|
|
825
|
+
maximized = _maximized(window)
|
|
826
|
+
sync_window_geometry(window)
|
|
827
|
+
sync_input_region(window)
|
|
828
|
+
# The strip and edge gestures hand the drag to the window manager
|
|
829
|
+
# (_begin_wm_move / _begin_wm_resize) - on Wayland only once wayland_move
|
|
830
|
+
# is ready. The right-drag resize is app-driven (set_window_size) and
|
|
831
|
+
# runs on both; on Wayland it reads the surface relative pointer and
|
|
832
|
+
# always grabs the bottom-right corner (the top-left is pinned).
|
|
833
|
+
wayland = _on_wayland()
|
|
834
|
+
gestures = _wm_gestures_available()
|
|
835
|
+
|
|
836
|
+
# --- the control buttons in the top corners, per the desktop's setup --
|
|
837
|
+
# (titlebar_buttons: re-read while frames render, if a changed desktop
|
|
838
|
+
# setting lands without a restart; the focus hook re-reads too)
|
|
839
|
+
titlebar_buttons.refresh_if_stale(Toggles.Melty.titlebar_button_refresh_s)
|
|
840
|
+
hypr_left_drag.refresh_if_stale(Toggles.Melty.titlebar_button_refresh_s)
|
|
841
|
+
buttons = _button_layout(disp_w, maximized)
|
|
842
|
+
strip_left, strip_right = _button_bands(buttons, disp_w)
|
|
843
|
+
|
|
844
|
+
over_button = None
|
|
845
|
+
for i, (kind, icon, (x0, y0, x1, y1)) in enumerate(buttons):
|
|
846
|
+
if x0 <= mx <= x1 and y0 <= my <= y1:
|
|
847
|
+
over_button = i
|
|
848
|
+
break
|
|
849
|
+
|
|
850
|
+
# --- edge/corner resize (skip while maximized) -------------------------
|
|
851
|
+
border = float(Toggles.Melty.resize_border)
|
|
852
|
+
corner = float(Toggles.Melty.resize_corner)
|
|
853
|
+
edge = None
|
|
854
|
+
if gestures and not maximized and over_button is None:
|
|
855
|
+
edge = _edge_at(mx, my, disp_w, disp_h, border, corner)
|
|
856
|
+
if edge is not None:
|
|
857
|
+
mouse_cursor.request(_EDGE_CURSOR[edge])
|
|
858
|
+
if imgui.is_mouse_clicked(0) and _begin_wm_resize(window, edge):
|
|
859
|
+
return
|
|
860
|
+
|
|
861
|
+
# --- buttons: arm on press, fire on release inside ---------------------
|
|
862
|
+
# (a root header hosting the controls clicks them itself -
|
|
863
|
+
# draw_header_controls - so only the strip / anywhere gating hits
|
|
864
|
+
# over_button on such a frame)
|
|
865
|
+
hosted = _hosted_frame == Melty.frame_count
|
|
866
|
+
if hosted:
|
|
867
|
+
_pressed_button = None
|
|
868
|
+
elif over_button is not None and imgui.is_mouse_clicked(0):
|
|
869
|
+
_pressed_button = over_button
|
|
870
|
+
if _pressed_button is not None and imgui.is_mouse_released(0):
|
|
871
|
+
if over_button == _pressed_button and _pressed_button < len(buttons):
|
|
872
|
+
_activate_button(buttons[_pressed_button][0], window)
|
|
873
|
+
_pressed_button = None
|
|
874
|
+
|
|
875
|
+
# --- drag strip along the top edge -------------------------------------
|
|
876
|
+
# Register as the lowest-priority drag subscriber while the cursor is in
|
|
877
|
+
# the strip; the handler's down-capture means a view that uses
|
|
878
|
+
# click-and-drag always wins, and the strip only sees the drag when
|
|
879
|
+
# nothing else claimed it. Clean clicks are untouched (dragged fires
|
|
880
|
+
# only past the handler's drag threshold). Double-click = maximize,
|
|
881
|
+
# same lowest-priority rule.
|
|
882
|
+
# drag_anywhere_enabled (Toggles.Melty.move_drag_anywhere, gated by the
|
|
883
|
+
# desktop's left-drag-move toggle where it exists) widens the drag (not
|
|
884
|
+
# the double-click) to the whole window at the same worst priority: even
|
|
885
|
+
# a drag nobody else claimed - bare background - moves the OS window.
|
|
886
|
+
# Toggles.Melty.disable_double_click_maximize drops the double-click
|
|
887
|
+
# subscription entirely, so the click reaches the view under the cursor.
|
|
888
|
+
# Both the strip and the right-drag resize below subscribe NON-BLOCKING:
|
|
889
|
+
# a closable meltygui window is an event blocker (core_render registers
|
|
890
|
+
# `blocker=closable`) and a meltygui app's root IS such a window, pinned
|
|
891
|
+
# over the whole surface — a plain subscription at worst priority sat
|
|
892
|
+
# behind that blocker and was dropped before dispatch (the app's left /
|
|
893
|
+
# right drags never reached the OS chrome; the studio's roots leave its
|
|
894
|
+
# background bare, which is why they worked there). A non_blocking
|
|
895
|
+
# subscription survives the blocker pass and still resolves LAST, so a
|
|
896
|
+
# view that claims the drag keeps winning (input_handler: the
|
|
897
|
+
# non-blocking chain stops at the first blocking subscriber).
|
|
898
|
+
strip_h = float(Toggles.Melty.drag_strip_height)
|
|
899
|
+
in_strip = my <= strip_h and strip_left <= mx < strip_right
|
|
900
|
+
strip_double_click = in_strip and not Toggles.Melty.disable_double_click_maximize
|
|
901
|
+
anywhere = drag_anywhere_enabled() and over_button is None
|
|
902
|
+
if gestures and edge is None and (in_strip or anywhere):
|
|
903
|
+
# Move pointer only where the strip would actually get the drag
|
|
904
|
+
# (cursor_gate): bare background, never over a view that claims it.
|
|
905
|
+
Melty.event_handler.register_hovered(
|
|
906
|
+
_STRIP_ID,
|
|
907
|
+
["non_blocking_left_mouse_dragged", "non_blocking_left_mouse_double_clicked"]
|
|
908
|
+
if strip_double_click else ["non_blocking_left_mouse_dragged"],
|
|
909
|
+
priority=_STRIP_PRIORITY,
|
|
910
|
+
cursor=mouse_cursor.MOVE if Toggles.Melty.window_move_cursor else None,
|
|
911
|
+
cursor_gate="left_mouse_dragged")
|
|
912
|
+
strip_events = (getattr(Melty, "events", None) or {}).get(_STRIP_ID, {})
|
|
913
|
+
if _wm_move_started and not Melty.event_handler.is_down("left_mouse"):
|
|
914
|
+
_wm_move_started = False # synthetic release landed - re-arm
|
|
915
|
+
if "non_blocking_left_mouse_double_clicked" in strip_events \
|
|
916
|
+
and not Toggles.Melty.disable_double_click_maximize:
|
|
917
|
+
_toggle_maximize(window)
|
|
918
|
+
elif "non_blocking_left_mouse_dragged" in strip_events and not _wm_move_started:
|
|
919
|
+
_wm_move_started = True
|
|
920
|
+
if _begin_wm_move(window):
|
|
921
|
+
return
|
|
922
|
+
|
|
923
|
+
# --- right-drag resize, anywhere in the window -------------------------
|
|
924
|
+
# Same worst-priority pattern as the strip, but over the WHOLE window:
|
|
925
|
+
# any view that actually uses a right drag (view orbits, meltygui window
|
|
926
|
+
# resize) captures it first, and plain right-CLICKS are untouched
|
|
927
|
+
# (dragged only fires past the handler's drag threshold, so context
|
|
928
|
+
# menus keep working). The drag is a cursor-driven drag of the OS
|
|
929
|
+
# window's OWN frame edges through the edge physics (os_frame.queue_drag
|
|
930
|
+
# → the roots' frame passes): the bottom-right corner, or the top-left
|
|
931
|
+
# on a DOUBLE right-drag (press-press-drag - the meltygui windows' rule),
|
|
932
|
+
# decided at the press for the whole gesture. The screen's work area is
|
|
933
|
+
# the wall; an edge blocked there grows the window on the other side,
|
|
934
|
+
# just like a meltygui window's edge against the display.
|
|
935
|
+
if not maximized and over_button is None and edge is None:
|
|
936
|
+
Melty.event_handler.register_hovered(
|
|
937
|
+
_RESIZE_ID, ["non_blocking_right_mouse_dragged", "non_blocking_right_mouse_double_dragged"],
|
|
938
|
+
priority=_STRIP_PRIORITY)
|
|
939
|
+
# (the drag events themselves are consumed by poll_os_window_drag at
|
|
940
|
+
# the frame's START - before the root windows solve - so the OS edge
|
|
941
|
+
# moves in the same frame the hand did)
|
|
942
|
+
if _rdrag is not None:
|
|
943
|
+
mouse_cursor.request(_EDGE_CURSOR[_SIZE_TOPLEFT if _rdrag["top_left"] else _SIZE_BOTTOMRIGHT])
|
|
944
|
+
|
|
945
|
+
# The buttons themselves are painted earlier in the frame, in the main
|
|
946
|
+
# window draw list (paint_window_controls) - see there for why.
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def poll_os_window_drag():
|
|
950
|
+
"""Frame START (draw_melty_windows, right after os_frame.begin_frame):
|
|
951
|
+
turn this frame's right-drag events on the background (_RESIZE_ID,
|
|
952
|
+
registered by draw_titlebar) into cursor-driven drags of the OS
|
|
953
|
+
window's frame edges — os_frame.queue_drag with the drag total's
|
|
954
|
+
per-frame increment — so the first root window's pass solves them in
|
|
955
|
+
THIS frame. Polled at the end of the frame (draw_titlebar runs after
|
|
956
|
+
the meltygui windows and os_frame.flush) the drag landed a frame late."""
|
|
957
|
+
global _rdrag
|
|
958
|
+
from meltygui.core.melty import Melty
|
|
959
|
+
handler = getattr(Melty, "event_handler", None)
|
|
960
|
+
if handler is None:
|
|
961
|
+
return
|
|
962
|
+
resize_events = (getattr(Melty, "events", None) or {}).get(_RESIZE_ID, {})
|
|
963
|
+
released = not handler.is_down("right_mouse")
|
|
964
|
+
drag = (resize_events.get("non_blocking_right_mouse_double_dragged")
|
|
965
|
+
or resize_events.get("non_blocking_right_mouse_dragged"))
|
|
966
|
+
if drag is None:
|
|
967
|
+
if released:
|
|
968
|
+
_rdrag = None
|
|
969
|
+
return
|
|
970
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
971
|
+
if _rdrag is None:
|
|
972
|
+
_rdrag = {"top_left": "non_blocking_right_mouse_double_dragged" in resize_events, "x": 0.0, "y": 0.0}
|
|
973
|
+
index = 0 if _rdrag["top_left"] else 1
|
|
974
|
+
for axis, total in (("x", "total_dx"), ("y", "total_dy")):
|
|
975
|
+
now = float(getattr(drag, total, 0.0) or 0.0)
|
|
976
|
+
inc = now - _rdrag[axis]
|
|
977
|
+
_rdrag[axis] = now
|
|
978
|
+
os_frame.queue_drag(axis, index, inc)
|
|
979
|
+
# The next frame can carry the drag total. Consume its remainder
|
|
980
|
+
# against the old baseline before clearing it, not the whole drag again.
|
|
981
|
+
if released:
|
|
982
|
+
_rdrag = None
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# ---------------------------------------------------------------------------
|
|
986
|
+
# Rounded window corners - a transparent framebuffer + a final alpha pass
|
|
987
|
+
# ---------------------------------------------------------------------------
|
|
988
|
+
|
|
989
|
+
def wants_transparent_framebuffer():
|
|
990
|
+
"""Boot hint (GLFW TRANSPARENT_FRAMEBUFFER): only a frameless window
|
|
991
|
+
with a corner radius or a shadow margin needs per-pixel alpha at the
|
|
992
|
+
compositor."""
|
|
993
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
994
|
+
return titlebar_enabled() and (Toggles.Melty.window_corner_radius > 0
|
|
995
|
+
or Toggles.Melty.window_shadow_margin > 0)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _frame_transparent(window):
|
|
999
|
+
"""Was the OS window CREATED with an alpha framebuffer (the boot hint)?
|
|
1000
|
+
Only then do the corner cut and the shadow margin make sense — on an
|
|
1001
|
+
opaque window the premultiply would paint the corners black."""
|
|
1002
|
+
try:
|
|
1003
|
+
return bool(window is not None
|
|
1004
|
+
and glfw.get_window_attrib(window, glfw.TRANSPARENT_FRAMEBUFFER))
|
|
1005
|
+
except Exception:
|
|
1006
|
+
return False
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _maximized(window):
|
|
1010
|
+
"""Is the OS window maximized? The ONE read every maximize-sensitive
|
|
1011
|
+
path takes (the shadow inset, the button glyph, the edge zones, the
|
|
1012
|
+
background right-drag's registration). GLFW's MAXIMIZED attribute
|
|
1013
|
+
everywhere but Hyprland: Hyprland sends the xdg `maximized` state to
|
|
1014
|
+
EVERY toplevel at map, to stop clients drawing their own decorations
|
|
1015
|
+
(XDGShell.cpp, "this forces apps to not draw CSD"), and never clears
|
|
1016
|
+
it — so GLFW read the studio as maximized for its whole life there:
|
|
1017
|
+
inset 0, no edge zones, and draw_titlebar never registered the
|
|
1018
|
+
background right-drag (09-09, "right-drag does nothing on
|
|
1019
|
+
Hyprland"). There the feed's own flag is the truth: Hyprland's
|
|
1020
|
+
`fullscreen == 1` (the maximize fullscreen mode) on the studio's
|
|
1021
|
+
client, False until the feed has seen the window."""
|
|
1022
|
+
if window is None:
|
|
1023
|
+
return False
|
|
1024
|
+
if _on_hyprland():
|
|
1025
|
+
# Whatever the compositor does with the window geometry, it still
|
|
1026
|
+
# forces the maximized state at map (XDGShell.cpp) - reading GLFW's
|
|
1027
|
+
# attribute on the state-honouring build zeroed the inset and
|
|
1028
|
+
# dropped the right-drag again (09-10, first login on that build).
|
|
1029
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1030
|
+
frame = geometry_feed._STATE.get("frame") or {}
|
|
1031
|
+
return bool(frame.get("maximized", False))
|
|
1032
|
+
try:
|
|
1033
|
+
return bool(glfw.get_window_attrib(window, glfw.MAXIMIZED))
|
|
1034
|
+
except Exception:
|
|
1035
|
+
return False
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def shadow_reach(fb_w, fb_h):
|
|
1039
|
+
"""px the shadow pass reaches past a caster's edge on a fb_w × fb_h
|
|
1040
|
+
framebuffer, from ShadowCast's own terms: it marches 16 depth slices of
|
|
1041
|
+
1/max_steps above the receiver, each contributing
|
|
1042
|
+
hit_strength − height·hit_falloff (so the height budget is the smaller
|
|
1043
|
+
of the 16 slices and hit_strength/hit_falloff), offsets the sample by
|
|
1044
|
+
height·height_scale along the light direction — in UV, so the reach in
|
|
1045
|
+
px grows with the framebuffer — and blurs by height·blur_scale. The
|
|
1046
|
+
larger of the x/y reaches, rounded UP to 16 px so small resizes don't
|
|
1047
|
+
wobble the margin. Measured against the real pass (4072×2136: predicts
|
|
1048
|
+
69/49 px right/down, the pass fades out by 57/42). 0 with the shadow
|
|
1049
|
+
pass off."""
|
|
1050
|
+
import math
|
|
1051
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1052
|
+
from meltygui.core.melty import Melty
|
|
1053
|
+
if not Toggles.filters or fb_w <= 0 or fb_h <= 0:
|
|
1054
|
+
return 0
|
|
1055
|
+
total_layers = 100.0 / ((Melty.max_layer - 1.0) * (Melty.max_depth - 1.0))
|
|
1056
|
+
depth_step = 1.0 / max(1.0, total_layers * 65535.0 / 2.0) # post_frame: max_steps = diff / 2
|
|
1057
|
+
height = 16.0 * depth_step
|
|
1058
|
+
hit_falloff = float(Toggles.shadow_hit_falloff)
|
|
1059
|
+
if hit_falloff > 0.0:
|
|
1060
|
+
height = min(height, float(Toggles.shadow_hit_strength) / hit_falloff)
|
|
1061
|
+
offset_uv = height * (float(Toggles.shadow_height_scale) + float(Toggles.shadow_blur_scale))
|
|
1062
|
+
lx, ly = Toggles.shadow_light_dir
|
|
1063
|
+
norm = math.hypot(lx, ly) or 1.0
|
|
1064
|
+
reach = max(offset_uv * abs(lx) / norm * fb_w, offset_uv * abs(ly) / norm * fb_h)
|
|
1065
|
+
return int(math.ceil(reach / 16.0)) * 16
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _monitor_size(window):
|
|
1069
|
+
"""Pixel size of the primary monitor's current video mode — the most a
|
|
1070
|
+
surface can span, so the reach derived from it is a CONSTANT."""
|
|
1071
|
+
try:
|
|
1072
|
+
mode = glfw.get_video_mode(glfw.get_primary_monitor())
|
|
1073
|
+
return int(mode.size.width), int(mode.size.height)
|
|
1074
|
+
except Exception:
|
|
1075
|
+
return None
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def window_inset():
|
|
1079
|
+
"""px of transparent shadow margin around the content THIS frame: on
|
|
1080
|
+
the transparent frameless window, the larger of
|
|
1081
|
+
Toggles.Melty.window_shadow_margin and the shadow pass's reach
|
|
1082
|
+
(shadow_reach — a fixed margin truncated the shadow mid-fall, a hard
|
|
1083
|
+
band); 0 while maximized / fullscreen (the shadow collapses against
|
|
1084
|
+
the screen edges, as GTK's does) and 0 everywhere else. The reach is
|
|
1085
|
+
taken at the MONITOR's size, not the live surface: derived from the
|
|
1086
|
+
surface it changed mid-resize whenever a 16 px rounding boundary went
|
|
1087
|
+
by, and every change is a visible jump — the content shrinks by twice
|
|
1088
|
+
it, the geometry origin moves so the compositor shifts the surface,
|
|
1089
|
+
the right-drag's latched cap goes stale (a gap at the top) — and a
|
|
1090
|
+
resize hovering at the boundary flickered the margin on and off. The
|
|
1091
|
+
surface simply overhangs a little more than it needs on a small
|
|
1092
|
+
window. imgui's display is the content: SplitOverlayRenderer
|
|
1093
|
+
.process_inputs shrinks display_size by twice this and shifts the
|
|
1094
|
+
pointer; the masks and tiles keep the whole surface."""
|
|
1095
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1096
|
+
margin = int(Toggles.Melty.window_shadow_margin)
|
|
1097
|
+
if margin <= 0:
|
|
1098
|
+
return 0
|
|
1099
|
+
window = _studio_window()
|
|
1100
|
+
if not _frame_transparent(window) or _maximized(window) or _fullscreen(window):
|
|
1101
|
+
return 0
|
|
1102
|
+
size = _monitor_size(window)
|
|
1103
|
+
if size:
|
|
1104
|
+
margin = max(margin, shadow_reach(int(size[0]), int(size[1])))
|
|
1105
|
+
return margin
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _fullscreen(window):
|
|
1109
|
+
"""GLFW-fullscreen (a monitor set on the window). pyglfw hands back a
|
|
1110
|
+
ctypes POINTER even when it is NULL — never compare it to None, a null
|
|
1111
|
+
pointer is FALSY, not None (that mistake collapsed the margin to 0)."""
|
|
1112
|
+
try:
|
|
1113
|
+
return window is not None and bool(glfw.get_window_monitor(window))
|
|
1114
|
+
except Exception:
|
|
1115
|
+
return False
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def frame_geometry(fb_w, fb_h):
|
|
1119
|
+
"""(origin, radius, content_size) of the frame for this frame's REAL
|
|
1120
|
+
framebuffer — origin the content's top-left in the surface — or
|
|
1121
|
+
((0, 0), 0, (0, 0)) when the window is not transparent: the shadow
|
|
1122
|
+
composite's frame uniforms (no frame → everything is content)."""
|
|
1123
|
+
if not _frame_transparent(_studio_window()):
|
|
1124
|
+
return (0.0, 0.0), 0.0, (0.0, 0.0)
|
|
1125
|
+
inset = float(window_inset())
|
|
1126
|
+
ox, oy = content_origin()
|
|
1127
|
+
return (ox, oy), frame_corner_radius(), (float(fb_w) - ox - inset, float(fb_h) - oy - inset)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def content_origin():
|
|
1131
|
+
"""(x, y) of the content's top-left inside the surface: the shadow
|
|
1132
|
+
margin; (0, 0) while maximized / fullscreen."""
|
|
1133
|
+
window = _studio_window()
|
|
1134
|
+
if _maximized(window) or _fullscreen(window):
|
|
1135
|
+
return (0.0, 0.0)
|
|
1136
|
+
inset = float(window_inset())
|
|
1137
|
+
return (inset, inset)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def frame_corner_radius():
|
|
1141
|
+
"""Corner radius of the content this frame: the toggle, 0 while
|
|
1142
|
+
maximized (square against the screen edge)."""
|
|
1143
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1144
|
+
if _maximized(_studio_window()):
|
|
1145
|
+
return 0.0
|
|
1146
|
+
return float(Toggles.Melty.window_corner_radius)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
# Coverage of the CONTENT rounded rect - inset `inset` px into the
|
|
1150
|
+
# framebuffer, `content_size` wide, `radius` corners - at a pixel centre
|
|
1151
|
+
# (gl_FragCoord, origin bottom-left; the rect is symmetric so the flip is
|
|
1152
|
+
# free). One pixel of anti-aliasing ramp across the edge.
|
|
1153
|
+
_COVERAGE_GLSL = """
|
|
1154
|
+
float coverage(vec2 p) {
|
|
1155
|
+
vec2 half_size = content_size * 0.5;
|
|
1156
|
+
vec2 d = abs(p - origin - half_size) - (half_size - vec2(radius));
|
|
1157
|
+
float dist = length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius;
|
|
1158
|
+
return 1.0 - smoothstep(-0.5, 0.5, dist);
|
|
1159
|
+
}
|
|
1160
|
+
"""
|
|
1161
|
+
|
|
1162
|
+
_CORNER_FRAG = """
|
|
1163
|
+
#version 330 core
|
|
1164
|
+
out vec4 FragColor;
|
|
1165
|
+
""" + _COVERAGE_GLSL + """
|
|
1166
|
+
void main() {
|
|
1167
|
+
FragColor = vec4(0.0, 0.0, 0.0, coverage(gl_FragCoord.xy));
|
|
1168
|
+
}
|
|
1169
|
+
"""
|
|
1170
|
+
|
|
1171
|
+
@shader_func(fragment=_CORNER_FRAG)
|
|
1172
|
+
def _corner_alpha_pass(gl_state: GLState = None, content_size=(1.0, 1.0), origin=(0.0, 0.0),
|
|
1173
|
+
radius=0.0, **kwargs):
|
|
1174
|
+
gl.glBindVertexArray(gl_state.vao("fs_triangle"))
|
|
1175
|
+
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
# The GLState for the passes, module-owned (hotswap keeps it).
|
|
1179
|
+
_corner_gl = globals().get("_corner_gl")
|
|
1180
|
+
# The input rect last handed to the compositor (sync_input_region).
|
|
1181
|
+
_input_rect_applied = globals().get("_input_rect_applied")
|
|
1182
|
+
# The window geometry last handed to the compositor (sync_window_geometry).
|
|
1183
|
+
_geometry_applied = globals().get("_geometry_applied")
|
|
1184
|
+
# True while OUR glfw.set_window_size is in flight - its framebuffer-size
|
|
1185
|
+
# callback is not a compositor configure (on_surface_resized).
|
|
1186
|
+
_self_resize = False
|
|
1187
|
+
# The surface size the framebuffer callback last reported (or the creation
|
|
1188
|
+
# size, note_surface_size): a callback repeating it is NOT a configure.
|
|
1189
|
+
_last_surface_size = globals().get("_last_surface_size")
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def note_surface_size(window):
|
|
1193
|
+
"""Record the window's current framebuffer size as the last one seen —
|
|
1194
|
+
Surface.__init__, right after creation: GLFW re-fires the framebuffer
|
|
1195
|
+
callback with the UNCHANGED size when the surface enters an output or
|
|
1196
|
+
gets its fractional scale, and on_surface_resized read that as a
|
|
1197
|
+
compositor configure and regrew the surface by the margin once too
|
|
1198
|
+
often at every launch (09-12)."""
|
|
1199
|
+
global _last_surface_size
|
|
1200
|
+
try:
|
|
1201
|
+
_last_surface_size = tuple(int(v) for v in glfw.get_framebuffer_size(window))
|
|
1202
|
+
except Exception:
|
|
1203
|
+
_last_surface_size = None
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def _on_hyprland():
|
|
1207
|
+
"""The Hyprland backend (whatever it does with the window geometry):
|
|
1208
|
+
the box is resized through its IPC, and its `maximized` state is the
|
|
1209
|
+
feed's flag, not GLFW's."""
|
|
1210
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1211
|
+
return geometry_feed.backend() == "hyprland"
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _box_is_surface():
|
|
1215
|
+
"""A Hyprland that does NOT honour the xdg window geometry: the
|
|
1216
|
+
compositor's window box IS the surface — it renders the whole surface
|
|
1217
|
+
at `at` and ignores the geometry for placement (geometry_feed's module
|
|
1218
|
+
docstring). Every place that reads a configure as a GEOMETRY size, or
|
|
1219
|
+
resizes by committing a buffer, branches on this. The patched
|
|
1220
|
+
compositor (`render:xdg_window_geometry`, 09-09) makes the box the
|
|
1221
|
+
CONTENT — then this is False and the GNOME handling applies: the
|
|
1222
|
+
geometry is the content rect, a configure names the content and the
|
|
1223
|
+
surface is regrown around it by the margin (geometry_feed
|
|
1224
|
+
.hypr_honors_geometry)."""
|
|
1225
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1226
|
+
return geometry_feed.backend() == "hyprland" and not geometry_feed.hypr_honors_geometry()
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def set_surface_size(window, width, height, offset=None, box=True):
|
|
1230
|
+
"""The one way this module resizes the OS surface: flags the resulting
|
|
1231
|
+
framebuffer-size callback as ours, so on_surface_resized leaves it
|
|
1232
|
+
alone (a compositor configure would be grown by the margin). On
|
|
1233
|
+
Hyprland the box is ALSO resized through its IPC — the buffer alone
|
|
1234
|
+
leaves the box where it was — and ``offset`` (dx, dy), the move that
|
|
1235
|
+
rides the same commit elsewhere, goes into that SAME request
|
|
1236
|
+
(geometry_feed.hypr_set_box: resize + move anchored top-left, one
|
|
1237
|
+
eval); other backends ignore ``offset`` here and arm the buffer
|
|
1238
|
+
offset themselves (apply_pending_surface_size). On a Hyprland that
|
|
1239
|
+
honours the window geometry the box is the CONTENT: the IPC gets the
|
|
1240
|
+
surface size less the shadow margin on every side. ``box=False``
|
|
1241
|
+
(on_surface_resized's regrow after a compositor configure) skips the
|
|
1242
|
+
IPC: the box already has that size, and asking again would configure
|
|
1243
|
+
the same size back at GLFW and regrow once more, forever."""
|
|
1244
|
+
global _self_resize
|
|
1245
|
+
_self_resize = True
|
|
1246
|
+
try:
|
|
1247
|
+
glfw.set_window_size(window, int(width), int(height))
|
|
1248
|
+
finally:
|
|
1249
|
+
_self_resize = False
|
|
1250
|
+
if box and _on_hyprland():
|
|
1251
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1252
|
+
dx, dy = offset if offset else (0, 0)
|
|
1253
|
+
box_w, box_h = int(width), int(height)
|
|
1254
|
+
if geometry_feed.hypr_honors_geometry():
|
|
1255
|
+
inset = int(window_inset())
|
|
1256
|
+
box_w, box_h = max(1, box_w - 2 * inset), max(1, box_h - 2 * inset)
|
|
1257
|
+
geometry_feed.hypr_set_box(box_w, box_h, dx, dy)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
# An app-side resize requested mid-frame, applied at the next frame's start.
|
|
1261
|
+
_pending_surface_size = globals().get("_pending_surface_size")
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
_pending_surface_offset = globals().get("_pending_surface_offset")
|
|
1265
|
+
_frame_surface_offset = globals().get("_frame_surface_offset")
|
|
1266
|
+
# The queued request also wants the CONTENT fitted into the work area
|
|
1267
|
+
# (the launch restore - request_surface_size(fit=True)).
|
|
1268
|
+
_pending_surface_fit = globals().get("_pending_surface_fit") or False
|
|
1269
|
+
# Frames a request has waited for the Hyprland feed to see the window (its
|
|
1270
|
+
# `address:` selector): the launch restore is filed before the feed's
|
|
1271
|
+
# first poll. Bounded so a dead feed can't hold a request forever.
|
|
1272
|
+
_pending_surface_wait = globals().get("_pending_surface_wait") or 0
|
|
1273
|
+
PENDING_SURFACE_WAIT_FRAMES = 300
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def request_surface_size(window, width, height, offset=None, fit=False):
|
|
1277
|
+
"""Queue an app-side resize (the right-drag) for the top of the NEXT
|
|
1278
|
+
frame (apply_pending_surface_size, before process_inputs). Applied
|
|
1279
|
+
mid-frame it committed a new buffer size and geometry with content laid
|
|
1280
|
+
out for the old size — one frame of jelly on every drag step. Later
|
|
1281
|
+
requests in the same frame replace earlier ones. ``offset`` = (dx, dy)
|
|
1282
|
+
px to MOVE the window by in the same commit (the buffer offset of the
|
|
1283
|
+
swap's attach — wayland_move.set_surface_offset; the near-edge push:
|
|
1284
|
+
grow right by d and move left by d = the left edge moved out).
|
|
1285
|
+
``fit`` (Hyprland, the launch restore): add to the move whatever keeps
|
|
1286
|
+
the CONTENT inside the work area at the new size — the compositor
|
|
1287
|
+
centred the window at its INITIAL size and the box grows anchored at
|
|
1288
|
+
its top-left, so a restored studio ran off the bottom and right of
|
|
1289
|
+
the screen (and the OS-edge walls then hold it there, 09-09)."""
|
|
1290
|
+
global _pending_surface_size, _pending_surface_offset, _pending_surface_fit
|
|
1291
|
+
_pending_surface_size = (int(width), int(height))
|
|
1292
|
+
_pending_surface_offset = tuple(int(v) for v in offset) if offset else None
|
|
1293
|
+
_pending_surface_fit = bool(fit)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def fit_offset(rect, size, area, inset):
|
|
1297
|
+
"""(dx, dy) that moves a surface at ``rect`` (x, y, w, h on screen) to
|
|
1298
|
+
where its CONTENT — the surface at its new ``size`` less ``inset`` px
|
|
1299
|
+
of shadow margin on every side — lies inside the work ``area`` (x, y,
|
|
1300
|
+
w, h). A content larger than the area keeps its near (top / left)
|
|
1301
|
+
edge on the area's."""
|
|
1302
|
+
out = []
|
|
1303
|
+
for i in (0, 1):
|
|
1304
|
+
near = rect[i] + inset
|
|
1305
|
+
far = rect[i] + size[i] - inset
|
|
1306
|
+
d = min(0, (area[i] + area[i + 2]) - far) # pull back past the far edge
|
|
1307
|
+
d = max(d, area[i] - near) # never past the near one
|
|
1308
|
+
out.append(int(d))
|
|
1309
|
+
return tuple(out)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def apply_pending_surface_size(window):
|
|
1313
|
+
"""LSDStudio's loop, before process_inputs: apply the queued resize so
|
|
1314
|
+
this frame lays out at the new size. Returns the size applied."""
|
|
1315
|
+
global _pending_surface_size, _pending_surface_offset, _frame_surface_offset
|
|
1316
|
+
global _pending_surface_wait, _pending_surface_fit
|
|
1317
|
+
import meltygui.core.windowing.wayland_move as wayland_move
|
|
1318
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
1319
|
+
wayland_move.clear_surface_offset() # last frame's offset is spent
|
|
1320
|
+
# The roots' passes re-base with the OS near edge's motion lands HERE,
|
|
1321
|
+
# with the move it compensates (os_frame: the solve booked it).
|
|
1322
|
+
os_frame.apply_rebase()
|
|
1323
|
+
size = _pending_surface_size
|
|
1324
|
+
if size is None or window is None:
|
|
1325
|
+
return None
|
|
1326
|
+
offset = _pending_surface_offset
|
|
1327
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1328
|
+
if (geometry_feed.backend() == "hyprland" and geometry_feed._hypr_selector() is None
|
|
1329
|
+
and _pending_surface_wait < PENDING_SURFACE_WAIT_FRAMES):
|
|
1330
|
+
# Hyprland resizes the box only through its IPC, addressed by the
|
|
1331
|
+
# window handle the feed reports - before its first poll (the
|
|
1332
|
+
# launch restore, LSDStudio's create-window path) nothing can be
|
|
1333
|
+
# sent, so the request holds for the next frame. Applying it now
|
|
1334
|
+
# spent it on a bare glfw.set_window_size Hyprland ignores.
|
|
1335
|
+
_pending_surface_wait += 1
|
|
1336
|
+
# An app renders on request only (app.py's loop): with nothing
|
|
1337
|
+
# else asking, the held request never got its retry frame and a
|
|
1338
|
+
# @glfw_window stayed at Hyprland's default floating size until the
|
|
1339
|
+
# first hover (09-12). Ask for the frame that retries it.
|
|
1340
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1341
|
+
request_render()
|
|
1342
|
+
return None
|
|
1343
|
+
_pending_surface_wait = 0
|
|
1344
|
+
if _pending_surface_fit and geometry_feed.backend() == "hyprland":
|
|
1345
|
+
rect, area = geometry_feed.frame_rect(), geometry_feed.workarea()
|
|
1346
|
+
if rect is not None and area is not None:
|
|
1347
|
+
inset = int(window_inset())
|
|
1348
|
+
if geometry_feed.hypr_honors_geometry():
|
|
1349
|
+
# the feed's rect and the box are the CONTENT: fit the surface less its margin
|
|
1350
|
+
dx, dy = fit_offset(rect, (size[0] - 2 * inset, size[1] - 2 * inset), area, 0)
|
|
1351
|
+
else:
|
|
1352
|
+
dx, dy = fit_offset(rect, size, area, inset)
|
|
1353
|
+
if dx or dy:
|
|
1354
|
+
ox, oy = offset or (0, 0)
|
|
1355
|
+
offset = (ox + dx, oy + dy)
|
|
1356
|
+
os_frame.expect_own_move(dx, dy) # ours, not the compositor's
|
|
1357
|
+
_pending_surface_fit = False
|
|
1358
|
+
_pending_surface_size = None
|
|
1359
|
+
_pending_surface_offset = None
|
|
1360
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1361
|
+
if Toggles.Melty.push_os_window_edges_trace:
|
|
1362
|
+
try:
|
|
1363
|
+
was = glfw.get_framebuffer_size(window)
|
|
1364
|
+
except Exception:
|
|
1365
|
+
was = None
|
|
1366
|
+
print(f"[os_frame] apply pending surface size {size} (was {was}) offset {offset}")
|
|
1367
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
1368
|
+
on_hyprland = _on_hyprland()
|
|
1369
|
+
# Hyprland ignores a toplevel's buffer offset (the surface stays
|
|
1370
|
+
# anchored at `at`): the move rides the box resize's own IPC request.
|
|
1371
|
+
set_surface_size(window, *size, offset=offset if on_hyprland else None)
|
|
1372
|
+
# The move rides the same commit: armed HERE, right after GLFW's resize
|
|
1373
|
+
# (which reset the EGL window's offset to 0) and before anything is
|
|
1374
|
+
# drawn. Arming it late - right before the swap - blanked every frame
|
|
1375
|
+
# that carried a move: wl_egl_window_resize flags the EGL surface as
|
|
1376
|
+
# resized, the driver re-creates its swapchain for that swap, and the
|
|
1377
|
+
# frame already rendered on the old buffers was lost (the window
|
|
1378
|
+
# vanished while the hand moved, reappeared at random). At frame start
|
|
1379
|
+
# the driver's resize handling runs BEFORE the frame is drawn, exactly
|
|
1380
|
+
# as for GLFW's own resizes. Nothing else touches the EGL window until
|
|
1381
|
+
# the swap.
|
|
1382
|
+
_frame_surface_offset = None
|
|
1383
|
+
if offset and (offset[0] or offset[1]) and not on_hyprland:
|
|
1384
|
+
wayland_move.set_surface_offset(offset[0], offset[1])
|
|
1385
|
+
return size
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
_last_stamped_size = globals().get("_last_stamped_size")
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def on_surface_resized(window, width, height):
|
|
1392
|
+
"""LSDStudio's framebuffer-size callback hook. With the window geometry
|
|
1393
|
+
inset to the content, a compositor configure (interactive resize from
|
|
1394
|
+
the edge zones, un-maximize, tiling) names a GEOMETRY size — GLFW makes
|
|
1395
|
+
the SURFACE that size, which would shrink the content by twice the
|
|
1396
|
+
margin. Grow the surface back right here, inside the callback, so the
|
|
1397
|
+
frame that follows is already right (a deferred fix would jitter the
|
|
1398
|
+
content edge on every configure of a drag). Our own resizes
|
|
1399
|
+
(set_surface_size) are flagged and pass through; maximized/fullscreen
|
|
1400
|
+
have no margin and pass through too. Returns the size applied."""
|
|
1401
|
+
global _last_surface_size, _last_stamped_size
|
|
1402
|
+
size = (int(width), int(height))
|
|
1403
|
+
if size != _last_stamped_size:
|
|
1404
|
+
# Any backend, ours or the compositor's: stamp the gesture for
|
|
1405
|
+
# freeze_resize views (Melty.resize_gesture_live); the cache keeps
|
|
1406
|
+
# frames coming past the last configure until they settle.
|
|
1407
|
+
from meltygui.core.melty import Melty
|
|
1408
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1409
|
+
_last_stamped_size = size
|
|
1410
|
+
Melty.os_resize_time = time.monotonic()
|
|
1411
|
+
request_render()
|
|
1412
|
+
if not _on_wayland() or not wayland_move.geometry_available():
|
|
1413
|
+
return None
|
|
1414
|
+
if _self_resize:
|
|
1415
|
+
# Our resize (the right-drag, a compensation): the geometry rides in
|
|
1416
|
+
# the SAME commit as the new buffer. If from draw_titlebar a frame
|
|
1417
|
+
# later it lagged one drag step behind - Mutter pushed the window
|
|
1418
|
+
# up against a stale rect (jitter) and the last step's growth never
|
|
1419
|
+
# got the push (a gap at the top).
|
|
1420
|
+
_last_surface_size = size
|
|
1421
|
+
sync_window_geometry(window, size)
|
|
1422
|
+
return None
|
|
1423
|
+
if size == _last_surface_size:
|
|
1424
|
+
# GLFW re-fired the callback with the size it already had (output
|
|
1425
|
+
# resize, fractional scale): no configure, nothing to regrow.
|
|
1426
|
+
return None
|
|
1427
|
+
_last_surface_size = size
|
|
1428
|
+
inset = window_inset() # MAXIMIZED is already current inside GLFW's configure handling
|
|
1429
|
+
# A compositor-driven size names the GEOMETRY (the content): the
|
|
1430
|
+
# surface is regrown outside it by the margin on every side. The OS
|
|
1431
|
+
# edge model (os_frame.begin_frame) reads the new content size next
|
|
1432
|
+
# frame and folds it into the roots' passes as the near edge's motion.
|
|
1433
|
+
# Hyprland's configure names the SURFACE (the box): never regrow.
|
|
1434
|
+
if inset <= 0 or _box_is_surface():
|
|
1435
|
+
sync_window_geometry(window, (int(width), int(height)))
|
|
1436
|
+
return None
|
|
1437
|
+
grown = (int(width) + 2 * inset, int(height) + 2 * inset)
|
|
1438
|
+
set_surface_size(window, *grown, box=False) # its nested callback syncs the geometry; the box IS this size
|
|
1439
|
+
return grown
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def sync_window_geometry(window, size=None):
|
|
1443
|
+
"""Wayland: keep xdg_surface.set_window_geometry on the CONTENT rect
|
|
1444
|
+
(the surface minus the shadow margin), the whole surface without one —
|
|
1445
|
+
re-marshalled only when it changes. From on_surface_resized with the
|
|
1446
|
+
new size (same commit as the buffer), and per frame from draw_titlebar
|
|
1447
|
+
as the catch-all (margin toggled, maximize state)."""
|
|
1448
|
+
global _geometry_applied
|
|
1449
|
+
if not _on_wayland() or not wayland_move.geometry_available():
|
|
1450
|
+
return False
|
|
1451
|
+
fb_w, fb_h = size if size is not None else glfw.get_framebuffer_size(window)
|
|
1452
|
+
inset = int(window_inset())
|
|
1453
|
+
if _box_is_surface():
|
|
1454
|
+
# Hyprland renders the surface at the box and ADDS the surface
|
|
1455
|
+
# origin to pointer coordinates (ViewHitTester.cpp) so an inset
|
|
1456
|
+
# geometry shifts every click by the margin. The whole surface.
|
|
1457
|
+
inset = 0
|
|
1458
|
+
rect = (inset, inset, max(1, int(fb_w) - 2 * inset), max(1, int(fb_h) - 2 * inset))
|
|
1459
|
+
if rect == _geometry_applied:
|
|
1460
|
+
return False
|
|
1461
|
+
if wayland_move.set_window_geometry(*rect):
|
|
1462
|
+
_geometry_applied = rect
|
|
1463
|
+
return True
|
|
1464
|
+
return False
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def sync_input_region(window):
|
|
1468
|
+
"""Wayland: keep the surface's input region on the CONTENT rect so
|
|
1469
|
+
clicks in the transparent shadow margin fall through to whatever is
|
|
1470
|
+
behind (wayland_move.set_input_rect); the whole surface when there is
|
|
1471
|
+
no margin. Re-marshalled only when the rect changes."""
|
|
1472
|
+
global _input_rect_applied
|
|
1473
|
+
if not _on_wayland() or not wayland_move.input_region_available():
|
|
1474
|
+
return False
|
|
1475
|
+
inset = window_inset()
|
|
1476
|
+
ox, oy = content_origin()
|
|
1477
|
+
if inset <= 0 and ox == 0 and oy == 0:
|
|
1478
|
+
rect = None
|
|
1479
|
+
else:
|
|
1480
|
+
fb_w, fb_h = glfw.get_framebuffer_size(window)
|
|
1481
|
+
rect = (int(ox), int(oy), max(1, int(fb_w - ox - inset)), max(1, int(fb_h - oy - inset)))
|
|
1482
|
+
if rect == _input_rect_applied:
|
|
1483
|
+
return False
|
|
1484
|
+
if wayland_move.set_input_rect(rect):
|
|
1485
|
+
_input_rect_applied = rect
|
|
1486
|
+
return True
|
|
1487
|
+
return False
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
def composite_window_frame(fb_w, fb_h):
|
|
1491
|
+
"""Last GL work of the frame (Melty.post_frame, after the overlay): the
|
|
1492
|
+
frameless window's ALPHA, one fullscreen pass over the REAL framebuffer
|
|
1493
|
+
(fb_w × fb_h, the content inset window_inset() px) from the content's
|
|
1494
|
+
rounded-rect coverage `cov` (1 inside, 0 outside, a one-pixel ramp).
|
|
1495
|
+
|
|
1496
|
+
With the shadow pass on (Toggles.filters), ShadowComposite has already
|
|
1497
|
+
rewritten everything outside the content as the shadow in premultiplied
|
|
1498
|
+
alpha — the OS window's shadow is that pass, nothing else draws one —
|
|
1499
|
+
so this only lifts the CONTENT's alpha back to 1: imgui's own blend
|
|
1500
|
+
leaves a translucent draw at dst_a = a² + dst_a·(1−a) < 1, through
|
|
1501
|
+
which the desktop would bleed. MAX-blended alpha, rgb untouched.
|
|
1502
|
+
|
|
1503
|
+
Without it (no composite ran) the pass PREMULTIPLIES by the coverage
|
|
1504
|
+
instead: rgb·cov, alpha = cov. Wayland composites premultiplied alpha,
|
|
1505
|
+
so alpha 0 alone is not invisible — a view drawn over a cut corner, or
|
|
1506
|
+
the brightness pass lifting the cleared black, would be ADDED onto the
|
|
1507
|
+
desktop. No-op unless the window was created transparent; it runs
|
|
1508
|
+
maximized too (radius and inset 0), where it is purely the alpha lift."""
|
|
1509
|
+
global _corner_gl
|
|
1510
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1511
|
+
radius = frame_corner_radius()
|
|
1512
|
+
inset = float(window_inset())
|
|
1513
|
+
origin = content_origin()
|
|
1514
|
+
if fb_w <= 0 or fb_h <= 0 or not is_gl_thread():
|
|
1515
|
+
return False
|
|
1516
|
+
if not _frame_transparent(_studio_window()):
|
|
1517
|
+
return False
|
|
1518
|
+
# No early-out on radius/inset/origin all zero (maximized): the window
|
|
1519
|
+
# was created with an alpha channel, so the content's alpha must still
|
|
1520
|
+
# be lifted to 1 - the compositor blends the desktop through anything
|
|
1521
|
+
# below 1 whatever the corners look like.
|
|
1522
|
+
content_size = (float(fb_w) - origin[0] - inset, float(fb_h) - origin[1] - inset)
|
|
1523
|
+
if _corner_gl is None:
|
|
1524
|
+
_corner_gl = GLState()
|
|
1525
|
+
saved_mask = gl.glGetBooleanv(gl.GL_COLOR_WRITEMASK)
|
|
1526
|
+
blend = gl.glIsEnabled(gl.GL_BLEND)
|
|
1527
|
+
blend_func = (int(gl.glGetIntegerv(gl.GL_BLEND_SRC_RGB)), int(gl.glGetIntegerv(gl.GL_BLEND_DST_RGB)),
|
|
1528
|
+
int(gl.glGetIntegerv(gl.GL_BLEND_SRC_ALPHA)), int(gl.glGetIntegerv(gl.GL_BLEND_DST_ALPHA)))
|
|
1529
|
+
blend_eq = (int(gl.glGetIntegerv(gl.GL_BLEND_EQUATION_RGB)), int(gl.glGetIntegerv(gl.GL_BLEND_EQUATION_ALPHA)))
|
|
1530
|
+
scissor = gl.glIsEnabled(gl.GL_SCISSOR_TEST)
|
|
1531
|
+
depth = gl.glIsEnabled(gl.GL_DEPTH_TEST)
|
|
1532
|
+
stencil = gl.glIsEnabled(gl.GL_STENCIL_TEST)
|
|
1533
|
+
try:
|
|
1534
|
+
from meltygui.core.melty import Melty
|
|
1535
|
+
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, Melty.default_framebuffer())
|
|
1536
|
+
gl.glViewport(0, 0, int(fb_w), int(fb_h))
|
|
1537
|
+
gl.glDisable(gl.GL_SCISSOR_TEST)
|
|
1538
|
+
gl.glDisable(gl.GL_DEPTH_TEST)
|
|
1539
|
+
gl.glDisable(gl.GL_STENCIL_TEST)
|
|
1540
|
+
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
|
|
1541
|
+
gl.glEnable(gl.GL_BLEND)
|
|
1542
|
+
if Toggles.filters:
|
|
1543
|
+
# alpha = max(dst_a, cov); rgb = dst
|
|
1544
|
+
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX)
|
|
1545
|
+
gl.glBlendFuncSeparate(gl.GL_ZERO, gl.GL_ONE, gl.GL_ONE, gl.GL_ONE)
|
|
1546
|
+
else:
|
|
1547
|
+
# rgb = 0·src + dst·src_a ; alpha = src_a·1 + dst_a·0
|
|
1548
|
+
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_FUNC_ADD)
|
|
1549
|
+
gl.glBlendFuncSeparate(gl.GL_ZERO, gl.GL_SRC_ALPHA, gl.GL_ONE, gl.GL_ZERO)
|
|
1550
|
+
_corner_alpha_pass(_corner_gl, content_size=content_size, origin=origin, radius=radius)
|
|
1551
|
+
return True
|
|
1552
|
+
finally:
|
|
1553
|
+
gl.glColorMask(*[gl.GL_TRUE if bool(m) else gl.GL_FALSE for m in saved_mask])
|
|
1554
|
+
gl.glBlendFuncSeparate(*blend_func)
|
|
1555
|
+
gl.glBlendEquationSeparate(*blend_eq)
|
|
1556
|
+
(gl.glEnable if blend else gl.glDisable)(gl.GL_BLEND)
|
|
1557
|
+
(gl.glEnable if scissor else gl.glDisable)(gl.GL_SCISSOR_TEST)
|
|
1558
|
+
(gl.glEnable if depth else gl.glDisable)(gl.GL_DEPTH_TEST)
|
|
1559
|
+
(gl.glEnable if stencil else gl.glDisable)(gl.GL_STENCIL_TEST)
|
|
1560
|
+
gl.glBindVertexArray(0)
|