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