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,1552 @@
1
+ """Native-frame and contained Melty-window edge coordination.
2
+
3
+ The native frame and participating Melty frames meet in screen coordinates;
4
+ the display supplies the final resize barriers. Local Melty drags can push a
5
+ native boundary where supported. Window-to-window collision is confined to
6
+ native containment, not general sibling overlap avoidance.
7
+
8
+ Native resize solves child edge order, minimum spans and existing overlap.
9
+ Parent-relative nested positions must be accounted for when applying results;
10
+ driving-edge phases below are an implementation strategy, not product rules.
11
+ Hand-drag replay restores displaced geometry when the gesture reverses.
12
+
13
+ Geometry/position availability determines whether the native frame can move.
14
+ The GLFW and Wayland adapters must honor those capabilities. See
15
+ ``docs/WINDOW_COLLISION_COLUMNS.md`` for the authoritative behavior contract.
16
+ """
17
+ import meltygui.core.windowing.window_api as glfw
18
+ from meltygui.core.layout import edge_constraints
19
+
20
+ # Smallest OS window the physics allows (content px): the OS frame cell is
21
+ # zero, so a drag to it pushes the other OS edge instead of collapsing.
22
+ MIN_SIZE = (320.0, 200.0)
23
+ # Frames a requested move may stay unseen in the feed before the observed
24
+ # position is taken as the truth (a compositor refusal we didn't predict).
25
+ INFLIGHT_FRAMES = 12
26
+
27
+ _AXIS = {"x": 0, "y": 1}
28
+
29
+ # Survives hotswap (a re-exec reuses the existing dict); the edge dicts
30
+ # are the graph nodes and keep their identity across frames.
31
+ _STATE = globals().get("_STATE") or {
32
+ "edges": {"x": [{"x": 0.0}, {"x": 0.0}], "y": [{"y": 0.0}, {"y": 0.0}]},
33
+ "screen": {"x": [{"x": 0.0}, {"x": 0.0}], "y": [{"y": 0.0}, {"y": 0.0}]},
34
+ "mode": "walls", "generation": 0,
35
+ "expected": [None, None], "inflight": [None, None], "size_expected": [None, None],
36
+ "pending": {"x": [], "y": []}, "consumed": {"x": False, "y": False},
37
+ "frame": -1, "trace_frame": -1,
38
+ }
39
+ # The OS near edge's change the roots' CONTENT coordinates have not been
40
+ # re-based for yet: a solve moves the model edge in frame N, the surface
41
+ # moves at frame N+1's start (the queued size + attach offset) - the
42
+ # re-base that keeps the roots on screen lands THERE (apply_rebase), in the
43
+ # same commit as the move. Re-based at the solve they were drawn shifted
44
+ # to a surface that had not moved yet: one frame of jelly per step.
45
+ _STATE.setdefault("unapplied", [0.0, 0.0])
46
+ # A move we asked for before the feed's first read (the launch frame): folded
47
+ # into the first observation so it never reads as the compositor's.
48
+ _STATE.setdefault("own_move_pending", [0.0, 0.0])
49
+ # The OS FAR edge's motion the far-hung children have not been re-based
50
+ # for yet (compensate_far computes, apply_rebase applies at the next frame
51
+ # pass - when the root is laid out at the size that includes it).
52
+ _STATE.setdefault("unapplied_far", [0.0, 0.0])
53
+ # The OS edges as of the last OS-level solve (solve): what the roots were
54
+ # last laid out against. Any difference at the next solve is the OS window
55
+ # having moved on its own - a compositor's resize - and is pushed through
56
+ # the roots; a landing of our own request only snaps it.
57
+ _STATE.setdefault("os_seen", [None, None])
58
+ _STATE.setdefault("window_id", None)
59
+ # the feed's own far edge (x + width) per axis as last observed - a
60
+ # foreign change is classified against THIS (one frame edge), rat
61
+ # against our size
62
+ _STATE.setdefault("feed_far", [None, None])
63
+ _STATE.setdefault("reset_frame", 0)
64
+ # The move we last asked for per axis (flush) - a foreign move that exactly
65
+ # cancels it is the compositor REFUSING it (a gap we don't see: the
66
+ # floating-window top gap, a bar), and the edge we tried to move is walled
67
+ # there for the rest of the gesture: `learned` = [near_or_None, far_or_None]
68
+ # per axis, applied by _walls_to_edges and removed when the gesture ends or
69
+ # the compositor lets the edge past it after all (begin_frame).
70
+ _STATE.setdefault("last_offset", [0, 0])
71
+ # Outstanding content sizes, oldest first. Retire an acknowledged prefix
72
+ # so an older request cannot rewind a newer drag, but a later compositor
73
+ # resize to a previously used size is still accepted. The size observation
74
+ # may stay unchanged while a request is in flight, bounded by its age.
75
+ _STATE.setdefault("size_requests", {"x": [], "y": []})
76
+ _STATE.setdefault("size_observed", [None, None])
77
+ _STATE.setdefault("size_request_frame", [None, None])
78
+ _STATE.setdefault("learned", {"x": [None, None], "y": [None, None]})
79
+ _STATE.setdefault("pin_rebases", {})
80
+ _STATE.setdefault("move_requests", {"x": [], "y": []})
81
+
82
+
83
+ def _trace(msg):
84
+ from meltygui.core.runtime.toggles import Toggles
85
+ if Toggles.Melty.push_os_window_edges_trace:
86
+ print(f"[os_frame] {msg}")
87
+
88
+
89
+ def reset(reason="studio start"):
90
+ """Forget the previous session's window: expected position, in-flight
91
+ moves, booked re-bases, the edges the roots were laid out against.
92
+ _STATE outlives a studio restart inside the server process, and a new
93
+ window met the old one's numbers as a giant foreign change — roots
94
+ re-based against a stale origin, pushed, left outside (08-27)."""
95
+ from meltygui.core.melty import Melty
96
+ _STATE["expected"] = [None, None]
97
+ _STATE["inflight"] = [None, None]
98
+ _STATE["size_expected"] = [None, None]
99
+ _STATE["unapplied"] = [0.0, 0.0]
100
+ _STATE["own_move_pending"] = [0.0, 0.0]
101
+ _STATE["os_seen"] = [None, None]
102
+ _STATE["pending"] = {"x": [], "y": []}
103
+ _STATE["gestures"] = {}
104
+ _STATE["last_offset"] = [0, 0]
105
+ _STATE["learned"] = {"x": [None, None], "y": [None, None]}
106
+ _STATE["size_requests"] = {"x": [], "y": []}
107
+ _STATE["size_observed"] = [None, None]
108
+ _STATE["size_request_frame"] = [None, None]
109
+ _STATE["unapplied_far"] = [0.0, 0.0]
110
+ _STATE["pin_rebases"] = {}
111
+ _STATE["move_requests"] = {"x": [], "y": []}
112
+ _STATE["window_id"] = None
113
+ _STATE["feed_far"] = [None, None]
114
+ _STATE["generation"] += 1
115
+ _STATE["reset_frame"] = getattr(Melty, "frame_count", 0) or 0
116
+ _trace(f"reset ({reason})")
117
+
118
+
119
+ def _enabled():
120
+ from meltygui.core.runtime.toggles import Toggles
121
+ return bool(Toggles.Melty.push_os_window_edges)
122
+
123
+
124
+ def mode():
125
+ """"feed" (Wayland, the extension's position), "x11" (glfw's position)
126
+ or "walls" (no position: the OS edges are immovable)."""
127
+ return _STATE["mode"]
128
+
129
+
130
+ def available():
131
+ return _STATE["mode"] != "walls"
132
+
133
+
134
+ def edges(axis):
135
+ """The OS frame pair on ``axis`` (screen coords; content coords in
136
+ walls mode)."""
137
+ return _STATE["edges"][axis]
138
+
139
+
140
+ def screen_edges(axis):
141
+ return _STATE["screen"][axis] if available() else None
142
+
143
+
144
+ def display_top():
145
+ """The top of the DISPLAY (the work area's top edge — below GNOME's
146
+ bar) in the studio's CONTENT coordinates, comparable straight against a
147
+ window's abs_top. The hard limit a meltygui window's top may never pass
148
+ (columns._frame_pass, Toggles.Melty.window_top_hard_limit). Against the
149
+ APPLIED surface origin (the model's near edge less what apply_rebase
150
+ has not applied yet — attach's ctx.base convention), so it holds on
151
+ the frames between an OS edge pushed in the model and the surface
152
+ move landing. Negative while the studio sits below the display top: a
153
+ window may rise above the STUDIO's top (pushing the OS edge ahead of
154
+ it) and stops only where the OS edge stops. Walls mode / no model: 0 —
155
+ the studio's top is the display's."""
156
+ if not _enabled():
157
+ return 0.0
158
+ near, _far = _STATE["edges"]["y"]
159
+ applied = near["y"] - _STATE["unapplied"][1]
160
+ if _STATE["mode"] == "walls":
161
+ return applied
162
+ scr_near, _scr_far = _STATE["screen"]["y"]
163
+ return scr_near["y"] - applied
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Private
168
+ # ---------------------------------------------------------------------------
169
+
170
+ def _observe():
171
+ """((x, y), (work_x, work_y, work_w, work_h), mode, (far_x, far_y),
172
+ window_id) of the content rect on screen — the far edges from the
173
+ SAME source as the position (the feed's own width / height; glfw's on
174
+ X11) — or None when no position is known."""
175
+ import meltygui.core.windowing.titlebar as titlebar
176
+ if titlebar._on_wayland():
177
+ import meltygui.core.windowing.geometry_feed as geometry_feed
178
+ geometry_feed.ensure_started() # a hotswap, not a restart (or a backend switch): start it here
179
+ # the Hyprland backend reports the SURFACE: shrink by the shadow
180
+ # inset to the content (a no-op on the GNOME feed's geometry rect)
181
+ rect, area = geometry_feed.frame_rect(inset=titlebar.window_inset()), geometry_feed.workarea()
182
+ if rect is None or area is None:
183
+ return None
184
+ frame = geometry_feed._current_frame() or {} # the ACTIVE surface's window (extension.py)
185
+ return ((float(rect[0]), float(rect[1])), tuple(float(v) for v in area), "feed",
186
+ (float(rect[0] + rect[2]), float(rect[1] + rect[3])), frame.get("id"))
187
+ window = titlebar._studio_window()
188
+ if window is None:
189
+ return None
190
+ try:
191
+ x, y = glfw.get_window_pos(window)
192
+ w, h = glfw.get_window_size(window)
193
+ wa_l, wa_t, wa_r, wa_b = titlebar._workarea_for(window)
194
+ except Exception:
195
+ return None
196
+ return ((float(x), float(y)), (wa_l, wa_t, wa_r - wa_l, wa_b - wa_t), "x11",
197
+ (float(x + w), float(y + h)), id(window))
198
+
199
+
200
+ def _root_windows():
201
+ """Top-level windows in this surface, deduped by identity. The studio
202
+ uses its registered windows; GLFW surfaces keep their own live roots
203
+ because the persisted registry also contains other surfaces' windows.
204
+ Include parentless entries in the surface-local root_draw_states too."""
205
+ from meltygui.core.melty import Melty
206
+ from meltygui.core.windowing.surface import Surface
207
+ seen, roots = set(), []
208
+ if Surface.active is not None:
209
+ candidates = list(getattr(Surface.active, "root_windows", {}).values())
210
+ else:
211
+ candidates = [getattr(managed, "draw_state", None)
212
+ for managed in list(getattr(Melty, "registered_windows", {}).values())]
213
+ for ds in candidates:
214
+ if ds is None or id(ds) in seen or getattr(ds, "parent_window", None) is not None:
215
+ continue
216
+ seen.add(id(ds))
217
+ roots.append(ds)
218
+ for group in list(getattr(Melty, "root_draw_states", {}).values()):
219
+ for ds in list(group):
220
+ if id(ds) in seen or getattr(ds, "parent_window", None) is not None:
221
+ continue
222
+ seen.add(id(ds))
223
+ roots.append(ds)
224
+ return roots
225
+
226
+
227
+ def _movable_roots():
228
+ """The roots that hold their SCREEN position when the OS near edge moves:
229
+ closable floating windows. The Main Window (not closable) IS the
230
+ content and rides with the OS frame. NESTED windows are not here: their
231
+ window_pos is parent-relative, they ride with their parent."""
232
+ return [ds for ds in _root_windows()
233
+ if getattr(ds, "closable", False) and ds.window_pos is not None]
234
+
235
+
236
+ def _frame_pinned(ds):
237
+ """A root whose frame the caller dictates every frame — an app's root
238
+ pinned to the OS window (core_render stamps `_frame_pinned` for a
239
+ closable, never-draggable window with width AND height passed). It
240
+ RIDES with the surface: it cannot hold a screen position (the pin
241
+ re-stamps window_pos next frame), so it is never re-based."""
242
+ return bool(getattr(ds, "_frame_pinned", False))
243
+
244
+
245
+ def _frame_parent(window):
246
+ """Containing window, skipping ordinary views used as placement parents."""
247
+ parent = getattr(window, "parent_window", None)
248
+ for _ in range(64):
249
+ if (parent is None or getattr(parent, "closable", False)
250
+ or _frame_pinned(parent)):
251
+ return parent
252
+ ancestor = getattr(parent, "parent_window", None)
253
+ if ancestor is None or ancestor is parent:
254
+ return parent
255
+ parent = ancestor
256
+ return None
257
+
258
+
259
+ def _pinned_children(axis, side):
260
+ """The nested closable windows immediately inside frame-pinned roots that
261
+ hang from the root's ``side`` ("near" / "far") corner on ``axis``
262
+ (os_frame._driver_of: parent_anchor_pos). A pinned root's frame IS
263
+ the OS window's, so its children are the studio roots of that world:
264
+ they hold their SCREEN position when the OS edge they hang from
265
+ moves — the near-hung ones through apply_rebase (the surface's origin
266
+ moved), the far-hung ones through compensate_far (the root's far
267
+ corner moved with the OS far edge: uncompensated, a context menu hung
268
+ from the app's right corner and dragged right pushed the OS edge out,
269
+ hung further right for it and pushed again — the app grew to the
270
+ screen in a dozen frames, 09-13). Pins with a booked actual anchor
271
+ defer to rebase_pin after parent layout instead. Ordinary placement
272
+ parents (such as the inline panel's text view) do not add a window level."""
273
+ pinned = {id(ds) for ds in _movable_roots() if _frame_pinned(ds)}
274
+ if not pinned:
275
+ return []
276
+ return [ds for ds in _all_windows()
277
+ if id(_frame_parent(ds)) in pinned
278
+ and getattr(ds, "closable", False) and ds.window_pos is not None
279
+ and id(ds) not in _STATE["pin_rebases"]
280
+ and _driver_of(ds, axis) == side]
281
+
282
+
283
+ def _has_pin_anchor(ds):
284
+ return getattr(ds, "_pin_target", None) is not None
285
+
286
+
287
+ def _caller_owns_frame(ds):
288
+ kwargs = getattr(ds, "_kwargs", {})
289
+ return all(kwargs.get(key) is not None for key in ("window_pos", "width", "height"))
290
+
291
+
292
+ def _native_layout_parent(ds):
293
+ """Native root reached through windows whose caller owns their geometry.
294
+
295
+ A fixed workspace can be closable without being the surface root. Its
296
+ next layout may follow a native resize, carrying a far-anchored inspector
297
+ a second time. Stop at freely placed/sized windows: their hand movement
298
+ must still carry descendants normally.
299
+ """
300
+ parent = _frame_parent(ds)
301
+ for _ in range(64):
302
+ if parent is None or _frame_pinned(parent):
303
+ return parent
304
+ if not _caller_owns_frame(parent):
305
+ return None
306
+ ancestor = _frame_parent(parent)
307
+ if ancestor is parent:
308
+ return None
309
+ parent = ancestor
310
+ return None
311
+
312
+
313
+ def _native_layout_frame(ds):
314
+ """An explicitly pinned layout body inside a native surface.
315
+
316
+ Width/height/position alone also describe fixed popovers. Only a body
317
+ marked frame_pinned participates in the native frame's background
318
+ gestures; it still owns its local columns and rows.
319
+ """
320
+ return (bool(getattr(ds, "_kwargs", {}).get("frame_pinned", False))
321
+ and _caller_owns_frame(ds) and _native_layout_parent(ds) is not None)
322
+
323
+
324
+ def frame_binding(window, axis):
325
+ """Read the declared surface relationship before any edge is solved.
326
+
327
+ The root's last layout size is the reference: the newly observed native
328
+ size may already differ from it. Measuring against that new size would
329
+ turn native motion into a changing margin and cancel the resize.
330
+ """
331
+ from meltygui.core.windowing.frame_geometry import SurfaceBinding
332
+ i = _AXIS[axis]
333
+ if _frame_pinned(window):
334
+ return SurfaceBinding(window, axis, (window.window_pos or (0, 0))[i], 0.)
335
+ if not _native_layout_frame(window):
336
+ return None
337
+ root = _native_layout_parent(window)
338
+ native_size = (root.width if axis == "x" else root.height) + (root.window_pos or (0, 0))[i]
339
+ near_gap = _screen_pos(window, axis)
340
+ size = window.width if axis == "x" else window.height
341
+ return SurfaceBinding(window, axis, near_gap, native_size - near_gap - size)
342
+
343
+
344
+ def _has_measured_anchor(ds):
345
+ if _caller_owns_frame(ds):
346
+ return False
347
+ parent = _frame_parent(ds)
348
+ return (_has_pin_anchor(ds)
349
+ or getattr(ds, "parent_window", None) is not parent
350
+ or (parent is not None and not _frame_pinned(parent)
351
+ and _native_layout_parent(ds) is not None))
352
+
353
+
354
+ def _anchor_base(ds):
355
+ """Placement origin before the child's own offset or display sliver cap.
356
+
357
+ An inline panel can name an ordinary text view as parent_window. Its
358
+ anchor follows that view's layout, not necessarily the OS frame corner.
359
+ """
360
+ if _has_pin_anchor(ds):
361
+ base = ds.clip_anchor_base
362
+ if base is None:
363
+ return None
364
+ return base[0], ds._pinned_base_y(base[1], ds.anchor_offset[1])
365
+ parent = ds.parent_window
366
+ if parent is None:
367
+ return None
368
+ scroll_x, scroll_y = ds._ancestor_scroll()
369
+ anchor_x, anchor_y = ds.parent_anchor_offset
370
+ return (parent.abs_left + ds.left_offset - scroll_x + anchor_x,
371
+ parent.abs_top + ds.top_offset - scroll_y + anchor_y)
372
+
373
+
374
+ def pin_origin(ds):
375
+ """The measured placement anchor in screen coordinates, outside a solve.
376
+
377
+ Pins and explicit view parents can point at a fixed-width text view or
378
+ split column; their corners need not move with the OS frame's corners.
379
+ """
380
+ base = _anchor_base(ds)
381
+ if base is None:
382
+ return None
383
+ return (applied_origin("x") + base[0], applied_origin("y") + base[1])
384
+
385
+
386
+ def _book_pin_rebases():
387
+ """Remember placement origins before native containment changes layout.
388
+
389
+ Surface-root anchors, including those reached through caller-controlled
390
+ workspace geometry, await a native request. Movable-frame anchors
391
+ are booked separately by solve() when containment actually changes that
392
+ frame; otherwise the next hand resize would be mistaken for feedback.
393
+ Pending origins belong to the layout commit, not an axis or solve call:
394
+ retain them until rebase_pin consumes them or the window closes.
395
+ """
396
+ pending = _STATE["pin_rebases"]
397
+ windows = [ds for ds in _all_windows() if _open(ds)]
398
+ alive = {id(ds) for ds in windows}
399
+ for identity in list(pending):
400
+ if identity not in alive:
401
+ del pending[identity]
402
+ for ds in windows:
403
+ if _native_layout_parent(ds) is not None and _has_measured_anchor(ds):
404
+ pending.setdefault(id(ds), pin_origin(ds))
405
+
406
+
407
+ def rebase_pin(ds):
408
+ """After parent layout, before the child's edge pass: keep its screen
409
+ position through an OS resize using the placement anchor's displacement.
410
+ The parent has not been laid out yet at apply_rebase (frame start).
411
+ """
412
+ previous = _STATE["pin_rebases"].pop(id(ds), None)
413
+ if previous is None or not _has_measured_anchor(ds):
414
+ return
415
+ current = pin_origin(ds)
416
+ if current is not None:
417
+ for axis, i in _AXIS.items():
418
+ delta = previous[i] - current[i]
419
+ if delta:
420
+ _rebase(ds, axis, delta)
421
+
422
+
423
+ def _rebased_windows(axis="x"):
424
+ """The windows that hold their SCREEN position when the OS NEAR edge
425
+ moves (apply_rebase / a foreign near resize): the movable roots that
426
+ are not frame-pinned, plus a pinned root's direct children hung from
427
+ its near corner (_pinned_children). Without this a nested window that
428
+ pushed the OS near edge out ended outside the surface by the push: its
429
+ own pass slid it as if its parent held the screen, the parent was
430
+ re-based, then pinned back (09-13)."""
431
+ held = [ds for ds in _movable_roots() if not _frame_pinned(ds)]
432
+ return held + _pinned_children(axis, "near")
433
+
434
+
435
+ def compensate_far(axis, d):
436
+ """The OS FAR edge moved by ``d`` (a pass or the OS-level solve): a
437
+ pinned root's children hung from its far corner ride that corner in
438
+ content coordinates, so they are re-based by -d to hold the screen —
439
+ exactly what apply_rebase does for the near edge, and like it BOOKED
440
+ here and applied at the next frame start: the root is laid out at
441
+ the model's size at frame start (surface.frame ← content_size), so
442
+ the corner moves THEN; re-based in the same frame the child drew a
443
+ push to the left of where it belonged for one frame (09-13)."""
444
+ if d:
445
+ _STATE["unapplied_far"][_AXIS[axis]] += d
446
+
447
+
448
+ def _depth(ds):
449
+ depth, node = 0, getattr(ds, "parent_window", None)
450
+ while node is not None and depth < 64:
451
+ depth += 1
452
+ node = getattr(node, "parent_window", None)
453
+ return depth
454
+
455
+
456
+ def _all_windows():
457
+ """Every meltygui window that collides at the OS level — the roots AND
458
+ the nested windows (Melty.root_draw_states holds those under their
459
+ parent's id) — parents before children (write-backs of a child are
460
+ relative to its parent's motion). Deduped by identity."""
461
+ from meltygui.core.melty import Melty
462
+ seen, windows = set(), []
463
+ for ds in _root_windows():
464
+ seen.add(id(ds))
465
+ windows.append(ds)
466
+ for group in list(getattr(Melty, "root_draw_states", {}).values()):
467
+ for ds in list(group):
468
+ if id(ds) in seen:
469
+ continue
470
+ seen.add(id(ds))
471
+ windows.append(ds)
472
+ windows.sort(key=_depth)
473
+ return windows
474
+
475
+
476
+ def _rebase(ds, axis, delta):
477
+ """Move a root by ``delta`` on ``axis`` in CONTENT coordinates so it
478
+ keeps its screen position after the content origin moved the other
479
+ way — the press-anchored corner drag's baseline with it."""
480
+ pos = ds.window_pos or (0, 0)
481
+ ds.window_pos = (pos[0] + delta, pos[1]) if axis == "x" else (pos[0], pos[1] + delta)
482
+ # press-anchored baselines ride along: the corner resize's and the
483
+ # window move's (the window dragging the studio along to stay under
484
+ # the hand - its position is re-derived from this every frame)
485
+ for attr in ("_initial_window_pos_resize", "_initial_window_pos"):
486
+ base = getattr(ds, attr, None)
487
+ if base is not None:
488
+ setattr(ds, attr, (base[0] + delta, base[1]) if axis == "x" else (base[0], base[1] + delta))
489
+
490
+
491
+ def _set_mode(new_mode):
492
+ if _STATE["mode"] != new_mode:
493
+ _STATE["mode"] = new_mode
494
+ _STATE["generation"] += 1 # every root forgets where it saw the OS edges
495
+ _STATE["gestures"] = {}
496
+ _STATE["expected"] = [None, None]
497
+ _STATE["inflight"] = [None, None]
498
+ _STATE["size_expected"] = [None, None]
499
+ _STATE["size_requests"] = {"x": [], "y": []}
500
+ _STATE["size_observed"] = [None, None]
501
+ _STATE["size_request_frame"] = [None, None]
502
+ _trace(f"mode → {new_mode}")
503
+
504
+
505
+ def _observe_size(axis, size):
506
+ """Retire acknowledged requests; only outstanding sizes may lag the model."""
507
+ i = _AXIS[axis]
508
+ recent = _STATE["size_requests"][axis]
509
+ previous = _STATE["size_observed"][i]
510
+ _STATE["size_observed"][i] = size
511
+ sent = _STATE["size_request_frame"][i]
512
+ if sent is not None and _STATE["frame"] - sent > INFLIGHT_FRAMES:
513
+ recent.clear()
514
+ matches = [k for k, requested in enumerate(recent) if abs(size - requested) < 1.0]
515
+ if matches:
516
+ del recent[:matches[-1] + 1]
517
+ elif previous is None or abs(size - previous) >= 1.0:
518
+ recent.clear() # a new, unrequested compositor size
519
+ if not recent:
520
+ _STATE["size_request_frame"][i] = None
521
+ return bool(recent)
522
+
523
+
524
+ def begin_frame():
525
+ """Start of the frame, right after Melty.display_size is stamped: bring
526
+ the OS model up to date with what is real — the content size (our own
527
+ resizes land at frame start, a compositor's through the resize
528
+ callback) and the observed position — and fold in what we did not
529
+ request."""
530
+ from meltygui.core.melty import Melty
531
+ # Surfaces saved before this code was hotswapped have no move history.
532
+ _STATE.setdefault("move_requests", {"x": [], "y": []})
533
+ _STATE["frame"] = Melty.frame_count
534
+ _STATE["consumed"] = {"x": False, "y": False}
535
+ display = Melty.display_size
536
+ if not display or not display[0] or not display[1]:
537
+ return
538
+ size = (float(display[0]), float(display[1]))
539
+ observed = _observe() if _enabled() else None
540
+ if observed is None:
541
+ _set_mode("walls")
542
+ for axis, i in _AXIS.items():
543
+ near, far = _STATE["edges"][axis]
544
+ near[axis], far[axis] = 0.0, size[i]
545
+ _STATE["os_seen"][i] = (0.0, size[i])
546
+ return
547
+ (pos, area, new_mode, feed_far, window_id) = observed
548
+ _set_mode(new_mode)
549
+ if _STATE["window_id"] is not None and window_id != _STATE["window_id"]:
550
+ reset(f"window {_STATE['window_id']} → {window_id}")
551
+ _STATE["window_id"] = window_id
552
+ for axis, i in _AXIS.items():
553
+ scr_near, scr_far = _STATE["screen"][axis]
554
+ scr_near[axis], scr_far[axis] = area[i], area[i] + area[i + 2]
555
+ near, far = _STATE["edges"][axis]
556
+ requested_size = (bool(_STATE["size_requests"][axis])
557
+ and _STATE["size_expected"][i] is not None
558
+ and abs(feed_far[i] - pos[i] - _STATE["size_expected"][i]) < 1.0)
559
+ size_pending = _observe_size(axis, size[i])
560
+ expected = _STATE["expected"][i]
561
+ if expected is None: # first sight
562
+ # A launch-fit move requested before this sight lands a frame
563
+ # or two later: see the model where it will be, or the landing
564
+ # reads as a foreign near resize and the roots get re-based.
565
+ pending = _STATE["own_move_pending"][i]
566
+ _STATE["own_move_pending"][i] = 0.0
567
+ at = pos[i] + pending
568
+ near[axis], far[axis] = at, at + size[i]
569
+ _STATE["expected"][i] = at
570
+ if pending:
571
+ _STATE["inflight"][i] = Melty.frame_count
572
+ _STATE["size_expected"][i] = size[i]
573
+ _STATE["os_seen"][i] = (near[axis], far[axis])
574
+ _STATE["feed_far"][i] = feed_far[i]
575
+ _trace(f"{axis}: first sight near={pos[i]:.0f} size={size[i]:.0f}")
576
+ continue
577
+ # Position: our requests land a frame or two later; before then the
578
+ # model already has them and the observation is behind.
579
+ if pos[i] != expected:
580
+ flight = _STATE["inflight"][i]
581
+ asked = _STATE["last_offset"][i]
582
+ # A move whose SIZE landed at a different position is not
583
+ # necessarily "still landing": the compositor can clamp only
584
+ # part of a move, or shift a resize to respect its own inset.
585
+ # Fold that constraint in now, so
586
+ # _foreign_change learns the wall this frame, not INFLIGHT_FRAMES
587
+ # later with the far edge ratcheting meanwhile.
588
+ # ... read off the FEED's own far edge (its size = far - pos):
589
+ # only the compositor's committed state shows the new size at
590
+ # the old position; a request merely in flight shows the old
591
+ # size ( (glfw's framebuffer alone landed a frame earlier).
592
+ refused = (asked and (pos[i] - expected) * asked < 0
593
+ and _STATE["size_expected"][i] is not None
594
+ and abs((feed_far[i] - pos[i]) - _STATE["size_expected"][i]) < 1.0)
595
+ # With a size floor, all moves with different sizes. An
596
+ # older requested box is an in-flight reply, not a refusal of
597
+ # the latest move. Rebasing to it would send the offset twice.
598
+ moves = _STATE["move_requests"][axis]
599
+ observed_box = (pos[i], feed_far[i] - pos[i])
600
+ older_reply = any(abs(observed_box[0] - at) < 1.0 and abs(observed_box[1] - extent) < 1.0
601
+ for at, extent in moves)
602
+ if older_reply:
603
+ refused = False
604
+ constrained = requested_size and _any_button_down() and not older_reply
605
+ if flight is not None and Melty.frame_count - flight <= INFLIGHT_FRAMES and not (refused or constrained):
606
+ pass # still landing
607
+ else:
608
+ _STATE["move_requests"][axis].clear()
609
+ d = pos[i] - expected
610
+ _STATE["expected"][i] = pos[i]
611
+ _STATE["inflight"][i] = None
612
+ far_seen = _STATE["feed_far"][i]
613
+ far_held = far_seen is not None and abs(feed_far[i] - far_seen) < 1.0
614
+ _STATE["feed_far"][i] = feed_far[i]
615
+ _foreign_change(axis, d, size[i], far_held, constrained=constrained)
616
+ continue
617
+ else:
618
+ _STATE["inflight"][i] = None
619
+ _STATE["move_requests"][axis].clear()
620
+ _STATE["last_offset"][i] = 0
621
+ _STATE["feed_far"][i] = feed_far[i]
622
+ # Size: ours lands at frame start (size_expected); anything else is
623
+ # the compositor's - the far edge moved.
624
+ if size[i] != far[axis] - near[axis]:
625
+ if size_pending:
626
+ # an OLDER request of ours landed; the newest is still in
627
+ # flight and the model already holds it - nothing to fold
628
+ pass
629
+ elif _STATE["size_expected"][i] != size[i]:
630
+ _trace(f"{axis}: foreign far edge {far[axis]:.0f} → {near[axis] + size[i]:.0f}")
631
+ far[axis] = near[axis] + size[i]
632
+ else:
633
+ # our resize landed: the integer size snaps the model's far
634
+ # edge by a fraction - not a move, nothing to solve
635
+ seen = _STATE["os_seen"][i]
636
+ far[axis] = near[axis] + size[i]
637
+ if seen is not None and abs(seen[1] - far[axis]) < 1.0:
638
+ _STATE["os_seen"][i] = (seen[0], far[axis])
639
+ if not size_pending:
640
+ _STATE["size_expected"][i] = size[i]
641
+ for axis in _AXIS:
642
+ _walls_to_edges(axis)
643
+
644
+
645
+ def _walls_to_edges(axis):
646
+ """An OS edge already PAST the screen wall (the studio dragged partly
647
+ off the screen by the compositor's own move; Hyprland's reserved strip
648
+ moving the work area's edge under it) is where it is: the wall on that
649
+ side moves out to the edge for this frame's solves. Left at the work
650
+ area, the solver read the wall's floor chain as violated and clamped
651
+ the edge back onto the wall on the first OS-level drag — the studio
652
+ teleported by the whole overhang and grew by as much on the other
653
+ side (a top-left right-drag on a studio 475 px off the left edge,
654
+ 09-09). Pinned there, a drag further out is blocked and flips (the
655
+ far side grows), a drag inward moves the edge, and the next frame's
656
+ stamp follows it back toward the work area."""
657
+ if _STATE["mode"] == "walls":
658
+ return
659
+ near, far = _STATE["edges"][axis]
660
+ scr_near, scr_far = _STATE["screen"][axis]
661
+ learned = _STATE["learned"][axis]
662
+ # A wall the compositor taught us (a refused move): forget once the
663
+ # hand lets go, or once the edge is seen past it after all.
664
+ if not _any_button_down():
665
+ learned[0] = learned[1] = None
666
+ if learned[0] is not None and near[axis] < learned[0] - 1.5:
667
+ learned[0] = None
668
+ if learned[1] is not None and far[axis] > learned[1] + 1.5:
669
+ learned[1] = None
670
+ if learned[0] is not None:
671
+ scr_near[axis] = max(scr_near[axis], learned[0])
672
+ if learned[1] is not None:
673
+ scr_far[axis] = min(scr_far[axis], learned[1])
674
+ scr_near[axis] = min(scr_near[axis], near[axis])
675
+ scr_far[axis] = max(scr_far[axis], far[axis])
676
+
677
+
678
+ def _foreign_change(axis, d, size, far_held, constrained=False):
679
+ """The window's near edge is ``d`` px from where we expected it and we
680
+ asked for nothing: the compositor moved or resized the studio.
681
+ ``far_held`` — the feed's own far edge (x + width) did not move → a
682
+ NEAR RESIZE: the roots hold their screen positions (re-base) and get
683
+ pushed by the edge where it reaches them (solve). Otherwise a MOVE by
684
+ ``d`` (the roots ride along, nothing to solve) plus whatever the far
685
+ edge did on top. Classified from ONE source on purpose: the feed's
686
+ position against our own size read as a resize whenever the two were
687
+ a frame apart."""
688
+ i = _AXIS[axis]
689
+ near, far = _STATE["edges"][axis]
690
+ old_far = far[axis]
691
+ near[axis] += d
692
+ if far_held:
693
+ _trace(f"{axis}: foreign near resize {d:+.0f} — roots hold the screen")
694
+ for ds in _rebased_windows(axis):
695
+ _rebase(ds, axis, -d)
696
+ far[axis] = near[axis] + size
697
+ else:
698
+ _trace(f"{axis}: foreign move {d:+.0f} (size {old_far - (near[axis] - d):.0f} → {size:.0f})")
699
+ for ds in _all_windows():
700
+ seen = getattr(ds, "_os_seen", None)
701
+ if seen and axis in seen:
702
+ seen[axis] = (seen[axis][0] + d, seen[axis][1] + d)
703
+ # A compositor MOVE carries the surface and its drag origin
704
+ # together. Otherwise sticky replay interprets the new origin as
705
+ # resize motion and throws the view back to its old screen spot.
706
+ gesture = (getattr(ds, "_edge_gestures", None) or {}).get(axis)
707
+ if gesture is not None:
708
+ gesture["origin"] += d
709
+ if gesture["os"]:
710
+ gesture["os"] = {key: value + d for key, value in gesture["os"].items()}
711
+ gesture = _STATE["gestures"].get(axis)
712
+ if gesture is not None:
713
+ gesture["snap"] = [value + d for value in gesture["snap"]]
714
+ for identity, saved in list(gesture.get("windows", {}).items()):
715
+ n, f, floor, span, edge_values = saved
716
+ gesture["windows"][identity] = (n + d, f + d, floor, span, edge_values)
717
+ os_seen = _STATE["os_seen"][i]
718
+ if os_seen is not None:
719
+ _STATE["os_seen"][i] = (os_seen[0] + d, os_seen[1] + d)
720
+ far[axis] = near[axis] + size
721
+ asked = _STATE["last_offset"][i]
722
+ if constrained or (asked and d * asked < 0):
723
+ # The compositor constrained our move/resize, possibly only partly:
724
+ # the displaced edge is clamped THERE. Learn that wall
725
+ # for the gesture - the flip then has no room on that side - and
726
+ # pull the OTHER edge back inside the work area: the size changes
727
+ # land, so the far edge now overhangs by the refused move, and
728
+ # left "where it is" the next flip grew it by one more screen
729
+ # size (the studio's bottom ratcheting below the display).
730
+ scr_near, scr_far = _STATE["screen"][axis]
731
+ learned = _STATE["learned"][axis]
732
+ if d > 0:
733
+ learned[0] = near[axis]
734
+ far[axis] = min(far[axis], scr_far[axis])
735
+ else:
736
+ learned[1] = far[axis]
737
+ near[axis] = max(near[axis], scr_near[axis])
738
+ _trace(f"{axis}: move/resize constrained (offset {asked:+.0f}) — wall learned at "
739
+ f"{learned[0] if d > 0 else learned[1]:.0f}")
740
+ _STATE["size_expected"][i] = size
741
+
742
+
743
+ # ---------------------------------------------------------------------------
744
+ # The solve: what a root's frame needs gets, and what it hands back
745
+ # ---------------------------------------------------------------------------
746
+
747
+ class Context:
748
+ """A local solve's private native/display edges, in window coordinates.
749
+
750
+ Shared native geometry stays in screen coordinates throughout the solve.
751
+ Only detach commits these results; an interrupted solve changes no native
752
+ geometry. Source identities keep gesture snapshots stable across contexts.
753
+ """
754
+ __slots__ = ("axis", "base", "os_near0", "os_far0", "lists", "specs", "walls",
755
+ "drags", "os_ids", "shifted", "move")
756
+
757
+ def __init__(self, axis):
758
+ self.axis = axis
759
+ self.base = 0.0
760
+ self.os_near0 = 0.0
761
+ self.os_far0 = 0.0
762
+ self.lists, self.specs, self.drags = [], [], []
763
+ self.walls = frozenset()
764
+ self.os_ids = frozenset()
765
+ self.shifted = ()
766
+ self.move = False # the window was moved by hand this frame
767
+
768
+ @property
769
+ def native(self):
770
+ return self.shifted.native
771
+
772
+ @property
773
+ def sources(self):
774
+ return self.shifted.sources
775
+
776
+ @property
777
+ def binding(self):
778
+ return self.shifted.binding
779
+
780
+
781
+ def applied_origin(axis):
782
+ """The surface's content origin on ``axis`` in SCREEN coords as it is
783
+ APPLIED right now: the model's near edge less the motion not yet
784
+ landed (apply_rebase). Shared edges remain in screen coordinates, even
785
+ while a local frame is solving. 0 with the OS level off."""
786
+ if not _enabled():
787
+ return 0.0
788
+ near, _far = _STATE["edges"][axis]
789
+ return float(near[axis] - _STATE["unapplied"][_AXIS[axis]])
790
+
791
+
792
+ def queue_drag(axis, index, inc):
793
+ """A cursor-driven drag of the OS window's own frame edge (``index`` 0
794
+ = near / left / top, 1 = far) by ``inc`` px — the background right-drag
795
+ (titlebar). Solved by the first root pass of the frame, else by flush."""
796
+ if inc:
797
+ _STATE["pending"][axis].append((int(index), float(inc)))
798
+
799
+
800
+ def attach(window, axis, has_pending=True, hand_move=False, binding=None):
801
+ """Called by columns._frame_pass for a ROOT window before its solve:
802
+ the native/display cells, walls and drags in window coordinates.
803
+ These are private copies; detach commits only their native results in
804
+ screen coordinates. None when the OS
805
+ level is off, or when there is nothing to solve at all — no queued
806
+ drag of the window (``has_pending``), no hand move of it this frame
807
+ (``hand_move``: its frame pushes the OS edge it overlaps,
808
+ Toggles.Melty.window_move_pushes_os_edges), none of the OS window's
809
+ own drags, no OS edge moved since this window last saw it — so an
810
+ idle frame touches nothing."""
811
+ from meltygui.core.melty import Melty
812
+ from meltygui.core.runtime.toggles import Toggles
813
+ if not _enabled() or _STATE["frame"] != Melty.frame_count: # only in a frame begin_frame set up
814
+ return None
815
+ # Nested windows take part exactly like roots (Lukas 08-27): their
816
+ # abs_left / abs_top IS their screen position (OS offsets folded
817
+ # in), and their parent-relative window_pos takes the same px deltas.
818
+ hand_move = bool(hand_move and Toggles.Melty.window_move_pushes_os_edges
819
+ and _STATE["mode"] != "walls")
820
+ near, far = _STATE["edges"][axis]
821
+ seen_all = getattr(window, "_os_seen", None)
822
+ if seen_all is None or getattr(window, "_os_gen", None) != _STATE["generation"]:
823
+ seen_all = window._os_seen = {}
824
+ window._os_gen = _STATE["generation"]
825
+ seen = seen_all.get(axis)
826
+ cur = (near[axis], far[axis])
827
+ os_moved = seen is not None and (abs(seen[0] - cur[0]) > 1e-6 or abs(seen[1] - cur[1]) > 1e-6)
828
+ if seen is None:
829
+ seen_all[axis] = cur
830
+ if not (has_pending or os_moved or hand_move):
831
+ return None
832
+ from meltygui.core.windowing.frame_geometry import EdgeProjection
833
+ ctx = Context(axis)
834
+ binding = binding if binding is not None else frame_binding(window, axis)
835
+ screen = () if _STATE["mode"] == "walls" else _STATE["screen"][axis]
836
+ ctx.shifted = EdgeProjection((near, far), screen, binding)
837
+ near, far = ctx.native
838
+ ctx.move = hand_move
839
+ ctx.os_ids = frozenset({id(near), id(far)})
840
+ ctx.os_near0 = near[axis]
841
+ ctx.os_far0 = far[axis]
842
+ i = _AXIS[axis]
843
+ # the window's frame origin: its own coordinate is relative to the
844
+ # screen origin its frame was last re-based for (the model's near
845
+ # origin less what apply_rebase has not applied yet)
846
+ ctx.base = near[axis] - _STATE["unapplied"][i] + _screen_pos(window, axis)
847
+ if _STATE["mode"] == "walls":
848
+ ctx.lists.append([near, far])
849
+ ctx.specs.append(([MIN_SIZE[i]], [None]))
850
+ ctx.walls = ctx.os_ids
851
+ else:
852
+ scr_near, scr_far = ctx.shifted.screen
853
+ ctx.lists.append([scr_near, near, far, scr_far])
854
+ ctx.specs.append(([0.0, MIN_SIZE[i], 0.0], [None, None, None]))
855
+ ctx.walls = frozenset({id(scr_near), id(scr_far)})
856
+ # the OS edges' motion since this window last saw them: a drag from
857
+ # there to here through this window's cells (foreign: the other OS
858
+ # edge and the screen are walls - the edge IS where it is, the pile
859
+ # packs against it or overflows)
860
+ if os_moved and hand_move:
861
+ # A hand-moved window is where the cursor put it: the OS edges'
862
+ # motion since it last looked (its PARENT's frame pushing the same
863
+ # edge, this very frame, ahead of the child riding with it) is
864
+ # not a foreign drag to fold through its cells - folded in, the
865
+ # edge the parent pushed OUT pushed the child back next to it. The
866
+ # push block (_solve_collisions - ctx.move) handles the rest.
867
+ os_moved = False
868
+ if os_moved:
869
+ # (the foreign drag is forced to its floor right after it is
870
+ # solved - columns._solve_collisions - so the edge ends where it
871
+ # really is whatever the pile could give)
872
+ near[axis], far[axis] = seen
873
+ endpoints = [(near, seen[0], cur[0]), (far, seen[1], cur[1])]
874
+ # A translating minimum-width frame must open up before its
875
+ # trailing edge pushes through it. Near-first on a rightward move
876
+ # walls that edge against the OLD far edge, and expands the frame
877
+ # instead of carrying the packed column edges along.
878
+ if cur[1] > seen[1]:
879
+ endpoints.reverse()
880
+ for edge, previous, target in endpoints:
881
+ if previous != target:
882
+ ctx.drags.append((edge, target - ctx.base, None))
883
+ for e in ctx.shifted:
884
+ e[axis] -= ctx.base
885
+ return ctx
886
+
887
+
888
+ def gap_lists(ctx, window_near, window_far):
889
+ """Contact or fixed-gap constraints in the local solve's coordinates."""
890
+ near, far = ctx.native
891
+ if ctx.binding is not None:
892
+ cells = ctx.binding.cells((near, far), (window_near, window_far))
893
+ else:
894
+ cells = [(near, window_near, 0., None), (window_far, far, 0., None)]
895
+ return ([[a, b] for a, b, _floor, _cap in cells],
896
+ [([floor], [cap]) for _a, _b, floor, cap in cells])
897
+
898
+
899
+ def content_size(display):
900
+ """The size an app's root should be laid out at THIS frame: the OS
901
+ model's (far - near per axis) once begin_frame has run for the frame,
902
+ else the display's. They differ only while our own resize request is
903
+ in flight (Hyprland answers the IPC resize a frame or two later); laid
904
+ out at the lagging display size the pinned root's frame was pulled
905
+ back to the old size for a frame and pushed out again the next — the
906
+ dividers froze and jittered against it (Lukas 09-13). Foreign sizes
907
+ are folded into the model at begin_frame, so this never hides one."""
908
+ from meltygui.core.melty import Melty
909
+ if not _enabled() or _STATE["frame"] != Melty.frame_count:
910
+ return display
911
+ out = []
912
+ for axis, i in _AXIS.items():
913
+ near, far = _STATE["edges"][axis]
914
+ size = far[axis] - near[axis]
915
+ out.append(float(display[i]) if size <= 0 else float(size))
916
+ return tuple(out)
917
+
918
+
919
+ def detach(window, axis, ctx):
920
+ """After the solve: force foreign OS edges to where they really are
921
+ (a root the compositor pushed past its floor overflows), book the OS
922
+ near edge's motion for the re-base that lands with the move
923
+ (apply_rebase — every root then keeps its screen position), remember
924
+ where this window saw the edges. Returns the OS near edge's motion
925
+ this pass."""
926
+ near, far = _STATE["edges"][axis]
927
+ for source, solved in zip((near, far), ctx.native):
928
+ source[axis] = solved[axis] + ctx.base
929
+ d_os = near[axis] - ctx.os_near0
930
+ if d_os:
931
+ _STATE["unapplied"][_AXIS[axis]] += d_os
932
+ _trace(f"{axis}: OS near edge moved {d_os:+.0f} (pushed by {getattr(window, 'name', '?')})")
933
+ compensate_far(axis, far[axis] - ctx.os_far0)
934
+ window._os_seen[axis] = (near[axis], far[axis])
935
+ return d_os
936
+
937
+
938
+ def expect_own_move(dx, dy):
939
+ """A window move WE requested outside flush — titlebar's launch fit
940
+ (apply_pending_surface_size: the size and the move that keeps the
941
+ content inside the work area, one commit). Book it like flush books
942
+ its own: the expected position moves with it (in flight, so the feed's
943
+ late report is not a foreign change) and the model's edges — near AND
944
+ far, the whole window moved — with everything that remembers them.
945
+ Without this the fit's move read as the compositor resizing from the
946
+ near edge and every root was re-based by it: a @glfw_window app came up
947
+ with its content shoved right by the fit's distance (09-12). Before the
948
+ feed's first sight the move is parked for begin_frame to fold in."""
949
+ from meltygui.core.melty import Melty
950
+ for axis, i in _AXIS.items():
951
+ d = float((dx, dy)[i])
952
+ if not d:
953
+ continue
954
+ if _STATE["expected"][i] is None:
955
+ _STATE["own_move_pending"][i] += d
956
+ _trace(f"{axis}: own move {d:+.0f} parked for first sight")
957
+ continue
958
+ _STATE["expected"][i] += d
959
+ _STATE["inflight"][i] = Melty.frame_count
960
+ near, far = _STATE["edges"][axis]
961
+ near[axis] += d
962
+ far[axis] += d
963
+ for ds in _all_windows():
964
+ seen = getattr(ds, "_os_seen", None)
965
+ if seen and axis in seen:
966
+ seen[axis] = (seen[axis][0] + d, seen[axis][1] + d)
967
+ os_seen = _STATE["os_seen"][i]
968
+ if os_seen is not None:
969
+ _STATE["os_seen"][i] = (os_seen[0] + d, os_seen[1] + d)
970
+ _trace(f"{axis}: own move {d:+.0f} (launch fit)")
971
+
972
+
973
+ def apply_rebase():
974
+ """Frame START, from titlebar.apply_pending_surface_size — the moment
975
+ the queued size / move is applied: move every root by the OS near
976
+ edge's booked motion in CONTENT coordinates so it keeps its SCREEN
977
+ position, in the same commit as the surface's move."""
978
+ for axis, i in _AXIS.items():
979
+ d_far = _STATE["unapplied_far"][i]
980
+ if d_far:
981
+ _STATE["unapplied_far"][i] = 0.0
982
+ for ds in _pinned_children(axis, "far"):
983
+ _rebase(ds, axis, -d_far)
984
+ d = _STATE["unapplied"][i]
985
+ if not d:
986
+ continue
987
+ _STATE["unapplied"][i] = 0.0
988
+ for ds in _rebased_windows(axis):
989
+ _rebase(ds, axis, -d)
990
+ _trace(f"{axis}: roots re-based {-d:+.0f} with the move")
991
+
992
+
993
+ # Edges closer than this occupy one place in the flat chain (tie by role).
994
+ CHAIN_TOL = 1.0
995
+
996
+
997
+ # Edge roles in the flat chain (also the tie order): a far edge sits before
998
+ # a near edge at the same place, the OS far edge last of the fars, the OS
999
+ # near edge first of the nears.
1000
+ ROOT_FAR, OS_FAR, OS_NEAR, ROOT_NEAR = 0, 1, 2, 3
1001
+
1002
+
1003
+ def _flat_chain_ranked(os_pair, roots, axis):
1004
+ """Position-ordered window edges enclosed by the two OS boundaries.
1005
+
1006
+ Preserve real window-edge order even when a window overhangs the OS
1007
+ frame. Clamping every sort coordinate into the OS span and then sorting
1008
+ by role reversed a fully outside window's near/far pair, making a
1009
+ positive-length cycle with its own minimum-size cell.
1010
+ """
1011
+ near, far = os_pair
1012
+ ranked = []
1013
+ for _window, window_near, window_far, _floor in roots:
1014
+ ranked.extend(((window_near, ROOT_NEAR), (window_far, ROOT_FAR)))
1015
+ ranked.sort(key=lambda item: item[0][axis])
1016
+ # Make genuinely coincident window edges settle by role. Their real
1017
+ # ordering remains intact outside the surface as well as inside it.
1018
+ for index in range(1, len(ranked)):
1019
+ cursor = index
1020
+ while cursor > 0:
1021
+ previous, current = ranked[cursor - 1], ranked[cursor]
1022
+ if (abs(current[0][axis] - previous[0][axis]) >= CHAIN_TOL
1023
+ or previous[1] <= current[1]):
1024
+ break
1025
+ ranked[cursor - 1], ranked[cursor] = current, previous
1026
+ cursor -= 1
1027
+ # The OS edges are the enclosing boundaries, including for an
1028
+ # overhanging window: an inward push must still be able to reach it.
1029
+ return [(near, OS_NEAR), *ranked, (far, OS_FAR)]
1030
+
1031
+
1032
+ def _flat_chain(os_pair, roots, axis):
1033
+ return [edge for edge, _role in _flat_chain_ranked(os_pair, roots, axis)]
1034
+
1035
+
1036
+ def _chain_floor(a, role_a, b, role_b, axis):
1037
+ """The floor of the cell between consecutive chain edges ``a`` → ``b``.
1038
+ Against an OS edge: 0 (a window touches the OS edge). A window's far
1039
+ edge followed by another's near edge: 0 — the windows meet on the
1040
+ OUTSIDE and touch (the collision rects do not extend past the
1041
+ windows, Lukas 08-27). Anything else — a near edge inside another
1042
+ window, a far edge inside another window — keeps the columns' usual
1043
+ margin, the axis minimum, never more than the edges are apart now
1044
+ (freely placed overlaps must not snap apart on a first push)."""
1045
+ if role_a in (OS_FAR, OS_NEAR) or role_b in (OS_FAR, OS_NEAR):
1046
+ return 0.0
1047
+ if role_a == ROOT_FAR and role_b == ROOT_NEAR:
1048
+ return 0.0
1049
+ from meltygui.core.layout.column_core import _axis_min
1050
+ return min(_axis_min(axis), max(0.0, b[axis] - a[axis]))
1051
+
1052
+
1053
+ def _screen_pos(ds, axis):
1054
+ """A window's content position on ``axis`` as DRAWN (the wrapper's
1055
+ abs_left / abs_top — a nested window's sliver cap included: solving on
1056
+ the uncapped position made a parent drag push the OS edge out to where
1057
+ a scrolled-off child "really" was, and the studio jumped — reverted,
1058
+ Lukas 08-27)."""
1059
+ return float((ds.abs_left if axis == "x" else ds.abs_top) or 0)
1060
+
1061
+
1062
+ def _window_floor(ds, axis):
1063
+ """How far the OS-level solve may compress ``ds`` on ``axis``: its
1064
+ declared minimum (raised to its columns' pile by its own pass), never
1065
+ below the axis minimum, never above its size."""
1066
+ from meltygui.core.layout.column_core import _axis_min
1067
+ size = float(ds.width if axis == "x" else ds.height)
1068
+ declared = float((ds.min_width if axis == "x" else ds.min_height) or 0)
1069
+ return min(size, max(_axis_min(axis), declared))
1070
+
1071
+
1072
+ def _open(ds):
1073
+ return (getattr(ds, "closable", False) and ds.window_pos is not None
1074
+ and not getattr(ds, "closed", False) and getattr(ds, "expanded", True)
1075
+ and bool(ds.width) and bool(ds.height))
1076
+
1077
+
1078
+ def _root_of(ds):
1079
+ node, depth = ds, 0
1080
+ while getattr(node, "parent_window", None) is not None and depth < 64:
1081
+ node = node.parent_window
1082
+ depth += 1
1083
+ return node
1084
+
1085
+
1086
+ def _any_button_down():
1087
+ """A mouse button is held — a hand gesture is alive (the sticky replay's
1088
+ lifetime, here and in columns)."""
1089
+ from meltygui.core.melty import Melty
1090
+ handler = getattr(Melty, "event_handler", None)
1091
+ if handler is None:
1092
+ return False
1093
+ try:
1094
+ return any(handler.is_down(b) for b in ("left_mouse", "right_mouse", "middle_mouse"))
1095
+ except Exception:
1096
+ return False
1097
+
1098
+
1099
+ def _colliding_windows():
1100
+ """The windows of the OS-level solve, parents before children: the
1101
+ open movable roots and their open nested descendants."""
1102
+ roots = [ds for ds in _movable_roots() if _open(ds)]
1103
+ root_ids = {id(r) for r in roots}
1104
+ nested = [ds for ds in _all_windows()
1105
+ if getattr(ds, "parent_window", None) is not None and _open(ds)
1106
+ and id(_root_of(ds)) in root_ids]
1107
+ return roots + nested
1108
+
1109
+
1110
+ def _driving_parent_edge(child, axis):
1111
+ """Which of the PARENT's edges on ``axis`` places ``child``: the
1112
+ corner of the parent the child is positioned from
1113
+ (draw_state.parent_anchor_pos, the top-left by default) — "near", or
1114
+ "far" for a right / bottom parent corner. The parent's OTHER edge is an
1115
+ ordinary edge: a child's free edge pushing it compresses the parent."""
1116
+ anchor = getattr(child, "parent_anchor_pos", None)
1117
+ name = getattr(anchor, "value", anchor) or ""
1118
+ if axis == "x":
1119
+ return "far" if str(name).endswith("right") else "near"
1120
+ return "far" if str(name).startswith("bottom") else "near"
1121
+
1122
+
1123
+ def _driver_of(child, axis):
1124
+ """The parent edge that drives ``child`` on ``axis`` — read off the
1125
+ draw_state, never guessed: ``parent_anchor_pos`` names the corner of
1126
+ the parent the child hangs from in BOTH placement paths (the pin's
1127
+ clip_anchor_base and the unpinned parent_anchor_offset) — a right /
1128
+ bottom parent anchor is the far edge, anything else the near edge
1129
+ (Lukas 08-27: "you have the draw_state, you don't need to guess
1130
+ anything, just look at the clip option")."""
1131
+ return _driving_parent_edge(child, axis)
1132
+
1133
+
1134
+ def _frame_of(ds, axis, applied):
1135
+ """A window's frame as two fresh proxy edges (screen coords) with its
1136
+ floor and cap: it may compress to its minimum, never widen."""
1137
+ pos = _screen_pos(ds, axis)
1138
+ size = float(ds.width if axis == "x" else ds.height)
1139
+ floor = _window_floor(ds, axis)
1140
+ return {axis: applied + pos}, {axis: applied + pos + size}, floor, size
1141
+
1142
+
1143
+ def _extent_of(root, children, frames, axis):
1144
+ """``root``'s collision EXTENT as fresh proxy edges: the union of its
1145
+ frame and its ``children``'s frames (all taken from ``frames``, the
1146
+ positions phase A left them at), with the floor that keeps every
1147
+ child inside — the root compresses down to its own minimum or to the
1148
+ farthest child's far edge relative to the root's near edge, whichever
1149
+ is larger (a child overhanging the far side leaves no give at all:
1150
+ compressing a parent never moves its children, they hang off its near
1151
+ edge). Returns (near, far, floor) with floor as the cell's floor."""
1152
+ from meltygui.core.layout.column_core import _axis_min
1153
+ r_n, r_f, r_floor, _size0 = frames[id(root)]
1154
+ r_size = r_f[axis] - r_n[axis] # as phase A left it, not as built
1155
+ near, far = r_n[axis], r_f[axis]
1156
+ floor = r_floor
1157
+ for child in children:
1158
+ if getattr(child, "_capped_x" if axis == "x" else "_capped_y", False):
1159
+ continue # drawn at the display's sliver cap: scrolled off
1160
+ c_n, c_f, _fl, _sz = frames[id(child)]
1161
+ near, far = min(near, c_n[axis]), max(far, c_f[axis])
1162
+ if _driver_of(child, axis) == "far":
1163
+ # driven by the root's far edge: it moves with that edge when
1164
+ # the root compresses - the still leave the root's give
1165
+ continue
1166
+ # a child inside keeps the minimum margin from the root's far edge
1167
+ # (as phase A left); an overhanging child leaves no give at all
1168
+ margin = min(_axis_min(axis), max(0.0, r_f[axis] - c_f[axis]))
1169
+ floor = max(floor, c_f[axis] - r_n[axis] + margin)
1170
+ floor = min(floor, r_size)
1171
+ give = max(0.0, r_size - floor)
1172
+ return {axis: near}, {axis: far}, max(0.0, (far - near) - give)
1173
+
1174
+
1175
+ def solve(bindings=()):
1176
+ """Frame start, after begin_frame and the titlebar's poll: the OS
1177
+ window's own drags (queue_drag) and the OS edges' motion since the last
1178
+ solve (the compositor's resize) — the GLFW window resizes — solved
1179
+ per axis against every root window and nested window, all edges
1180
+ independent collidable objects in screen coordinates (Lukas 08-27):
1181
+ every window's frame as a cell floored at its minimum and capped at
1182
+ its size, the OS frame cell, the screen walls, and a cell between each
1183
+ consecutive pair of edges in position order (_chain_floor). No other
1184
+ drag collides windows with each other: a meltygui window's own resize and
1185
+ a hand move solve in the window's own pass (attach). A frame-pinned
1186
+ app root sends its frame-handle drags here too: it IS the GLFW frame.
1187
+
1188
+ Nested parent-relative geometry is handled in two phases below. Driver
1189
+ walls prevent counting parent-induced child motion twice; residual motion
1190
+ is then solved using parent extents. This is implementation machinery,
1191
+ not a rule that children must stay inside their Melty parent. Ordinary
1192
+ placement views are skipped for collision ancestry; their actual origin
1193
+ displacement is reconciled after layout by rebase_pin. Both axes read
1194
+ the same window geometry; their results are committed together after the
1195
+ solves. Native origin changes go through apply_rebase."""
1196
+ from meltygui.core.melty import Melty
1197
+ from meltygui.core.layout.column_core import _cells_from_lists
1198
+ from meltygui.core.layout.column_core import snap_int
1199
+ if not _enabled() or _STATE["frame"] != Melty.frame_count:
1200
+ return
1201
+ # Binding discovery is independent of the axis solves and their writes.
1202
+ surface_bindings = {(id(ds), axis): binding
1203
+ for ds in _all_windows() if _open(ds)
1204
+ for axis in _AXIS
1205
+ if (binding := frame_binding(ds, axis)) is not None}
1206
+ surface_bindings.update({(id(binding.window), binding.axis): binding for binding in bindings})
1207
+ changes = []
1208
+ anchors_booked = False
1209
+ for axis, i in _AXIS.items():
1210
+ near, far = _STATE["edges"][axis]
1211
+ cur = (near[axis], far[axis])
1212
+ seen = _STATE["os_seen"][i]
1213
+ own = _STATE["pending"][axis]
1214
+ _STATE["pending"][axis] = []
1215
+ gestures = _STATE.setdefault("gestures", {})
1216
+ released = not _any_button_down()
1217
+ # A stationary release still ends the gesture. Do this before
1218
+ # the idle fast-forward, or the next drag replays an old position.
1219
+ if released and not own:
1220
+ gestures.pop(axis, None)
1221
+ os_moved = seen is not None and (abs(seen[0] - cur[0]) > 1e-6 or abs(seen[1] - cur[1]) > 1e-6)
1222
+ if not own and not os_moved:
1223
+ continue
1224
+ if not anchors_booked:
1225
+ _book_pin_rebases()
1226
+ anchors_booked = True
1227
+ walls_mode = _STATE["mode"] == "walls"
1228
+ applied = near[axis] - _STATE["unapplied"][i]
1229
+ near0 = near[axis]
1230
+ if os_moved:
1231
+ # the OS edges start from where the windows were laid out
1232
+ # against (the graph's position order must see them THERE, or a
1233
+ # moved edge sorts past the very edges it tries to shift)
1234
+ near[axis], far[axis] = seen
1235
+ # A frame-pinned app root IS the OS frame, not another obstacle
1236
+ # inside it. A duplicate proxy root to wall off the inspector
1237
+ # since an inward OS edge could reach and compress it.
1238
+ windows = [ds for ds in _colliding_windows()
1239
+ if (id(ds), axis) not in surface_bindings
1240
+ and not getattr(ds, "_capped_x" if axis == "x" else "_capped_y", False)]
1241
+ window_ids = {id(ds) for ds in windows}
1242
+
1243
+ def collision_root(ds):
1244
+ while id(_frame_parent(ds)) in window_ids:
1245
+ ds = _frame_parent(ds)
1246
+ return ds
1247
+
1248
+ children_of = {}
1249
+ for ds in windows:
1250
+ root = collision_root(ds)
1251
+ if root is not ds:
1252
+ children_of.setdefault(id(root), []).append(ds)
1253
+ frames = {id(ds): _frame_of(ds, axis, applied) for ds in windows}
1254
+ start = {wid: (n[axis], f[axis]) for wid, (n, f, _fl, _sz) in frames.items()}
1255
+ placement_origins = {id(ds): pin_origin(ds) for ds in windows
1256
+ if id(_frame_parent(ds)) in frames
1257
+ and _has_measured_anchor(ds)}
1258
+ gesture = gestures.get(axis)
1259
+ if own:
1260
+ from meltygui.core.layout.column_core import snapshot_edges
1261
+ if gesture is None:
1262
+ gesture = gestures[axis] = {"snap": list(cur), "totals": [0.0, 0.0], "windows": {}}
1263
+ snapshots = gesture.setdefault("windows", {})
1264
+ for window in windows:
1265
+ identity = id(window)
1266
+ n, f, floor, size = frames[identity]
1267
+ if identity not in snapshots:
1268
+ snapshots[identity] = (n[axis], f[axis], floor, size, snapshot_edges(window, axis))
1269
+ saved_near, saved_far, floor, size, saved_edges = snapshots[identity]
1270
+ n[axis], f[axis] = saved_near, saved_far
1271
+ frames[identity] = n, f, floor, size
1272
+ for edge, value in saved_edges:
1273
+ edge[axis] = value
1274
+ os_floor = max([MIN_SIZE[i]] + [
1275
+ binding.minimum_size(_window_floor(binding.window, axis))
1276
+ for (_identity, bound_axis), binding in surface_bindings.items()
1277
+ if bound_axis == axis])
1278
+
1279
+ if walls_mode:
1280
+ os_list, os_spec = [near, far], ([os_floor], [None])
1281
+ walls = frozenset({id(near), id(far)})
1282
+ else:
1283
+ scr_near, scr_far = _STATE["screen"][axis]
1284
+ os_list, os_spec = [scr_near, near, far, scr_far], ([0.0, os_floor, 0.0], [None, None, None])
1285
+ walls = frozenset({id(scr_near), id(scr_far)})
1286
+
1287
+ def graph_of(cells):
1288
+ """cells: [(ds_or_None, n, f, floor, cap)] → the cell graph with
1289
+ the OS list and the flat chain over all of them."""
1290
+ lists, specs = [os_list], [os_spec]
1291
+ ranked_roots = []
1292
+ for _ds, n, f, floor, cap in cells:
1293
+ lists.append([n, f]); specs.append(([floor], [max(floor, cap)]))
1294
+ ranked_roots.append((None, n, f, floor))
1295
+ ranked = _flat_chain_ranked([near, far], ranked_roots, axis)
1296
+ if len(ranked) > 1:
1297
+ floors = [_chain_floor(a, ra, b, rb, axis)
1298
+ for (a, ra), (b, rb) in zip(ranked, ranked[1:])]
1299
+ lists.append([edge for edge, _role in ranked])
1300
+ specs.append((floors, [None] * len(floors)))
1301
+ return edge_constraints.EdgeGraph(_cells_from_lists(lists, axis, specs=specs))
1302
+
1303
+ # the drags: the OS edges' foreign motion (the edge IS where it is;
1304
+ # the other OS edge holds), then the OS window's own drags
1305
+ drags = []
1306
+ if os_moved:
1307
+ for edge, target in ((near, cur[0]), (far, cur[1])):
1308
+ if abs(target - edge[axis]) > 1e-6:
1309
+ drags.append((edge, target, False))
1310
+ # The OS window's own hand drags are STICKY (Lukas 09-13, the feel of
1311
+ # Hyprland's right-drag): the edge's target is the gesture's start
1312
+ # plus the accumulated total, not the current position plus this
1313
+ # frame's increment - a step lost to the screen wall is not
1314
+ # forgotten, and the edge returns to where it began when the hand
1315
+ # does. The affected window frames and their layout edges are
1316
+ # restored above, so their geometry returns with the OS edge.
1317
+ # The gesture lives while a mouse button is held.
1318
+ gesture = gestures.get(axis)
1319
+ for index, inc in own:
1320
+ if gesture is None:
1321
+ gesture = gestures[axis] = {"snap": list(cur), "totals": [0.0, 0.0]}
1322
+ gesture["totals"][index] += inc
1323
+ if own and gesture is not None:
1324
+ # replay from the gesture's start: BOTH edges go back to the
1325
+ # snapshot - the wall moves the opposite edge by the residual,
1326
+ # and with only the opposite edge restored that residual (the
1327
+ # whole travel past the wall, growing every frame) lands on
1328
+ # the other edge again and again: -98, -198, -545 px for 100
1329
+ # px of hand (09-13)
1330
+ near[axis], far[axis] = gesture["snap"]
1331
+ for index in (0, 1):
1332
+ if gesture["totals"][index]:
1333
+ edge = (near, far)[index]
1334
+ drags.append((edge, gesture["snap"][index] + gesture["totals"][index], True))
1335
+ if released:
1336
+ gestures.pop(axis, None) # the release frame's own increment replayed, then done
1337
+
1338
+ # ---- phase A: every window its own object. A child's own edges
1339
+ # are ordinary: its far edge compresses it to its minimum and then
1340
+ # pushes its near edge (the child moves relative to its corner),
1341
+ # like any window. The ONLY special case is a child and its DRIVING
1342
+ # edge (Lukas 08-27): the parent corner that places it
1343
+ # (_driver_of, up the ancestors) is a wall - a child pushing it
1344
+ # would move the parent, hence the corner, hence the child again.
1345
+ # For the same reason a child may push a parent edge only INWARD
1346
+ # (compressing the parent, the pending_waves window pushing its
1347
+ # parent's right edge), never OUTWARD (that moves the whole parent,
1348
+ # its cell capped at its size): the parent edge a drag would move
1349
+ # outward is a wall for that drag. A push that causes the driving edge
1350
+ # to move is the cascade phase B answers.
1351
+ cells_a = [(ds, n, f, floor, size) for ds, (n, f, floor, size) in ((ds, frames[id(ds)]) for ds in windows)]
1352
+ driving_walls, parent_near, parent_far = set(), set(), set()
1353
+ for ds in windows:
1354
+ parent = _frame_parent(ds)
1355
+ if parent is None:
1356
+ continue
1357
+ node = parent
1358
+ while node is not None and id(node) in frames:
1359
+ pn, pf, _pfl, _psz = frames[id(node)]
1360
+ driving_walls.add(id(pn) if _driver_of(ds, axis) == "near" else id(pf))
1361
+ parent_near.add(id(pn))
1362
+ parent_far.add(id(pf))
1363
+ node = _frame_parent(node)
1364
+ graph_a = graph_of(cells_a)
1365
+ residual = []
1366
+ for edge, target, cursor in drags:
1367
+ outward = parent_far if target > edge[axis] else parent_near # the parent edges this drag would push OUT
1368
+ edge_walls = walls | driving_walls | outward
1369
+ if not cursor:
1370
+ edge_walls = edge_walls | ({id(near), id(far)} - {id(edge)})
1371
+ edge_constraints.solve_edge(graph_a, edge, target, walls=frozenset(edge_walls), axis=axis)
1372
+ if abs(target - edge[axis]) > 1e-6:
1373
+ residual.append((edge, target, cursor))
1374
+ after_a = {wid: (n[axis], f[axis]) for wid, (n, f, _fl, _sz) in frames.items()}
1375
+
1376
+ # ---- phase B: the remainder, parents as blocks (their children flat)
1377
+ extents = {}
1378
+ if residual:
1379
+ cells_b = []
1380
+ for ds in windows:
1381
+ if collision_root(ds) is not ds:
1382
+ continue # folded into its root's extent
1383
+ kids = children_of.get(id(ds))
1384
+ if kids:
1385
+ n, f, floor = _extent_of(ds, kids, frames, axis)
1386
+ extents[id(ds)] = (n, f, n[axis], f[axis])
1387
+ cells_b.append((ds, n, f, floor, f[axis] - n[axis]))
1388
+ else:
1389
+ n0, f0, floor, _size = frames[id(ds)]
1390
+ n, f = {axis: n0[axis]}, {axis: f0[axis]}
1391
+ extents[id(ds)] = (n, f, n[axis], f[axis])
1392
+ cells_b.append((ds, n, f, floor, f[axis] - n[axis]))
1393
+ graph_b = graph_of(cells_b)
1394
+ for edge, target, cursor in residual:
1395
+ edge_walls = walls if cursor else (walls | ({id(near), id(far)} - {id(edge)}))
1396
+ edge_constraints.solve_edge(graph_b, edge, target, walls=frozenset(edge_walls), axis=axis)
1397
+ left = target - edge[axis]
1398
+ if cursor:
1399
+ opposite = near if left > 0 else far
1400
+ if abs(left) > 1e-6 and opposite is not edge: # the flip
1401
+ edge_constraints.solve_edge(graph_b, opposite, opposite[axis] - left, walls=frozenset(edge_walls), axis=axis)
1402
+ else:
1403
+ edge[axis] = float(target) # forced: it's there
1404
+
1405
+ # ---- fold back (children: their phase-A motion, parent-relative;
1406
+ # roots: phase A + their block's phase B; round off the size)
1407
+ for ds in windows:
1408
+ wid = id(ds)
1409
+ n0, f0 = start[wid]
1410
+ n_a, f_a = after_a[wid]
1411
+ delta, new_size = n_a - n0, f_a - n_a
1412
+ parent = _frame_parent(ds)
1413
+ root = collision_root(ds)
1414
+ block = extents.get(id(root))
1415
+ block_moved = block is not None and (
1416
+ block[0][axis] != block[2] or block[1][axis] != block[3])
1417
+ if (wid in placement_origins and placement_origins[wid] is not None
1418
+ and (after_a[id(parent)] != start[id(parent)] or block_moved)):
1419
+ # Only solver-induced parent changes need compensation.
1420
+ # Merely accepting last frame's native request must leave the
1421
+ # next intentional hand resize free to carry this child.
1422
+ _STATE["pin_rebases"].setdefault(wid, placement_origins[wid])
1423
+ if id(parent) in after_a and id(ds) not in _STATE["pin_rebases"]:
1424
+ # A parent's solved anchor edge already carries its child.
1425
+ # Write back the child's remaining motion in parent coordinates.
1426
+ anchor = 0 if _driver_of(ds, axis) == "near" else 1
1427
+ delta -= after_a[id(parent)][anchor] - start[id(parent)][anchor]
1428
+ if id(ds) in _STATE["pin_rebases"] and id(parent) in after_a:
1429
+ # An ordinary placement view may reflow when its containing
1430
+ # frame resizes. Keep the screen-space result until layout,
1431
+ # then rebase_pin subtracts that actual anchor displacement.
1432
+ # Phase B carries the whole family, so carry the measured
1433
+ # origin along with its root instead of cancelling that ride.
1434
+ # Caller-owned layout rewrites its frame next draw. A
1435
+ # temporary block shift of that stale frame is not a lasting
1436
+ # family move; carrying it would offset the child on reversal.
1437
+ if id(root) in extents and not _caller_owns_frame(root):
1438
+ n_b, f_b, nb0, fb0 = extents[id(root)]
1439
+ shift = n_b[axis] - nb0
1440
+ origin = list(_STATE["pin_rebases"][id(ds)])
1441
+ origin[i] += shift
1442
+ _STATE["pin_rebases"][id(ds)] = tuple(origin)
1443
+ if wid in extents:
1444
+ n_b, f_b, nb0, fb0 = extents[wid]
1445
+ delta += n_b[axis] - nb0
1446
+ new_size -= (fb0 - nb0) - (f_b[axis] - n_b[axis])
1447
+ changes.append((ds, axis, delta, new_size))
1448
+ seen_all = getattr(ds, "_os_seen", None)
1449
+ if seen_all is not None:
1450
+ seen_all[axis] = (near[axis], far[axis])
1451
+ d_os = near[axis] - near0
1452
+ if d_os:
1453
+ _STATE["unapplied"][i] += d_os
1454
+ _trace(f"{axis}: OS near edge moved {d_os:+.0f} (OS-level solve)")
1455
+ compensate_far(axis, far[axis] - cur[1])
1456
+ _STATE["os_seen"][i] = (near[axis], far[axis])
1457
+
1458
+ # Commit once, after both independent axis graphs have finished. In
1459
+ # particular, measuring a vertical placement must not observe a horizontal
1460
+ # parent resize before its child's corresponding position is committed.
1461
+ for ds, axis, delta, new_size in changes:
1462
+ if abs(delta) > 1e-6:
1463
+ _rebase(ds, axis, delta)
1464
+ attr = "width" if axis == "x" else "height"
1465
+ if abs(new_size - float(getattr(ds, attr))) > 0.5:
1466
+ setattr(ds, attr, snap_int(new_size))
1467
+
1468
+
1469
+ # ---------------------------------------------------------------------------
1470
+ # Apply
1471
+ # ---------------------------------------------------------------------------
1472
+
1473
+ def flush():
1474
+ """End of the frame (after end_frame's window dispatch): the model's
1475
+ size / position → ONE surface request, applied at the next frame's
1476
+ start (titlebar.apply_pending_surface_size: size + attach-offset move
1477
+ in one commit). Returns the requested content size or None."""
1478
+ import meltygui.core.windowing.titlebar as titlebar
1479
+ from meltygui.core.melty import Melty
1480
+ if not _enabled():
1481
+ return None
1482
+ window = titlebar._studio_window()
1483
+ display = Melty.display_size
1484
+ if window is None or not display:
1485
+ return None
1486
+ size, offset = [0, 0], [0, 0]
1487
+ changed = False
1488
+ for axis, i in _AXIS.items():
1489
+ near, far = _STATE["edges"][axis]
1490
+ want = far[axis] - near[axis]
1491
+ size[i] = int(round(want))
1492
+ if abs(want - float(display[i])) > 0.5:
1493
+ changed = True
1494
+ recent = _STATE["size_requests"][axis]
1495
+ if recent and recent[-1] != size[i]:
1496
+ changed = True # cancel the older request even at the observed size
1497
+ if _STATE["mode"] != "walls":
1498
+ expected = _STATE["expected"][i]
1499
+ if expected is not None and abs(near[axis] - expected) > 0.5:
1500
+ moves = _STATE["move_requests"][axis]
1501
+ previous = (expected, _STATE["size_expected"][i])
1502
+ if not moves or moves[-1] != previous:
1503
+ moves.append(previous)
1504
+ # Keep a bounded history even if the OS feed stalls.
1505
+ del moves[:-INFLIGHT_FRAMES * 2]
1506
+ offset[i] = int(round(near[axis] - expected))
1507
+ _STATE["expected"][i] = expected + offset[i]
1508
+ _STATE["inflight"][i] = Melty.frame_count
1509
+ changed = True
1510
+ _STATE["size_expected"][i] = float(size[i])
1511
+ if not changed:
1512
+ return None
1513
+ _book_pin_rebases()
1514
+ for axis, i in _AXIS.items():
1515
+ recent = _STATE["size_requests"][axis]
1516
+ if (recent or size[i] != _STATE["size_observed"][i]) and (not recent or recent[-1] != size[i]):
1517
+ recent.append(float(size[i]))
1518
+ _STATE["size_request_frame"][i] = Melty.frame_count
1519
+ for i in range(2):
1520
+ if offset[i]:
1521
+ # A size-only follow-up must not overwrite the unacknowledged move.
1522
+ _STATE["last_offset"][i] = offset[i]
1523
+ inset = int(titlebar.window_inset())
1524
+ surface = (size[0] + 2 * inset, size[1] + 2 * inset)
1525
+ if _STATE["mode"] == "x11":
1526
+ if offset[0] or offset[1]:
1527
+ try:
1528
+ x, y = glfw.get_window_pos(window)
1529
+ glfw.set_window_pos(window, x + offset[0], y + offset[1])
1530
+ except Exception:
1531
+ pass
1532
+ titlebar.request_surface_size(window, *surface)
1533
+ else:
1534
+ titlebar.request_surface_size(window, *surface, offset=tuple(offset) if any(offset) else None)
1535
+ _trace(f"flush: content {size} offset {offset} → surface {surface}")
1536
+ return tuple(size)
1537
+
1538
+
1539
+ def clamp_far_edge(abs_pos, size, display, absorbed=0.0, cap_size=True):
1540
+ """The in-display pin-and-slide of a window whose far edge passes
1541
+ ``display`` (the direct corner path when its frame edges aren't in the
1542
+ solve): (size, slide) — slide the window the other way so the far edge
1543
+ pins at the display edge, never past the display's near edge (the
1544
+ top / left stop at 0), the size capped at what fits when ``cap_size``."""
1545
+ limit = display + absorbed
1546
+ if cap_size and size > limit:
1547
+ size = limit
1548
+ slide = max(0.0, abs_pos + size - limit)
1549
+ slide = min(slide, max(0.0, abs_pos))
1550
+ if cap_size and abs_pos - slide + size > limit:
1551
+ size = max(0.0, limit - (abs_pos - slide))
1552
+ return size, slide