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,932 @@
|
|
|
1
|
+
"""Compositor-driven move / resize for the studio's native-Wayland window.
|
|
2
|
+
|
|
3
|
+
Wayland forbids a client to position itself, and GLFW exposes no route to
|
|
4
|
+
xdg_toplevel.move() — but that request IS the interface: it is what GLFW's
|
|
5
|
+
caption strip, libdecor's title bar and GNOME's Super+drag all send, and the
|
|
6
|
+
compositor then drives the window (snapping, tiling, edge resistance).
|
|
7
|
+
GLFW only hides the xdg_toplevel proxy. This module finds it and speaks the
|
|
8
|
+
protocol through libwayland-client's public marshal API:
|
|
9
|
+
|
|
10
|
+
* the wl_surface GLFW hands out (glfw.get_wayland_window) carries the
|
|
11
|
+
_GLFWwindow as its user data; that struct holds the xdg_toplevel proxy.
|
|
12
|
+
_find_proxy scans it word by word with process_vm_readv (unmapped
|
|
13
|
+
memory reads as a miss, never a fault) for a pointer that reads as a
|
|
14
|
+
wl_object whose interface->name is "xdg_toplevel", then resolves the
|
|
15
|
+
move/resize opcodes BY NAME from that interface's method table — no
|
|
16
|
+
struct offsets, no hardcoded opcodes, so a GLFW or libwayland bump
|
|
17
|
+
degrades to "unavailable", not to a crash.
|
|
18
|
+
* the compositor only honours a move/resize carrying the serial of the
|
|
19
|
+
button press that is still held, and GLFW keeps its serial private, so
|
|
20
|
+
attach() binds this client's OWN wl_seat + wl_pointer on the same
|
|
21
|
+
connection: every wl_pointer of the seat receives the button events,
|
|
22
|
+
the listener records the press serial.
|
|
23
|
+
* the grab swallows the button RELEASE (the X11 path synthesizes one;
|
|
24
|
+
GLFW's Wayland state can't be poked), so begin_* arms a mask that
|
|
25
|
+
button_masked() reports until GLFW itself sees the next real event on
|
|
26
|
+
that button (attach chains a mouse-button callback for that); the
|
|
27
|
+
imgui poll (SplitOverlayRenderer.process_inputs) and the legacy poll
|
|
28
|
+
(LSDStudio.on_mouse) read the mask, titlebar.py feeds the handler its
|
|
29
|
+
synthetic release.
|
|
30
|
+
|
|
31
|
+
Everything runs on the render thread — the thread that dispatches GLFW's
|
|
32
|
+
wl_display. Toggles: Toggles.Melty.wayland_native_frame (libdecor off,
|
|
33
|
+
which is when GLFW owns the xdg_toplevel), Toggles.Melty.move_drag_anywhere.
|
|
34
|
+
Tests: tests/test_wayland_move.py.
|
|
35
|
+
"""
|
|
36
|
+
import ctypes
|
|
37
|
+
import os
|
|
38
|
+
import struct
|
|
39
|
+
|
|
40
|
+
import meltygui.core.windowing.window_api as glfw
|
|
41
|
+
|
|
42
|
+
# Two scopes, both surviving hotswap (module re-exec reuses the dicts).
|
|
43
|
+
#
|
|
44
|
+
# _STATE is the WINDOW's: the proxies GLFW owns for one OS window (its
|
|
45
|
+
# xdg_toplevel, xdg_surface, wl_surface, EGL window) and the per-window
|
|
46
|
+
# request opcodes. surface.py swaps it per Surface (MODULE_GLOBALS), so a
|
|
47
|
+
# meltygui app with multiple windows sees the active window's here.
|
|
48
|
+
#
|
|
49
|
+
# _CONN is the CONNECTION's - one per wl_display, never swapped: the seat
|
|
50
|
+
# and pointer bound for the press serials, the registry, the compositor,
|
|
51
|
+
# the relative pointer, the toplevel-tag manager, the listener tables and
|
|
52
|
+
# the ctypes callbacks they point at (`keep`). Binding these per window was
|
|
53
|
+
# the 09-12 abort on closing one of two windows: each Surface bound its own
|
|
54
|
+
# wl_pointer to a listener table that lived only in that Surface's
|
|
55
|
+
# swapped copy of the state; the Surface was collected, the table freed,
|
|
56
|
+
# and the next pointer.enter on the still-registered proxy hit a NULL slot
|
|
57
|
+
# ("listener function for opcode 0 of wl_pointer is NULL").
|
|
58
|
+
_STATE = globals().get("_STATE") or {
|
|
59
|
+
"attached": False, "window": None, "display": None, "toplevel": None, "surface": None,
|
|
60
|
+
"xdg_surface": None, "opcodes": {}, "prev_button_cb": None, "error": None, "tag": None,
|
|
61
|
+
}
|
|
62
|
+
_CONN = globals().get("_CONN") or {
|
|
63
|
+
"display": None, "registry": None, "seat": None, "pointer": None, "compositor": None,
|
|
64
|
+
"relative_manager": None, "relative_manager_iface": None, "relative_iface": None,
|
|
65
|
+
"relative_pointer": None, "tag_manager": None, "tag_iface": None,
|
|
66
|
+
"rel_x": 0.0, "rel_y": 0.0, "rel_events": 0,
|
|
67
|
+
"press_serial": 0, "grab_serial": 0, "press_button": None, "held": set(),
|
|
68
|
+
"enter_serial": 0, "masked": set(), "keep": [], "pointer_listener": None,
|
|
69
|
+
"opcodes": {}, "error": None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# xdg_toplevel.resize_edge values (xdg-shell.xml).
|
|
73
|
+
EDGE_NONE, EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT = 0, 1, 2, 4
|
|
74
|
+
EDGE_TOP_LEFT, EDGE_BOTTOM_LEFT, EDGE_RIGHT = 5, 6, 8
|
|
75
|
+
EDGE_TOP_RIGHT, EDGE_BOTTOM_RIGHT = 9, 10
|
|
76
|
+
|
|
77
|
+
# wl_pointer.button state
|
|
78
|
+
_BTN_PRESSED = 1
|
|
79
|
+
# wl_proxy_marshal_flags: destroy the request after sending (wl_region one-shots)
|
|
80
|
+
WL_MARSHAL_FLAG_DESTROY = 1
|
|
81
|
+
# Linux evdev button codes → GLFW buttons
|
|
82
|
+
_EVDEV_TO_GLFW = {0x110: glfw.MOUSE_BUTTON_LEFT, 0x111: glfw.MOUSE_BUTTON_RIGHT,
|
|
83
|
+
0x112: glfw.MOUSE_BUTTON_MIDDLE}
|
|
84
|
+
|
|
85
|
+
_SCAN_BYTES = 4096 # _GLFWwindow is ~1-2 KB; heap past it reads fine
|
|
86
|
+
_CHUNK = 256
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _iovec(ctypes.Structure):
|
|
90
|
+
_fields_ = [("iov_base", ctypes.c_void_p), ("iov_len", ctypes.c_size_t)]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _wl_message(ctypes.Structure):
|
|
94
|
+
_fields_ = [("name", ctypes.c_char_p), ("signature", ctypes.c_char_p),
|
|
95
|
+
("types", ctypes.c_void_p)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _wl_interface(ctypes.Structure):
|
|
99
|
+
_fields_ = [("name", ctypes.c_char_p), ("version", ctypes.c_int),
|
|
100
|
+
("method_count", ctypes.c_int), ("methods", ctypes.POINTER(_wl_message)),
|
|
101
|
+
("event_count", ctypes.c_int), ("events", ctypes.POINTER(_wl_message))]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_libc = None
|
|
105
|
+
_wl = None
|
|
106
|
+
_egl = None
|
|
107
|
+
_STATE.setdefault("egl_window", None)
|
|
108
|
+
_STATE.setdefault("offset_armed", False)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _wl_egl_window(ctypes.Structure):
|
|
112
|
+
"""libwayland-egl's struct wl_egl_window (public header, version 3):
|
|
113
|
+
what GLFW hands EGL for the surface. dx/dy are the buffer OFFSET the EGL
|
|
114
|
+
driver puts on the next wl_surface.attach — and for an xdg toplevel the
|
|
115
|
+
compositor applies that offset as a window MOVE (Mutter:
|
|
116
|
+
meta_window_wayland_finish_move_resize, rect.x += dx). The one
|
|
117
|
+
client-initiated move Wayland allows."""
|
|
118
|
+
_fields_ = [("version", ctypes.c_ssize_t),
|
|
119
|
+
("width", ctypes.c_int), ("height", ctypes.c_int),
|
|
120
|
+
("dx", ctypes.c_int), ("dy", ctypes.c_int),
|
|
121
|
+
("attached_width", ctypes.c_int), ("attached_height", ctypes.c_int),
|
|
122
|
+
("driver_private", ctypes.c_void_p),
|
|
123
|
+
("resize_callback", ctypes.c_void_p),
|
|
124
|
+
("destroy_window_callback", ctypes.c_void_p),
|
|
125
|
+
("surface", ctypes.c_void_p)]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_WL_EGL_WINDOW_VERSION = 3
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _egl_lib():
|
|
132
|
+
global _egl
|
|
133
|
+
if _egl is None:
|
|
134
|
+
_egl = ctypes.CDLL("libwayland-egl.so.1")
|
|
135
|
+
_egl.wl_egl_window_resize.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int,
|
|
136
|
+
ctypes.c_int, ctypes.c_int]
|
|
137
|
+
_egl.wl_egl_window_resize.restype = None
|
|
138
|
+
return _egl
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _find_egl_window(base, surface, span=_SCAN_BYTES):
|
|
142
|
+
"""The wl_egl_window in the _GLFWwindow struct at ``base``: a pointer
|
|
143
|
+
to a struct whose version word is WL_EGL_WINDOW_VERSION, whose size is
|
|
144
|
+
sane and whose surface field is OUR wl_surface."""
|
|
145
|
+
if not base or not surface:
|
|
146
|
+
return None
|
|
147
|
+
size = ctypes.sizeof(_wl_egl_window)
|
|
148
|
+
for off in range(0, span, _CHUNK):
|
|
149
|
+
chunk = _read(base + off, _CHUNK)
|
|
150
|
+
if chunk is None:
|
|
151
|
+
return None
|
|
152
|
+
for i in range(0, _CHUNK, 8):
|
|
153
|
+
cand = struct.unpack_from("<Q", chunk, i)[0]
|
|
154
|
+
if not cand or cand & 7:
|
|
155
|
+
continue
|
|
156
|
+
raw = _read(cand, size)
|
|
157
|
+
if raw is None:
|
|
158
|
+
continue
|
|
159
|
+
win = _wl_egl_window.from_buffer_copy(raw)
|
|
160
|
+
if (win.version == _WL_EGL_WINDOW_VERSION and win.surface == surface
|
|
161
|
+
and 0 < win.width < 65536 and 0 < win.height < 65536):
|
|
162
|
+
return cand
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _ensure_egl_window():
|
|
167
|
+
"""The EGL window, found at attach — or here, lazily, after a hotswap
|
|
168
|
+
of this module (attach runs once at boot; the surface is kept). A failed
|
|
169
|
+
scan is remembered as 0 so it isn't repeated every frame."""
|
|
170
|
+
win = _STATE["egl_window"]
|
|
171
|
+
if win is None and _STATE.get("surface"):
|
|
172
|
+
try:
|
|
173
|
+
_, wl = _c()
|
|
174
|
+
glfw_window = wl.wl_proxy_get_user_data(_STATE["surface"])
|
|
175
|
+
win = _find_egl_window(glfw_window, _STATE["surface"]) or 0
|
|
176
|
+
except Exception:
|
|
177
|
+
win = 0
|
|
178
|
+
_STATE["egl_window"] = win
|
|
179
|
+
return win or None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def offset_available():
|
|
183
|
+
"""Can the surface carry a buffer offset (a client-side window move)?"""
|
|
184
|
+
return bool(_ensure_egl_window())
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def set_surface_offset(dx, dy):
|
|
188
|
+
"""Arm the buffer offset for the NEXT swap: wl_egl_window_resize with
|
|
189
|
+
the window's current size and (dx, dy) — the EGL driver's resize
|
|
190
|
+
callback records them and the swap's wl_surface.attach carries them,
|
|
191
|
+
which the compositor applies as a move of the window by (dx, dy). Call
|
|
192
|
+
right before the swap, AFTER any GLFW resize of the frame (GLFW's own
|
|
193
|
+
wl_egl_window_resize resets the offset to 0). Returns True when armed."""
|
|
194
|
+
win = _ensure_egl_window()
|
|
195
|
+
if not win:
|
|
196
|
+
return False
|
|
197
|
+
raw = _read(win, ctypes.sizeof(_wl_egl_window))
|
|
198
|
+
if raw is None:
|
|
199
|
+
_STATE["egl_window"] = None
|
|
200
|
+
return False
|
|
201
|
+
cur = _wl_egl_window.from_buffer_copy(raw)
|
|
202
|
+
_egl_lib().wl_egl_window_resize(win, cur.width, cur.height, int(dx), int(dy))
|
|
203
|
+
_STATE["offset_armed"] = bool(dx or dy)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def clear_surface_offset():
|
|
208
|
+
"""Next frame's start: an armed offset is one-shot on the driver side
|
|
209
|
+
(Mesa zeroes it after the attach) but the struct keeps the values —
|
|
210
|
+
reset them so no later attach can carry it again."""
|
|
211
|
+
if _STATE["offset_armed"]:
|
|
212
|
+
set_surface_offset(0, 0)
|
|
213
|
+
_STATE["offset_armed"] = False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _c():
|
|
217
|
+
global _libc, _wl
|
|
218
|
+
if _libc is None:
|
|
219
|
+
_libc = ctypes.CDLL(None, use_errno=True)
|
|
220
|
+
_libc.process_vm_readv.argtypes = [ctypes.c_int, ctypes.POINTER(_iovec), ctypes.c_ulong,
|
|
221
|
+
ctypes.POINTER(_iovec), ctypes.c_ulong, ctypes.c_ulong]
|
|
222
|
+
_libc.process_vm_readv.restype = ctypes.c_ssize_t
|
|
223
|
+
if _wl is None:
|
|
224
|
+
# The same soname GLFW dlopens, so this is the library its proxies
|
|
225
|
+
# live in (dlopen of an already-loaded library returns its handle).
|
|
226
|
+
_wl = ctypes.CDLL("libwayland-client.so.0")
|
|
227
|
+
_wl.wl_proxy_get_user_data.argtypes = [ctypes.c_void_p]
|
|
228
|
+
_wl.wl_proxy_get_user_data.restype = ctypes.c_void_p
|
|
229
|
+
_wl.wl_proxy_get_class.argtypes = [ctypes.c_void_p]
|
|
230
|
+
_wl.wl_proxy_get_class.restype = ctypes.c_char_p
|
|
231
|
+
_wl.wl_proxy_get_version.argtypes = [ctypes.c_void_p]
|
|
232
|
+
_wl.wl_proxy_get_version.restype = ctypes.c_uint32
|
|
233
|
+
_wl.wl_proxy_add_listener.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
|
234
|
+
_wl.wl_proxy_add_listener.restype = ctypes.c_int
|
|
235
|
+
# variadic: the static prefix is typed, the variable args passed as
|
|
236
|
+
# explicit ctypes instances (ints/pointers only: SysV passes them
|
|
237
|
+
# in registers exactly as the generated C stubs do)
|
|
238
|
+
_wl.wl_proxy_marshal_flags.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
|
|
239
|
+
ctypes.c_uint32, ctypes.c_uint32]
|
|
240
|
+
_wl.wl_proxy_marshal_flags.restype = ctypes.c_void_p
|
|
241
|
+
_wl.wl_display_roundtrip.argtypes = [ctypes.c_void_p]
|
|
242
|
+
_wl.wl_display_roundtrip.restype = ctypes.c_int
|
|
243
|
+
_wl.wl_display_flush.argtypes = [ctypes.c_void_p]
|
|
244
|
+
_wl.wl_display_flush.restype = ctypes.c_int
|
|
245
|
+
return _libc, _wl
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Safe memory reads + the proxy scan
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def _iface_addr(wl, name):
|
|
253
|
+
"""Address of one of libwayland-client's exported wl_interface structs."""
|
|
254
|
+
return ctypes.addressof(ctypes.c_char.in_dll(wl, name))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _read(addr, n):
|
|
258
|
+
"""n bytes at addr, or None when any of it is unmapped (EFAULT) — a
|
|
259
|
+
syscall copy, never a dereference, so a stale pointer can't fault."""
|
|
260
|
+
if not addr or n <= 0:
|
|
261
|
+
return None
|
|
262
|
+
libc, _ = _c()
|
|
263
|
+
buf = ctypes.create_string_buffer(n)
|
|
264
|
+
local = _iovec(ctypes.addressof(buf), n)
|
|
265
|
+
remote = _iovec(addr, n)
|
|
266
|
+
got = libc.process_vm_readv(os.getpid(), ctypes.byref(local), 1, ctypes.byref(remote), 1, 0)
|
|
267
|
+
return buf.raw if got == n else None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _read_word(addr):
|
|
271
|
+
raw = _read(addr, 8)
|
|
272
|
+
return struct.unpack("<Q", raw)[0] if raw else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _read_cstr(addr, cap=48):
|
|
276
|
+
raw = _read(addr, cap)
|
|
277
|
+
if raw is None:
|
|
278
|
+
# The string may sit right before an unmapped page: shrink
|
|
279
|
+
for n in (32, 16, 8):
|
|
280
|
+
raw = _read(addr, n)
|
|
281
|
+
if raw is not None:
|
|
282
|
+
break
|
|
283
|
+
return raw.split(b"\0", 1)[0] if raw else None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _proxy_class(ptr):
|
|
287
|
+
"""Interface name of a wl_proxy candidate, read defensively: wl_proxy
|
|
288
|
+
begins with wl_object {const wl_interface *interface; ...} and
|
|
289
|
+
wl_interface begins with {const char *name; ...}."""
|
|
290
|
+
if not ptr or ptr & 7:
|
|
291
|
+
return None
|
|
292
|
+
iface = _read_word(ptr)
|
|
293
|
+
if not iface or iface & 7:
|
|
294
|
+
return None
|
|
295
|
+
name_ptr = _read_word(iface)
|
|
296
|
+
if not name_ptr:
|
|
297
|
+
return None
|
|
298
|
+
return _read_cstr(name_ptr)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _find_proxy(base, class_name, span=_SCAN_BYTES):
|
|
302
|
+
"""First pointer-sized word in [base, base+span) that reads as a
|
|
303
|
+
wl_proxy of interface `class_name` (bytes). Chunked so a struct near the
|
|
304
|
+
end of a mapping still scans up to the edge."""
|
|
305
|
+
if not base:
|
|
306
|
+
return None
|
|
307
|
+
for off in range(0, span, _CHUNK):
|
|
308
|
+
chunk = _read(base + off, _CHUNK)
|
|
309
|
+
if chunk is None:
|
|
310
|
+
return None
|
|
311
|
+
for i in range(0, _CHUNK, 8):
|
|
312
|
+
cand = struct.unpack_from("<Q", chunk, i)[0]
|
|
313
|
+
if cand and not (cand & 7) and _proxy_class(cand) == class_name:
|
|
314
|
+
return cand
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _opcodes(proxy, names):
|
|
319
|
+
"""{name: opcode} for the proxy's requests, resolved from its interface's
|
|
320
|
+
method table (the compiled xdg_toplevel_interface inside GLFW)."""
|
|
321
|
+
iface_ptr = _read_word(proxy)
|
|
322
|
+
raw = _read(iface_ptr, ctypes.sizeof(_wl_interface))
|
|
323
|
+
if raw is None:
|
|
324
|
+
return {}
|
|
325
|
+
return _opcodes_of(_wl_interface.from_buffer_copy(raw), names)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _opcodes_of(iface, names):
|
|
329
|
+
"""{name: opcode} from a wl_interface struct (a proxy's, read defensively,
|
|
330
|
+
or one of libwayland's exported ones like wl_region_interface)."""
|
|
331
|
+
count = iface.method_count
|
|
332
|
+
methods_ptr = ctypes.cast(iface.methods, ctypes.c_void_p).value
|
|
333
|
+
out = {}
|
|
334
|
+
size = ctypes.sizeof(_wl_message)
|
|
335
|
+
for op in range(max(0, min(count, 64))):
|
|
336
|
+
mraw = _read(methods_ptr + op * size, size)
|
|
337
|
+
if mraw is None:
|
|
338
|
+
break
|
|
339
|
+
name_ptr = struct.unpack_from("<Q", mraw, 0)[0]
|
|
340
|
+
name = _read_cstr(name_ptr)
|
|
341
|
+
if name in names:
|
|
342
|
+
out[name.decode()] = op
|
|
343
|
+
return out
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# zwp_relative_pointer_v1, built by hand (not in libwayland-client): raw
|
|
348
|
+
# pointer motion in SCREEN space. A surface-drag reads the pointer in
|
|
349
|
+
# surface coordinates, and when the compositor moves the surface under it
|
|
350
|
+
# (its keep-on-screen push once the bottom edge leaves the screen) the
|
|
351
|
+
# surface-relative pointer jumps by the same amount - the drag read that
|
|
352
|
+
# as further movement, grew the window more, the compositor pushed again: a
|
|
353
|
+
# feedback loop that ran the top edge straight up to the screen edge.
|
|
354
|
+
# relative pointer is unaffected by where the surface is.
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
def _wl_message_array(entries, keep):
|
|
358
|
+
"""A wl_message[] from (name, signature, [interface addr or None, ...])."""
|
|
359
|
+
arr = (_wl_message * max(1, len(entries)))()
|
|
360
|
+
for i, (name, signature, types) in enumerate(entries):
|
|
361
|
+
types_arr = (ctypes.c_void_p * max(1, len(types)))(*[t for t in types])
|
|
362
|
+
keep.append(types_arr)
|
|
363
|
+
arr[i].name = name
|
|
364
|
+
arr[i].signature = signature
|
|
365
|
+
arr[i].types = ctypes.addressof(types_arr)
|
|
366
|
+
keep.append(arr)
|
|
367
|
+
return arr
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _build_relative_pointer_interfaces(wl, keep):
|
|
371
|
+
"""The two wl_interface structs of relative-pointer-unstable-v1, kept
|
|
372
|
+
alive for the proxies' lifetime (libwayland stores the pointers)."""
|
|
373
|
+
rel = _wl_interface()
|
|
374
|
+
manager = _wl_interface()
|
|
375
|
+
keep.extend([rel, manager])
|
|
376
|
+
rel_methods = _wl_message_array([(b"destroy", b"", [])], keep)
|
|
377
|
+
rel_events = _wl_message_array([(b"relative_motion", b"uuffff", [None] * 6)], keep)
|
|
378
|
+
rel.name = b"zwp_relative_pointer_v1"
|
|
379
|
+
rel.version = 1
|
|
380
|
+
rel.method_count = 1
|
|
381
|
+
rel.methods = ctypes.cast(rel_methods, ctypes.POINTER(_wl_message))
|
|
382
|
+
rel.event_count = 1
|
|
383
|
+
rel.events = ctypes.cast(rel_events, ctypes.POINTER(_wl_message))
|
|
384
|
+
mgr_methods = _wl_message_array([
|
|
385
|
+
(b"destroy", b"", []),
|
|
386
|
+
(b"get_relative_pointer", b"no", [ctypes.addressof(rel), _iface_addr(wl, "wl_pointer_interface")]),
|
|
387
|
+
], keep)
|
|
388
|
+
manager.name = b"zwp_relative_pointer_manager_v1"
|
|
389
|
+
manager.version = 1
|
|
390
|
+
manager.method_count = 2
|
|
391
|
+
manager.methods = ctypes.cast(mgr_methods, ctypes.POINTER(_wl_message))
|
|
392
|
+
manager.event_count = 0
|
|
393
|
+
manager.events = ctypes.cast(_wl_message_array([], keep), ctypes.POINTER(_wl_message))
|
|
394
|
+
return manager, rel
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
_RELATIVE_MOTION_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
|
|
398
|
+
ctypes.c_uint32, ctypes.c_int32, ctypes.c_int32,
|
|
399
|
+
ctypes.c_int32, ctypes.c_int32)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _on_relative_motion(data, pointer, utime_hi, utime_lo, dx, dy, dx_unaccel, dy_unaccel):
|
|
403
|
+
# wl_fixed_t: 24.8 fixed point. dx/dy are the coordinate deltas - the
|
|
404
|
+
# pointer's on-screen motion.
|
|
405
|
+
_CONN["rel_x"] += int(dx) / 256.0
|
|
406
|
+
_CONN["rel_y"] += int(dy) / 256.0
|
|
407
|
+
_CONN["rel_events"] += 1
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _setup_relative_pointer():
|
|
411
|
+
"""After the registry roundtrip: a relative pointer for our wl_pointer."""
|
|
412
|
+
manager, pointer = _CONN["relative_manager"], _CONN["pointer"]
|
|
413
|
+
if not manager or not pointer or _CONN["relative_pointer"]:
|
|
414
|
+
return False
|
|
415
|
+
_, wl = _c()
|
|
416
|
+
rel_iface = _CONN["relative_iface"]
|
|
417
|
+
# get_relative_pointer(new_id, wl_pointer) - "no"
|
|
418
|
+
rel = wl.wl_proxy_marshal_flags(manager, 1, ctypes.addressof(rel_iface),
|
|
419
|
+
wl.wl_proxy_get_version(manager), 0,
|
|
420
|
+
ctypes.c_void_p(None), ctypes.c_void_p(pointer))
|
|
421
|
+
if not rel:
|
|
422
|
+
return False
|
|
423
|
+
cb = _RELATIVE_MOTION_CB(_on_relative_motion)
|
|
424
|
+
table = (ctypes.c_void_p * 1)(ctypes.cast(cb, ctypes.c_void_p).value)
|
|
425
|
+
_CONN["keep"].extend([cb, table])
|
|
426
|
+
wl.wl_proxy_add_listener(rel, ctypes.addressof(table), None)
|
|
427
|
+
_CONN["relative_pointer"] = rel
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def relative_motion_available():
|
|
432
|
+
"""Whether this connection has received relative motion, not just bound it.
|
|
433
|
+
|
|
434
|
+
An agent seat can advertise the protocol and accept the subscription
|
|
435
|
+
while delivering only absolute wl_pointer motion. Treating its untouched
|
|
436
|
+
totals as a stationary hand makes surface-slide compensation erase every
|
|
437
|
+
drag. Wait for an actual relative event before using these totals.
|
|
438
|
+
"""
|
|
439
|
+
return bool(_CONN["relative_pointer"]) and _CONN["rel_events"] > 0
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def relative_motion_total():
|
|
443
|
+
"""Accumulated screen-space pointer motion (px) since attach — an
|
|
444
|
+
arbitrary origin; gestures latch a value and use the difference."""
|
|
445
|
+
return _CONN["rel_x"], _CONN["rel_y"]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# xdg_toplevel_tag_v1, built by hand: a stable per-window TAG not an
|
|
450
|
+
# app id. A compositor that remembers floating sizes / positions keys on
|
|
451
|
+
# app id + tag (Lukas's Hyprland: `persistent_size`, ignore_title on), so
|
|
452
|
+
# without a tag every window of one app shared ONE remembered box and two
|
|
453
|
+
# @glfw_window roots opened on top of each other, each at the other's center.
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
def _build_toplevel_tag_interface(keep):
|
|
457
|
+
"""The xdg_toplevel_tag_manager_v1 wl_interface (xdg-toplevel-tag-v1.xml:
|
|
458
|
+
destroy, set_toplevel_tag(toplevel, tag), set_toplevel_description)."""
|
|
459
|
+
manager = _wl_interface()
|
|
460
|
+
keep.append(manager)
|
|
461
|
+
methods = _wl_message_array([
|
|
462
|
+
(b"destroy", b"", []),
|
|
463
|
+
(b"set_toplevel_tag", b"os", [None, None]),
|
|
464
|
+
(b"set_toplevel_description", b"os", [None, None]),
|
|
465
|
+
], keep)
|
|
466
|
+
manager.name = b"xdg_toplevel_tag_manager_v1"
|
|
467
|
+
manager.version = 1
|
|
468
|
+
manager.method_count = 3
|
|
469
|
+
manager.methods = ctypes.cast(methods, ctypes.POINTER(_wl_message))
|
|
470
|
+
manager.event_count = 0
|
|
471
|
+
manager.events = ctypes.cast(_wl_message_array([], keep), ctypes.POINTER(_wl_message))
|
|
472
|
+
return manager
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def tag_available():
|
|
476
|
+
return bool(_CONN["tag_manager"])
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def set_toplevel_tag(toplevel, tag):
|
|
480
|
+
"""xdg_toplevel_tag_manager_v1.set_toplevel_tag(toplevel, tag): the
|
|
481
|
+
window's stable identity for the compositor (an untranslated string
|
|
482
|
+
like the @glfw_window name). True when the request went out."""
|
|
483
|
+
manager = _CONN["tag_manager"]
|
|
484
|
+
if not manager or not toplevel or not tag:
|
|
485
|
+
return False
|
|
486
|
+
_, wl = _c()
|
|
487
|
+
wl.wl_proxy_marshal_flags(manager, 1, None, wl.wl_proxy_get_version(manager), 0,
|
|
488
|
+
ctypes.c_void_p(toplevel), ctypes.c_char_p(str(tag).encode()))
|
|
489
|
+
if _CONN["display"]:
|
|
490
|
+
wl.wl_display_flush(_CONN["display"])
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
# Own seat + pointer: the press serial
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
_GLOBAL_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
|
|
499
|
+
ctypes.c_char_p, ctypes.c_uint32)
|
|
500
|
+
_GLOBAL_REMOVE_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32)
|
|
501
|
+
_ENTER_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
|
|
502
|
+
ctypes.c_void_p, ctypes.c_int32, ctypes.c_int32)
|
|
503
|
+
_LEAVE_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p)
|
|
504
|
+
_BUTTON_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
|
|
505
|
+
ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32)
|
|
506
|
+
_ANY_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
|
|
507
|
+
ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _on_global(data, registry, name, interface, version):
|
|
511
|
+
if interface == b"zwp_relative_pointer_manager_v1" and _CONN["relative_manager"] is None:
|
|
512
|
+
_, wl = _c()
|
|
513
|
+
manager_iface = _CONN.get("relative_manager_iface")
|
|
514
|
+
if manager_iface is not None:
|
|
515
|
+
_CONN["relative_manager"] = wl.wl_proxy_marshal_flags(
|
|
516
|
+
registry, _CONN["opcodes"]["bind"], ctypes.addressof(manager_iface), 1, 0,
|
|
517
|
+
ctypes.c_uint32(name), ctypes.c_char_p(b"zwp_relative_pointer_manager_v1"),
|
|
518
|
+
ctypes.c_uint32(1), ctypes.c_void_p(None))
|
|
519
|
+
return
|
|
520
|
+
if interface == b"xdg_toplevel_tag_manager_v1" and _CONN["tag_manager"] is None:
|
|
521
|
+
_, wl = _c()
|
|
522
|
+
tag_iface = _CONN.get("tag_iface")
|
|
523
|
+
if tag_iface is not None:
|
|
524
|
+
_CONN["tag_manager"] = wl.wl_proxy_marshal_flags(
|
|
525
|
+
registry, _CONN["opcodes"]["bind"], ctypes.addressof(tag_iface), 1, 0,
|
|
526
|
+
ctypes.c_uint32(name), ctypes.c_char_p(b"xdg_toplevel_tag_manager_v1"),
|
|
527
|
+
ctypes.c_uint32(1), ctypes.c_void_p(None))
|
|
528
|
+
return
|
|
529
|
+
if interface == b"wl_compositor" and _CONN["compositor"] is None:
|
|
530
|
+
_, wl = _c()
|
|
531
|
+
comp_iface = _iface_addr(wl, "wl_compositor_interface")
|
|
532
|
+
ver = min(int(version), 4)
|
|
533
|
+
_CONN["compositor"] = wl.wl_proxy_marshal_flags(
|
|
534
|
+
registry, _CONN["opcodes"]["bind"], comp_iface, ver, 0,
|
|
535
|
+
ctypes.c_uint32(name), ctypes.c_char_p(b"wl_compositor"),
|
|
536
|
+
ctypes.c_uint32(ver), ctypes.c_void_p(None))
|
|
537
|
+
return
|
|
538
|
+
if interface == b"wl_seat" and _CONN["seat"] is None:
|
|
539
|
+
_, wl = _c()
|
|
540
|
+
seat_iface = _iface_addr(wl, "wl_seat_interface")
|
|
541
|
+
ver = min(int(version), 5)
|
|
542
|
+
# wl_registry.bind(name, interface, version, new_id) - "usun"
|
|
543
|
+
seat = wl.wl_proxy_marshal_flags(registry, _CONN["opcodes"]["bind"], seat_iface, ver, 0,
|
|
544
|
+
ctypes.c_uint32(name), ctypes.c_char_p(b"wl_seat"),
|
|
545
|
+
ctypes.c_uint32(ver), ctypes.c_void_p(None))
|
|
546
|
+
_CONN["seat"] = seat
|
|
547
|
+
if seat:
|
|
548
|
+
pointer_iface = _iface_addr(wl, "wl_pointer_interface")
|
|
549
|
+
# wl_seat.get_pointer(new_id) - "n", opcode 0
|
|
550
|
+
ptr = wl.wl_proxy_marshal_flags(seat, 0, pointer_iface, ver, 0, ctypes.c_void_p(None))
|
|
551
|
+
_CONN["pointer"] = ptr
|
|
552
|
+
if ptr:
|
|
553
|
+
wl.wl_proxy_add_listener(ptr, _CONN["pointer_listener"], None)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _on_global_remove(data, registry, name):
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _on_enter(data, pointer, serial, surface, sx, sy):
|
|
561
|
+
_CONN["enter_serial"] = int(serial)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _on_leave(data, pointer, serial, surface):
|
|
565
|
+
# Under an exclusive grab the compositor sends no leave until the release,
|
|
566
|
+
# so a leave with buttons still held means the grab was broken (a freeze
|
|
567
|
+
# tripped Mutter's not-responding handling, a focus steal): the release
|
|
568
|
+
# will be delivered elsewhere and GLFW's button state stays PRESS for
|
|
569
|
+
# good. Mark the buttons as up, and mask GLFW's stale reading until
|
|
570
|
+
# its next real event on them (note_glfw_button).
|
|
571
|
+
held = _CONN["held"]
|
|
572
|
+
if held:
|
|
573
|
+
_CONN["masked"].update(held)
|
|
574
|
+
held.clear()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _on_button(data, pointer, serial, time_ms, button, state):
|
|
578
|
+
glfw_button = _EVDEV_TO_GLFW.get(int(button), int(button))
|
|
579
|
+
if int(state) == _BTN_PRESSED:
|
|
580
|
+
if not _CONN["held"]:
|
|
581
|
+
# First button in a button sequence: the compositor's pointer
|
|
582
|
+
# grab starts here. A chord's second button gets its own serial.
|
|
583
|
+
_CONN["grab_serial"] = int(serial)
|
|
584
|
+
_CONN["press_serial"] = int(serial)
|
|
585
|
+
_CONN["press_button"] = glfw_button
|
|
586
|
+
_CONN["held"].add(glfw_button)
|
|
587
|
+
else:
|
|
588
|
+
_CONN["held"].discard(glfw_button)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _on_any(*args):
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _build_listeners():
|
|
596
|
+
"""Function tables sized to the interfaces' event counts, every slot
|
|
597
|
+
filled (libwayland invokes the slot for any event that arrives)."""
|
|
598
|
+
_, wl = _c()
|
|
599
|
+
reg_iface = _wl_interface.in_dll(wl, "wl_registry_interface")
|
|
600
|
+
ptr_iface = _wl_interface.in_dll(wl, "wl_pointer_interface")
|
|
601
|
+
keep = _CONN["keep"]
|
|
602
|
+
reg_fns = [_GLOBAL_CB(_on_global), _GLOBAL_REMOVE_CB(_on_global_remove)]
|
|
603
|
+
while len(reg_fns) < reg_iface.event_count:
|
|
604
|
+
reg_fns.append(_ANY_CB(_on_any))
|
|
605
|
+
ptr_fns = [_ENTER_CB(_on_enter), _LEAVE_CB(_on_leave), _ANY_CB(_on_any), _BUTTON_CB(_on_button)]
|
|
606
|
+
while len(ptr_fns) < ptr_iface.event_count:
|
|
607
|
+
ptr_fns.append(_ANY_CB(_on_any))
|
|
608
|
+
keep.extend(reg_fns + ptr_fns)
|
|
609
|
+
reg_tab = (ctypes.c_void_p * len(reg_fns))(*[ctypes.cast(f, ctypes.c_void_p).value for f in reg_fns])
|
|
610
|
+
ptr_tab = (ctypes.c_void_p * len(ptr_fns))(*[ctypes.cast(f, ctypes.c_void_p).value for f in ptr_fns])
|
|
611
|
+
keep.extend([reg_tab, ptr_tab])
|
|
612
|
+
return ctypes.addressof(reg_tab), ctypes.addressof(ptr_tab)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
# Public surface
|
|
617
|
+
# ---------------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
def _bind_connection(display):
|
|
620
|
+
"""Once per wl_display: the registry, and through it the seat + pointer
|
|
621
|
+
(press serials), the compositor (input regions), the relative pointer
|
|
622
|
+
manager (screen-space right-drag motion) and the toplevel-tag manager.
|
|
623
|
+
Every window of the process shares these. False with the reason in
|
|
624
|
+
_CONN["error"] when the seat is missing."""
|
|
625
|
+
_, wl = _c()
|
|
626
|
+
if _CONN["display"] != display:
|
|
627
|
+
# A new connection (new GLFW window): the old seat/pointer/registry
|
|
628
|
+
# proxies are dangling - never marshal on them again.
|
|
629
|
+
_CONN.update(seat=None, pointer=None, registry=None, compositor=None, press_serial=0,
|
|
630
|
+
grab_serial=0, press_button=None, held=set(), enter_serial=0,
|
|
631
|
+
relative_manager=None, relative_pointer=None, tag_manager=None,
|
|
632
|
+
opcodes={}, error=None)
|
|
633
|
+
_CONN["masked"].clear()
|
|
634
|
+
_CONN["display"] = display
|
|
635
|
+
if _CONN["registry"]:
|
|
636
|
+
return bool(_CONN["pointer"])
|
|
637
|
+
keep = _CONN["keep"]
|
|
638
|
+
if _CONN.get("relative_manager_iface") is None:
|
|
639
|
+
manager_iface, rel_iface = _build_relative_pointer_interfaces(wl, keep)
|
|
640
|
+
_CONN["relative_manager_iface"] = manager_iface
|
|
641
|
+
_CONN["relative_iface"] = rel_iface
|
|
642
|
+
if _CONN.get("tag_iface") is None:
|
|
643
|
+
_CONN["tag_iface"] = _build_toplevel_tag_interface(keep)
|
|
644
|
+
reg_tab, ptr_tab = _build_listeners()
|
|
645
|
+
_CONN["pointer_listener"] = ptr_tab
|
|
646
|
+
# registry: wl_display.get_registry (opcode 1, "n"); bind is opcode 0
|
|
647
|
+
_CONN["opcodes"]["bind"] = 0
|
|
648
|
+
reg_iface = _iface_addr(wl, "wl_registry_interface")
|
|
649
|
+
registry = wl.wl_proxy_marshal_flags(display, 1, reg_iface, wl.wl_proxy_get_version(display), 0,
|
|
650
|
+
ctypes.c_void_p(None))
|
|
651
|
+
if not registry:
|
|
652
|
+
_CONN["error"] = "wl_display.get_registry failed"
|
|
653
|
+
return False
|
|
654
|
+
_CONN["registry"] = registry
|
|
655
|
+
wl.wl_proxy_add_listener(registry, reg_tab, None)
|
|
656
|
+
wl.wl_display_roundtrip(display) # globals → seat → pointer, compositor, managers
|
|
657
|
+
if not _CONN["pointer"]:
|
|
658
|
+
_CONN["error"] = "no wl_seat advertised"
|
|
659
|
+
return False
|
|
660
|
+
_setup_relative_pointer() # screen-space motion for the right-drag
|
|
661
|
+
# Input region plumbing (set_input_rect): wl_compositor.create_region,
|
|
662
|
+
# wl_region.add/destroy - by name.
|
|
663
|
+
if _CONN["compositor"]:
|
|
664
|
+
_CONN["opcodes"].update(_opcodes(_CONN["compositor"], {b"create_region"}))
|
|
665
|
+
region_iface = _wl_interface.in_dll(wl, "wl_region_interface")
|
|
666
|
+
_CONN["opcodes"].update({"region_" + k: v for k, v in
|
|
667
|
+
_opcodes_of(region_iface, {b"add", b"destroy"}).items()})
|
|
668
|
+
_CONN["error"] = None
|
|
669
|
+
return True
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def attach(window, tag=None):
|
|
673
|
+
"""Find the window's xdg_toplevel (+ xdg_surface, EGL window), bind the
|
|
674
|
+
connection's seat + pointer for serials (once per display) and chain a
|
|
675
|
+
GLFW mouse-button callback that clears the post-grab mask. Render
|
|
676
|
+
thread, once per window, AFTER Melty.init_input_backend (so the chain
|
|
677
|
+
runs first). ``tag`` (a stable string, the @glfw_window name) is set as
|
|
678
|
+
the toplevel's xdg tag when the compositor offers the protocol.
|
|
679
|
+
Returns True when moves are available; the reason for a False sits in
|
|
680
|
+
last_error()."""
|
|
681
|
+
if _STATE["attached"] and _STATE["window"] == window:
|
|
682
|
+
return available()
|
|
683
|
+
_STATE["attached"] = True
|
|
684
|
+
_STATE["window"] = window
|
|
685
|
+
_STATE["tag"] = tag
|
|
686
|
+
try:
|
|
687
|
+
if glfw.get_platform() != glfw.PLATFORM_WAYLAND:
|
|
688
|
+
_STATE["error"] = "not a Wayland session"
|
|
689
|
+
return False
|
|
690
|
+
_, wl = _c()
|
|
691
|
+
display = glfw.get_wayland_display()
|
|
692
|
+
surface = glfw.get_wayland_window(window)
|
|
693
|
+
if not display or not surface:
|
|
694
|
+
_STATE["error"] = "GLFW exposes no Wayland display/surface"
|
|
695
|
+
return False
|
|
696
|
+
_STATE["display"] = display
|
|
697
|
+
native_window = glfw.is_native_window(window)
|
|
698
|
+
glfw_window = None if native_window else wl.wl_proxy_get_user_data(surface)
|
|
699
|
+
toplevel = window.toplevel if native_window else _find_proxy(glfw_window, b"xdg_toplevel")
|
|
700
|
+
if not toplevel or wl.wl_proxy_get_class(toplevel) != b"xdg_toplevel":
|
|
701
|
+
_STATE["error"] = ("no xdg_toplevel in the GLFW window (libdecor owns "
|
|
702
|
+
"the surface? Toggles.Melty.wayland_native_frame)")
|
|
703
|
+
return False
|
|
704
|
+
ops = _opcodes(toplevel, {b"move", b"resize"})
|
|
705
|
+
if "move" not in ops or "resize" not in ops:
|
|
706
|
+
_STATE["error"] = f"xdg_toplevel request table unreadable: {ops}"
|
|
707
|
+
return False
|
|
708
|
+
_STATE["toplevel"] = toplevel
|
|
709
|
+
_STATE["opcodes"].update(ops)
|
|
710
|
+
# The xdg_surface beside it: set_window_geometry tells the compositor
|
|
711
|
+
# the window's REAL edges (the content rect inside the shadow margin).
|
|
712
|
+
xdg_surface = window.xdg_surface if native_window else _find_proxy(glfw_window, b"xdg_surface")
|
|
713
|
+
if xdg_surface and wl.wl_proxy_get_class(xdg_surface) == b"xdg_surface":
|
|
714
|
+
geo = _opcodes(xdg_surface, {b"set_window_geometry"})
|
|
715
|
+
if "set_window_geometry" in geo:
|
|
716
|
+
_STATE["xdg_surface"] = xdg_surface
|
|
717
|
+
_STATE["opcodes"].update(geo)
|
|
718
|
+
_STATE["surface"] = surface
|
|
719
|
+
# The EGL window beside them: its attach offset is the client-side
|
|
720
|
+
# window rect the OS-edge physics rides (os_frame.flush at the flip).
|
|
721
|
+
_STATE["egl_window"] = window.egl_window if native_window else _find_egl_window(glfw_window, surface)
|
|
722
|
+
if not _bind_connection(display):
|
|
723
|
+
_STATE["error"] = _CONN["error"]
|
|
724
|
+
return False
|
|
725
|
+
# wl_surface.set_input_region - by name, on this window's surface.
|
|
726
|
+
_STATE["opcodes"].update({"surface_" + k: v for k, v in
|
|
727
|
+
_opcodes(surface, {b"set_input_region"}).items()})
|
|
728
|
+
if tag:
|
|
729
|
+
set_toplevel_tag(toplevel, tag)
|
|
730
|
+
_STATE["prev_button_cb"] = glfw.set_mouse_button_callback(window, _glfw_button)
|
|
731
|
+
_STATE["error"] = None
|
|
732
|
+
return True
|
|
733
|
+
except Exception as e: # never take the studio down over a move
|
|
734
|
+
_STATE["error"] = f"{type(e).__name__}: {e}"
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def detach(window):
|
|
739
|
+
"""The window is going away (Surface.destroy, before glfw.destroy_window):
|
|
740
|
+
forget its proxies — GLFW destroys them with the window. The
|
|
741
|
+
connection's seat, pointer, registry and managers stay bound for the
|
|
742
|
+
other windows; nothing of theirs is per window."""
|
|
743
|
+
if _STATE["window"] != window:
|
|
744
|
+
return
|
|
745
|
+
_STATE.update(attached=False, window=None, toplevel=None, xdg_surface=None, surface=None,
|
|
746
|
+
egl_window=None, offset_armed=False, prev_button_cb=None, tag=None, opcodes={})
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def available():
|
|
750
|
+
return bool(_STATE["toplevel"] and _CONN["pointer"] and _STATE["error"] is None)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def last_error():
|
|
754
|
+
return _STATE["error"]
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def press_serial():
|
|
758
|
+
return _CONN["press_serial"]
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def enter_serial():
|
|
762
|
+
"""Serial of the last wl_pointer.enter on this client: it changes when the
|
|
763
|
+
pointer comes back after a compositor grab — the grab-over signal."""
|
|
764
|
+
return _CONN["enter_serial"]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _grab(opcode_name, *extra):
|
|
768
|
+
"""Send the move/resize request. The compositor honours it only with
|
|
769
|
+
the serial of the press it keys the pointer grab to — for a single
|
|
770
|
+
button that is simply the press; with two buttons held it is either the
|
|
771
|
+
sequence's first press or the latest one depending on the compositor, so
|
|
772
|
+
both are sent when they differ:
|
|
773
|
+
exactly one matches and starts the grab, the other is ignored."""
|
|
774
|
+
if not available():
|
|
775
|
+
return False
|
|
776
|
+
serials = []
|
|
777
|
+
for serial in (_CONN["grab_serial"], _CONN["press_serial"]):
|
|
778
|
+
if serial and serial not in serials:
|
|
779
|
+
serials.append(serial)
|
|
780
|
+
if not serials:
|
|
781
|
+
return False
|
|
782
|
+
_, wl = _c()
|
|
783
|
+
toplevel = _STATE["toplevel"]
|
|
784
|
+
for serial in serials:
|
|
785
|
+
args = [ctypes.c_void_p(_CONN["seat"]), ctypes.c_uint32(serial)] + list(extra)
|
|
786
|
+
wl.wl_proxy_marshal_flags(toplevel, _STATE["opcodes"][opcode_name], None,
|
|
787
|
+
wl.wl_proxy_get_version(toplevel), 0, *args)
|
|
788
|
+
wl.wl_display_flush(_STATE["display"])
|
|
789
|
+
# The grab swallows the release of EVERY button held until now (two
|
|
790
|
+
# when a second button joined before the grab).
|
|
791
|
+
held = set(_CONN["held"])
|
|
792
|
+
if _CONN["press_button"] is not None:
|
|
793
|
+
held.add(_CONN["press_button"])
|
|
794
|
+
_CONN["masked"].update(held)
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def toplevel_proxy():
|
|
799
|
+
"""The attached window's xdg_toplevel proxy (for set_parent), or None."""
|
|
800
|
+
return _STATE.get("toplevel")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def set_parent(child_toplevel, parent_toplevel):
|
|
804
|
+
"""xdg_toplevel.set_parent(child, parent): the compositor keeps the
|
|
805
|
+
child stacked above its parent and minimises them together (app.py's
|
|
806
|
+
child surfaces). Both proxies come from toplevel_proxy() of the
|
|
807
|
+
respective attached windows. True when the request went out."""
|
|
808
|
+
if not child_toplevel:
|
|
809
|
+
return False
|
|
810
|
+
try:
|
|
811
|
+
_, wl = _c()
|
|
812
|
+
opcodes = _opcodes(child_toplevel, (b"set_parent",))
|
|
813
|
+
if "set_parent" not in opcodes:
|
|
814
|
+
return False
|
|
815
|
+
wl.wl_proxy_marshal_flags(child_toplevel, opcodes["set_parent"], None,
|
|
816
|
+
wl.wl_proxy_get_version(child_toplevel), 0,
|
|
817
|
+
ctypes.c_void_p(parent_toplevel or 0))
|
|
818
|
+
if _STATE.get("display"):
|
|
819
|
+
wl.wl_display_flush(_STATE["display"])
|
|
820
|
+
return True
|
|
821
|
+
except Exception as ex:
|
|
822
|
+
_STATE["error"] = f"set_parent: {ex}"
|
|
823
|
+
return False
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def begin_move(window=None):
|
|
827
|
+
"""xdg_toplevel.move with the held press's serial: the compositor moves
|
|
828
|
+
the window from here (Super+drag semantics). The pressed button is
|
|
829
|
+
masked until GLFW reports its next real event — see button_masked."""
|
|
830
|
+
return _grab("move")
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def begin_resize(window=None, edges=EDGE_BOTTOM_RIGHT):
|
|
834
|
+
"""xdg_toplevel.resize from the given edge/corner (EDGE_* constants)."""
|
|
835
|
+
return _grab("resize", ctypes.c_uint32(int(edges)))
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def geometry_available():
|
|
839
|
+
return bool(_STATE["xdg_surface"] and "set_window_geometry" in _STATE["opcodes"])
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def set_window_geometry(x, y, w, h):
|
|
843
|
+
"""xdg_surface.set_window_geometry: the rect of the surface the
|
|
844
|
+
compositor treats as the window — placement, edge constraints, snapping,
|
|
845
|
+
maximize sizes all use it — so a shadow margin outside it may hang off
|
|
846
|
+
the screen. Double-buffered: applies at the next commit (swap). A
|
|
847
|
+
compositor configure then names a GEOMETRY size; GLFW applies it to the
|
|
848
|
+
surface, and titlebar.on_surface_resized grows the surface back by the
|
|
849
|
+
margin."""
|
|
850
|
+
if not geometry_available():
|
|
851
|
+
return False
|
|
852
|
+
_, wl = _c()
|
|
853
|
+
xdg_surface = _STATE["xdg_surface"]
|
|
854
|
+
wl.wl_proxy_marshal_flags(xdg_surface, _STATE["opcodes"]["set_window_geometry"], None,
|
|
855
|
+
wl.wl_proxy_get_version(xdg_surface), 0,
|
|
856
|
+
ctypes.c_int32(int(x)), ctypes.c_int32(int(y)),
|
|
857
|
+
ctypes.c_int32(int(w)), ctypes.c_int32(int(h)))
|
|
858
|
+
wl.wl_display_flush(_STATE["display"])
|
|
859
|
+
return True
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def input_region_available():
|
|
863
|
+
ops = {**_CONN["opcodes"], **_STATE["opcodes"]}
|
|
864
|
+
return bool(_CONN["compositor"] and _STATE["surface"]
|
|
865
|
+
and all(k in ops for k in ("create_region", "surface_set_input_region",
|
|
866
|
+
"region_add", "region_destroy")))
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def set_input_rect(rect):
|
|
870
|
+
"""wl_surface.set_input_region: `rect` = (x, y, w, h) in surface pixels
|
|
871
|
+
that receives pointer input — the CONTENT rect of the frameless window,
|
|
872
|
+
so clicks in its transparent shadow margin fall through to whatever is
|
|
873
|
+
behind; None restores the whole surface. Takes effect at the next
|
|
874
|
+
commit (swap). The wl_region is a one-shot: the compositor copies it at
|
|
875
|
+
set time, so it is destroyed right after."""
|
|
876
|
+
if not input_region_available():
|
|
877
|
+
return False
|
|
878
|
+
_, wl = _c()
|
|
879
|
+
ops = {**_CONN["opcodes"], **_STATE["opcodes"]}
|
|
880
|
+
surface = _STATE["surface"]
|
|
881
|
+
if rect is None:
|
|
882
|
+
wl.wl_proxy_marshal_flags(surface, ops["surface_set_input_region"], None,
|
|
883
|
+
wl.wl_proxy_get_version(surface), 0, ctypes.c_void_p(None))
|
|
884
|
+
else:
|
|
885
|
+
x, y, w, h = (int(v) for v in rect)
|
|
886
|
+
region_iface = _iface_addr(wl, "wl_region_interface")
|
|
887
|
+
compositor = _CONN["compositor"]
|
|
888
|
+
region = wl.wl_proxy_marshal_flags(compositor, ops["create_region"], region_iface,
|
|
889
|
+
wl.wl_proxy_get_version(compositor), 0, ctypes.c_void_p(None))
|
|
890
|
+
if not region:
|
|
891
|
+
return False
|
|
892
|
+
version = wl.wl_proxy_get_version(region)
|
|
893
|
+
wl.wl_proxy_marshal_flags(region, ops["region_add"], None, version, 0,
|
|
894
|
+
ctypes.c_int32(x), ctypes.c_int32(y), ctypes.c_int32(w), ctypes.c_int32(h))
|
|
895
|
+
wl.wl_proxy_marshal_flags(surface, ops["surface_set_input_region"], None,
|
|
896
|
+
wl.wl_proxy_get_version(surface), 0, ctypes.c_void_p(region))
|
|
897
|
+
wl.wl_proxy_marshal_flags(region, ops["region_destroy"], None, version,
|
|
898
|
+
WL_MARSHAL_FLAG_DESTROY)
|
|
899
|
+
wl.wl_display_flush(_STATE["display"])
|
|
900
|
+
return True
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def button_masked(button):
|
|
904
|
+
"""True while GLFW's level state for `button` is a stale PRESS: the
|
|
905
|
+
compositor's grab took the release. Polls (imgui, LSDStudio.on_mouse)
|
|
906
|
+
read the button as up while this holds."""
|
|
907
|
+
return button in _CONN["masked"]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def masked_buttons():
|
|
911
|
+
return set(_CONN["masked"])
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def button_held(button):
|
|
915
|
+
"""Our own wl_pointer's level state for `button` (GLFW index): True /
|
|
916
|
+
False, or None when no pointer is bound (X11, attach failed) — then
|
|
917
|
+
there is no independent truth and the caller must not trust this."""
|
|
918
|
+
if not _CONN["pointer"]:
|
|
919
|
+
return None
|
|
920
|
+
return button in _CONN["held"]
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def note_glfw_button(button, action):
|
|
924
|
+
"""GLFW saw a real event for `button` — its state is truthful again."""
|
|
925
|
+
_CONN["masked"].discard(button)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _glfw_button(window, button, action, mods):
|
|
929
|
+
note_glfw_button(button, action)
|
|
930
|
+
prev = _STATE["prev_button_cb"]
|
|
931
|
+
if prev is not None:
|
|
932
|
+
prev(window, button, action, mods)
|