meltygui 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,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)