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,2153 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
from meltygui.core.layout import edge_constraints
|
|
4
|
+
|
|
5
|
+
import meltygui_imgui as imgui
|
|
6
|
+
from meltygui.hdr_color import pack_color
|
|
7
|
+
|
|
8
|
+
import meltygui.core.input.mouse_cursor as mouse_cursor
|
|
9
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
10
|
+
from meltygui.core.cache.tile_cache import snap_int
|
|
11
|
+
from meltygui.core.core_render import render_func
|
|
12
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
13
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
14
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
15
|
+
|
|
16
|
+
MIN_COLUMN_WIDTH = 60
|
|
17
|
+
# Minimum span of a ROW cell (the row-axis twin of MIN_COLUMN_WIDTH), and
|
|
18
|
+
# the floor every row band / grabber reads when a host hasn't measured
|
|
19
|
+
# its height yet.
|
|
20
|
+
MIN_ROW_HEIGHT = 40
|
|
21
|
+
EDGE_GRAB_WIDTH = 20.0
|
|
22
|
+
# Band color when the cursor is over the row (hard-coded for now).
|
|
23
|
+
HIGHLIGHT_TINT = (0,0,0, 1.0)
|
|
24
|
+
|
|
25
|
+
_NOTE = dict(name="draw_columns", tint=(0.5, 0.8, 1.0))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Edge model
|
|
30
|
+
#
|
|
31
|
+
# An EDGE is a dict with a single float on its AXIS - {"x": 123.0} for a
|
|
32
|
+
# column edge, {"y": 123.0} for a row edge - in WINDOW coordinates (offset
|
|
33
|
+
# from window.abs_left / window.abs_top). Edges are passed around BY
|
|
34
|
+
# REFERENCE: a nested columns view does not create its own far edges, it
|
|
35
|
+
# adopts the two edge objects of its enclosing cell, so a shared boundary is
|
|
36
|
+
# always the same object in both views and can never drift apart.
|
|
37
|
+
#
|
|
38
|
+
# ALL edges live on the ROOT WINDOW's draw_state, one registry per axis:
|
|
39
|
+
# every columns view in that window upserts its edge list into
|
|
40
|
+
# window._edge_views (rows views into window._row_views), and drags from any
|
|
41
|
+
# view queue on window._pending_drags (window._pending_row_drags). Once per
|
|
42
|
+
# frame - triggered by the first layout view that renders - the window
|
|
43
|
+
# resolves each queue in one collision solve over the whole cells of that
|
|
44
|
+
# axis: Each registered edge list contributes its cells (consecutive
|
|
45
|
+
# pairs of its own edges, with each cell's floor / cap), so two edges only
|
|
46
|
+
# ever push or pull each other from the cell that spans between them. A
|
|
47
|
+
# nested layout shares its far edges with the enclosing cell by reference,
|
|
48
|
+
# so contact crosses nesting levels by needed; layouts that share a cell -
|
|
49
|
+
# the columns of two different rows, the rows of two different columns -
|
|
50
|
+
# are independent however close their lines come (the the sort-by-
|
|
51
|
+
# position version this replaced collided them, 08-26). Net motion of the
|
|
52
|
+
# window-direct row's far edges drives the window size itself (with a
|
|
53
|
+
# recenter so interior lines hold their screen positions); contributors are
|
|
54
|
+
# invalidated and every view lines its cells up with the same edges in
|
|
55
|
+
# that same frame.
|
|
56
|
+
#
|
|
57
|
+
# The two axes never interact: an x-edge only ever collides with x-edges.
|
|
58
|
+
# Every function below that walks edges takes ``axis`` ("x" default, "y"
|
|
59
|
+
# for rows) and reads the coordinate under that key.
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# Per-axis registry attribute names on the window draw_state:
|
|
63
|
+
# (edge views, pending drags, frame edge pair, cell specs). Cell specs are
|
|
64
|
+
# keyed like the views: key → (floors, caps), one entry per cell in the
|
|
65
|
+
# view's list - a cell's floor / cap belong to the VIEW that owns the
|
|
66
|
+
# cell, never to the cell's the edge dict: two cells ending on the same
|
|
67
|
+
# edge (an outer column and the nested view's last column, the window
|
|
68
|
+
# frame and the last column) have their own.
|
|
69
|
+
# The fifth slot holds the across-axis BAND a keyed layout registered
|
|
70
|
+
# with its edge list (``band=`` on ColumnLayout / RowLayout): a pair of
|
|
71
|
+
# edge dicts of the OTHER axis, read live, so _edge_under_cursor can scope
|
|
72
|
+
# a layout that shares its draw_state's clip with many siblings (a tile
|
|
73
|
+
# tree built flat layout a draw_state).
|
|
74
|
+
_REGISTRY = {"x": ("_edge_views", "_pending_drags", "_frame_edges", "_edge_cells",
|
|
75
|
+
"_edge_bands"),
|
|
76
|
+
"y": ("_row_views", "_pending_row_drags", "_frame_rows", "_row_cells",
|
|
77
|
+
"_row_bands")}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _axis_min(axis):
|
|
81
|
+
"""The flat minimum span between consecutive edges on ``axis``: a
|
|
82
|
+
column's MIN_COLUMN_WIDTH along x, a row's MIN_ROW_HEIGHT along y."""
|
|
83
|
+
return float(MIN_COLUMN_WIDTH if axis == "x" else MIN_ROW_HEIGHT)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _views(window, axis):
|
|
87
|
+
return getattr(window, _REGISTRY[axis][0])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _pending(window, axis):
|
|
91
|
+
return getattr(window, _REGISTRY[axis][1])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _frame(window, axis):
|
|
95
|
+
return getattr(window, _REGISTRY[axis][2], None)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _specs(window, axis):
|
|
99
|
+
return getattr(window, _REGISTRY[axis][3])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def layout_window(draw_state):
|
|
103
|
+
"""The coordinate owner of a layout, including a window's own body.
|
|
104
|
+
|
|
105
|
+
parent_window is the containing window of an ordinary view, but the
|
|
106
|
+
PARENT of a closable window. A window body owns its own frame/cells;
|
|
107
|
+
adopting its parent's edges separates its contents from its movement.
|
|
108
|
+
"""
|
|
109
|
+
if getattr(draw_state, "closable", False):
|
|
110
|
+
return draw_state
|
|
111
|
+
return draw_state.parent_window or draw_state
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def frame_edges(window):
|
|
115
|
+
"""The window's four frame edge dicts ``(left, right, top, bottom)`` —
|
|
116
|
+
the root frame a layout drawn straight on the window adopts (the tile
|
|
117
|
+
manager's root). Runs the per-frame edge pass so the pairs exist."""
|
|
118
|
+
_ensure_window_state(window)
|
|
119
|
+
window_edge_pass(window)
|
|
120
|
+
left, right = _frame(window, "x") or ({"x": 0.0}, {"x": float(window.width or 0)})
|
|
121
|
+
top, bottom = _frame(window, "y") or ({"y": 0.0}, {"y": float(window.height or 0)})
|
|
122
|
+
return left, right, top, bottom
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _bands(window, axis):
|
|
126
|
+
return getattr(window, _REGISTRY[axis][4])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def layout_key(kind, draw_state, key=None):
|
|
130
|
+
"""The registry key of one layout on its window: ``("row", ds.id)`` for
|
|
131
|
+
a ColumnLayout / ``("rows", ds.id)`` for a RowLayout — NOT the bare
|
|
132
|
+
draw_state id (a host ds that IS the window would clobber the frame
|
|
133
|
+
entry) — with ``key`` folded in when the host builds SEVERAL layouts of
|
|
134
|
+
one axis on the same draw_state (each needs its own entry, handles and
|
|
135
|
+
drag baselines; see ``key=`` on the layouts)."""
|
|
136
|
+
return (kind, draw_state.id) if key is None else (kind, draw_state.id, key)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _column_floor(column_mins, i):
|
|
140
|
+
"""Minimum width of column ``i``: its ``column_mins`` entry when given
|
|
141
|
+
(None / 0 / a short list fall through), else MIN_COLUMN_WIDTH."""
|
|
142
|
+
if column_mins and i < len(column_mins) and column_mins[i]:
|
|
143
|
+
return float(column_mins[i])
|
|
144
|
+
return float(MIN_COLUMN_WIDTH)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _row_floor(row_mins, i):
|
|
148
|
+
"""Minimum height of row ``i`` — _column_floor's twin over
|
|
149
|
+
MIN_ROW_HEIGHT."""
|
|
150
|
+
if row_mins and i < len(row_mins) and row_mins[i]:
|
|
151
|
+
return float(row_mins[i])
|
|
152
|
+
return float(MIN_ROW_HEIGHT)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _column_cap(column_maxes, i, floor):
|
|
156
|
+
"""Maximum width of column ``i``: its ``column_maxes`` entry when given
|
|
157
|
+
(never below the column's ``floor`` — a cap under the minimum reads as
|
|
158
|
+
the minimum), else None: unbounded."""
|
|
159
|
+
if column_maxes and i < len(column_maxes) and column_maxes[i]:
|
|
160
|
+
return max(float(column_maxes[i]), float(floor))
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def resolve_column_widths(column_widths, n_cols, content_width,
|
|
165
|
+
column_mins=None, column_maxes=None, axis="x"):
|
|
166
|
+
"""Pixel width per column for ``n_cols`` columns in ``content_width``.
|
|
167
|
+
|
|
168
|
+
Entries are pixels; None (or a missing entry — the list may be shorter
|
|
169
|
+
than the column count) takes an equal share of whatever the sized columns
|
|
170
|
+
leave over. Widths never drop below the column's minimum
|
|
171
|
+
(``column_mins`` per column, else MIN_COLUMN_WIDTH) and never pass its
|
|
172
|
+
cap (``column_maxes`` per column, None = unbounded): a flex column
|
|
173
|
+
whose equal share would overshoot its cap takes the cap, and the
|
|
174
|
+
remaining flex columns split what it left on the table.
|
|
175
|
+
|
|
176
|
+
The same arithmetic sizes ROWS: ``axis="y"`` reads the row floors
|
|
177
|
+
(MIN_ROW_HEIGHT) for entries ``column_mins`` doesn't cover.
|
|
178
|
+
"""
|
|
179
|
+
available = max(0.0, float(content_width))
|
|
180
|
+
spec = list(column_widths)[:n_cols] if column_widths else []
|
|
181
|
+
spec += [None] * (n_cols - len(spec))
|
|
182
|
+
floor_of = _column_floor if axis == "x" else _row_floor
|
|
183
|
+
mins = [floor_of(column_mins, i) for i in range(n_cols)]
|
|
184
|
+
maxes = [_column_cap(column_maxes, i, mins[i]) for i in range(n_cols)]
|
|
185
|
+
|
|
186
|
+
def bounded(i, w):
|
|
187
|
+
w = max(float(w), mins[i])
|
|
188
|
+
return w if maxes[i] is None else min(w, maxes[i])
|
|
189
|
+
|
|
190
|
+
widths = [None if w is None else bounded(i, w) for i, w in enumerate(spec)]
|
|
191
|
+
remaining = available - sum(w for w in widths if w is not None)
|
|
192
|
+
pool = [i for i, w in enumerate(widths) if w is None]
|
|
193
|
+
while pool:
|
|
194
|
+
# Equal share is floored at the largest min in the pool; any pool
|
|
195
|
+
# column capped under that share takes its cap and drops out, the
|
|
196
|
+
# rest re-split the remainder.
|
|
197
|
+
share = max(max(mins[i] for i in pool), remaining / len(pool))
|
|
198
|
+
capped = [i for i in pool if maxes[i] is not None and maxes[i] < share]
|
|
199
|
+
if not capped:
|
|
200
|
+
for i in pool:
|
|
201
|
+
widths[i] = share
|
|
202
|
+
break
|
|
203
|
+
for i in capped:
|
|
204
|
+
widths[i] = maxes[i]
|
|
205
|
+
remaining -= maxes[i]
|
|
206
|
+
pool = [i for i in pool if i not in capped]
|
|
207
|
+
return widths
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _seed_edges(column_widths, n_cols, content_width, base=0.0,
|
|
211
|
+
column_mins=None, column_maxes=None, axis="x"):
|
|
212
|
+
"""Fresh edge dicts for n_cols columns (rows with ``axis="y"``):
|
|
213
|
+
n_cols+1 lines accumulated from ``base`` (window coordinates)."""
|
|
214
|
+
widths = resolve_column_widths(column_widths, n_cols, content_width,
|
|
215
|
+
column_mins=column_mins,
|
|
216
|
+
column_maxes=column_maxes, axis=axis)
|
|
217
|
+
edges = [{axis: float(base)}]
|
|
218
|
+
for w in widths:
|
|
219
|
+
edges.append({axis: edges[-1][axis] + w})
|
|
220
|
+
return edges
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _edge_min(edges, m, axis="x"):
|
|
224
|
+
"""Minimum span between edges[m-1] and edges[m]: the "min" a layout
|
|
225
|
+
stamped on the FAR edge of that column / row (column_mins / row_mins),
|
|
226
|
+
else the axis's flat minimum. The flat solve sorts every edge by
|
|
227
|
+
position and consecutive edges always bound exactly one cell of some
|
|
228
|
+
view, so a per-edge floor slots straight into the contact physics."""
|
|
229
|
+
return float(edges[m].get("min") or _axis_min(axis))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _edge_max(edges, m):
|
|
233
|
+
"""Maximum span between edges[m-1] and edges[m]: the "max" a
|
|
234
|
+
ColumnLayout stamped on the RIGHT edge of that column (column_maxes),
|
|
235
|
+
else None — unbounded. _edge_min's counterpart for the PULL side of a
|
|
236
|
+
drag: a column at its cap can't open any further, so its far edge is
|
|
237
|
+
carried along instead (see _drag_edge)."""
|
|
238
|
+
cap = edges[m].get("max")
|
|
239
|
+
return float(cap) if cap else None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _clamp_interior(edges, axis="x"):
|
|
243
|
+
"""Pack out-of-frame interior edges back inside the far edges at the
|
|
244
|
+
axis's minimum spacing. Edges only ever move on drag CONTACT, so an
|
|
245
|
+
interior edge that lands OUTSIDE its frame — edges persisted from a
|
|
246
|
+
wider window, a width change that ran while the row wasn't registered
|
|
247
|
+
— would otherwise stay there for good: it sorts past the frame edge in
|
|
248
|
+
the flat solve (never pushed) and its grab handle sits off-window where
|
|
249
|
+
no drag can reach it. In-frame, in-order edges are untouched.
|
|
250
|
+
|
|
251
|
+
Then the caps: a column persisted WIDER than its maximum (a cap added
|
|
252
|
+
since, a frame widened while the row wasn't registered) pulls its right
|
|
253
|
+
edge in, the overflow rolling rightward through capped neighbours until
|
|
254
|
+
an uncapped column absorbs it; the LAST column can't move the frame, so
|
|
255
|
+
it pulls its LEFT edge out through the same chain a drag runs, walled
|
|
256
|
+
at both frame edges — a frame wider than every cap put together leaves
|
|
257
|
+
the last column over its cap (the frame is the authority; it never
|
|
258
|
+
fights the width) and the pass is idempotent from then on."""
|
|
259
|
+
count = len(edges)
|
|
260
|
+
for m in range(count - 2, 0, -1):
|
|
261
|
+
limit = edges[m + 1][axis] - _edge_min(edges, m + 1, axis)
|
|
262
|
+
if edges[m][axis] > limit:
|
|
263
|
+
edges[m][axis] = float(limit)
|
|
264
|
+
for m in range(1, count - 1):
|
|
265
|
+
floor = edges[m - 1][axis] + _edge_min(edges, m, axis)
|
|
266
|
+
if edges[m][axis] < floor:
|
|
267
|
+
edges[m][axis] = float(floor)
|
|
268
|
+
for m in range(1, count - 1):
|
|
269
|
+
cap = _edge_max(edges, m)
|
|
270
|
+
if cap is not None and edges[m][axis] - edges[m - 1][axis] > cap:
|
|
271
|
+
edges[m][axis] = edges[m - 1][axis] + cap
|
|
272
|
+
cap = _edge_max(edges, count - 1) if count > 2 else None
|
|
273
|
+
if cap is not None and edges[-1][axis] - edges[-2][axis] > cap:
|
|
274
|
+
_drag_edge(edges, count - 2, edges[-1][axis] - cap,
|
|
275
|
+
walls=frozenset({id(edges[0]), id(edges[-1])}), axis=axis)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _ensure_window_state(window):
|
|
279
|
+
for views_attr, pending_attr, _frame_attr, specs_attr, bands_attr in _REGISTRY.values():
|
|
280
|
+
if getattr(window, views_attr, None) is None:
|
|
281
|
+
setattr(window, views_attr, {})
|
|
282
|
+
if getattr(window, pending_attr, None) is None:
|
|
283
|
+
setattr(window, pending_attr, [])
|
|
284
|
+
if getattr(window, specs_attr, None) is None:
|
|
285
|
+
setattr(window, specs_attr, {})
|
|
286
|
+
if getattr(window, bands_attr, None) is None:
|
|
287
|
+
setattr(window, bands_attr, {})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _all_edges(window, axis="x"):
|
|
291
|
+
"""Every edge of ``axis`` registered on the window, deduped by identity
|
|
292
|
+
(shared refs appear once)."""
|
|
293
|
+
seen, flat = set(), []
|
|
294
|
+
for _, edge_list in _views(window, axis).values():
|
|
295
|
+
for e in edge_list:
|
|
296
|
+
if id(e) not in seen:
|
|
297
|
+
seen.add(id(e))
|
|
298
|
+
flat.append(e)
|
|
299
|
+
return flat
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def snapshot_edges(window, axis):
|
|
303
|
+
"""One gesture's local edge positions, shared by Melty and GLFW replay."""
|
|
304
|
+
_ensure_window_state(window)
|
|
305
|
+
return [(edge, edge[axis]) for edge in _all_edges(window, axis)]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _cells_from_lists(edge_lists, axis="x", specs=()):
|
|
309
|
+
"""The CELLS of every edge list: consecutive pairs (near, far) with the
|
|
310
|
+
cell's floor and cap — from the list's per-view spec ``(floors, caps)``
|
|
311
|
+
when one is given (``specs`` parallel to ``edge_lists``; a None floor
|
|
312
|
+
is the axis minimum, a None cap unbounded), else from the "min" /
|
|
313
|
+
"max" stamped on the far edge (_edge_min / _edge_max). A pair two
|
|
314
|
+
views both register (a shared cell) merges to the strictest floor /
|
|
315
|
+
cap. A cell is the ONLY thing that links two edges — which is what
|
|
316
|
+
makes the columns of different rows independent: their dividers share
|
|
317
|
+
no cell, just the frame edges."""
|
|
318
|
+
cells = {}
|
|
319
|
+
specs = list(specs) + [None] * (len(edge_lists) - len(specs))
|
|
320
|
+
for edges, spec in zip(edge_lists, specs):
|
|
321
|
+
for m in range(1, len(edges)):
|
|
322
|
+
near, far = edges[m - 1], edges[m]
|
|
323
|
+
if near is far:
|
|
324
|
+
continue
|
|
325
|
+
if spec is not None:
|
|
326
|
+
floors, caps = spec
|
|
327
|
+
floor = floors[m - 1] if m - 1 < len(floors) else None
|
|
328
|
+
# None = the axis minimum; an explicit 0.0 is a GAP cell (the
|
|
329
|
+
# OS level's "os_near, W),) contact at zero width
|
|
330
|
+
floor = _axis_min(axis) if floor is None else float(floor)
|
|
331
|
+
cap = caps[m - 1] if m - 1 < len(caps) else None
|
|
332
|
+
# None = unbounded; an explicit 0.0 is a RIGID cell (the
|
|
333
|
+
# pinned root's link to the OS frame: os_frame.gap_cell)
|
|
334
|
+
cap = None if cap is None else max(float(cap), floor)
|
|
335
|
+
else:
|
|
336
|
+
floor, cap = _edge_min(edges, m, axis), _edge_max(edges, m)
|
|
337
|
+
key = (id(near), id(far))
|
|
338
|
+
if key in cells:
|
|
339
|
+
_near, _far, floor0, cap0 = cells[key]
|
|
340
|
+
floor = max(floor, floor0)
|
|
341
|
+
if cap is None:
|
|
342
|
+
cap = cap0
|
|
343
|
+
elif cap0 is not None:
|
|
344
|
+
cap = min(cap, cap0)
|
|
345
|
+
cells[key] = (near, far, floor, cap)
|
|
346
|
+
return list(cells.values())
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _detached_outer_edges(window, axis):
|
|
350
|
+
"""``{id(edge): (edge, "near" | "far")}`` — every layout's OUTER edge
|
|
351
|
+
(its first / last) that is DETACHED: neither the window's frame edge
|
|
352
|
+
nor an edge any other list on the window shares (a nested layout's
|
|
353
|
+
adopted cell edges are shared, a layout built with its own
|
|
354
|
+
``top_edge=`` / ``bottom_edge=`` dicts is not — the chat sidebar's
|
|
355
|
+
rows, 09-13). Such an edge is the layout's own boundary, re-dictated
|
|
356
|
+
on every build (see _window_graph), so to the hand it is where the
|
|
357
|
+
window ends: _edge_under_cursor lands a right-drag resize on the frame
|
|
358
|
+
edge behind it instead of on the dict itself."""
|
|
359
|
+
fe = _frame(window, axis) or ()
|
|
360
|
+
frame_ids = {id(e) for e in fe}
|
|
361
|
+
views = _views(window, axis)
|
|
362
|
+
seen = {}
|
|
363
|
+
for key, (ds, edges) in views.items():
|
|
364
|
+
if key == window.id:
|
|
365
|
+
continue
|
|
366
|
+
for e in edges:
|
|
367
|
+
seen[id(e)] = seen.get(id(e), 0) + 1
|
|
368
|
+
detached = {}
|
|
369
|
+
for key, (ds, edges) in views.items():
|
|
370
|
+
if key == window.id or len(edges) < 2:
|
|
371
|
+
continue
|
|
372
|
+
for e, side in ((edges[0], "near"), (edges[-1], "far")):
|
|
373
|
+
if id(e) in frame_ids or seen.get(id(e), 0) != 1:
|
|
374
|
+
continue
|
|
375
|
+
detached[id(e)] = (e, side)
|
|
376
|
+
return detached
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _window_graph(window, axis, extra_lists=(), extra_specs=()):
|
|
380
|
+
"""The cell graph of every layout registered on the window for
|
|
381
|
+
``axis`` (per-view specs where the layouts stamped them), plus
|
|
382
|
+
``extra_lists`` / ``extra_specs`` — the OS level's cells a root
|
|
383
|
+
window's pass adds for the solve only (os_frame.attach). A layout's
|
|
384
|
+
DETACHED outer edge (_detached_outer_edges) is deliberately NOT linked
|
|
385
|
+
to the frame: a layout that hands its far edges in re-dictates their
|
|
386
|
+
positions every frame (the chat sidebar builds them from the flow
|
|
387
|
+
origin), so a push the solve gave such an edge was thrown away on the
|
|
388
|
+
next build and the divider only jittered against the clamp (09-13).
|
|
389
|
+
The push stops at the layout's floor there; the right-drag RESIZE
|
|
390
|
+
gesture reaches the frame through _edge_under_cursor instead."""
|
|
391
|
+
views, specs = _views(window, axis), _specs(window, axis)
|
|
392
|
+
keys = list(views)
|
|
393
|
+
lists = [views[k][1] for k in keys] + list(extra_lists)
|
|
394
|
+
spec_list = [specs.get(k) for k in keys] + list(extra_specs)
|
|
395
|
+
return edge_constraints.EdgeGraph(_cells_from_lists(lists, axis, specs=spec_list))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _drag_edge(edges, k, target, walls=frozenset(), axis="x"):
|
|
399
|
+
"""Move edge k of ONE ordered edge list to ``target`` — the single-list
|
|
400
|
+
case of edge_constraints.solve_edge (consecutive edges of one list are its cells);
|
|
401
|
+
_clamp_interior's cap repair runs through here."""
|
|
402
|
+
graph = edge_constraints.EdgeGraph(_cells_from_lists([edges], axis))
|
|
403
|
+
edge_constraints.solve_edge(graph, edges[k], target, walls=walls, axis=axis)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _replay_hand_drags(window, axis, pending, os_ctx):
|
|
407
|
+
"""STICKY hand drags — the feel of Hyprland's right-drag resize, for
|
|
408
|
+
every drag the edge system takes (Lukas 09-13): a divider, a frame
|
|
409
|
+
handle, a right-drag latch, in a studio window or an app's root. The
|
|
410
|
+
first frame of a gesture (a cursor-driven entry while a button is
|
|
411
|
+
held) SNAPSHOTS what this axis's solve can touch: every registered
|
|
412
|
+
edge of the window, its position and size, and the OS edges of the
|
|
413
|
+
frame's link (os_ctx). Every following frame restores that snapshot
|
|
414
|
+
and re-solves with the gesture's ACCUMULATED total as an absolute
|
|
415
|
+
target — so a step the wall or a floor swallowed is not lost, a flip
|
|
416
|
+
unwinds, pushed neighbours and the OS window come back, and the
|
|
417
|
+
original state returns exactly when the hand does. One mechanism in
|
|
418
|
+
the one solve; no per-feature bookkeeping.
|
|
419
|
+
|
|
420
|
+
The snapshot is kept in SCREEN terms where the surface can move under
|
|
421
|
+
a window: a root's window_pos (a free root holds its screen position
|
|
422
|
+
when the surface moves, apply_rebase) and a frame-pinned root's edges
|
|
423
|
+
(its content origin IS the surface's) are converted through
|
|
424
|
+
os_frame.applied_origin each frame; a nested window is parent-relative
|
|
425
|
+
and restores plainly. Foreign (non-cursor) entries pass through
|
|
426
|
+
untouched. The gesture ends when no mouse button is held."""
|
|
427
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
428
|
+
gestures = getattr(window, "_edge_gestures", None)
|
|
429
|
+
if gestures is None:
|
|
430
|
+
gestures = window._edge_gestures = {}
|
|
431
|
+
hand = [item for item in pending if len(item) > 2 and bool(item[2])]
|
|
432
|
+
# A hand MOVE of this window (os_ctx.move) is a gesture too: its push
|
|
433
|
+
# of the OS edge is re-derived from the window's current position
|
|
434
|
+
# and applied against the OS edges as they were when the move began,
|
|
435
|
+
# so an edge pushed out slides BACK as the window comes back (the
|
|
436
|
+
# window's own position is already absolute, press-anchored).
|
|
437
|
+
# Only a window moved by ITS OWN hand (the wrapper's move drag has
|
|
438
|
+
# _hand_move_frame): os_ctx.move is also set for a window riding an
|
|
439
|
+
# ancestor's hand move / resize (_hand_moved), and replaying THAT as
|
|
440
|
+
# a gesture restored the OS edges to the child's move snapshot right
|
|
441
|
+
# after the parent's solve had pushed them: the push was undone every
|
|
442
|
+
# other frame and the app crawled 16 px per 200 of hand whenever a
|
|
443
|
+
# context menu was open (09-13). The ancestor's move gesture replays
|
|
444
|
+
# the OS edges; the child just rides.
|
|
445
|
+
from meltygui.core.melty import Melty
|
|
446
|
+
move = (os_ctx is not None and bool(getattr(os_ctx, "move", False))
|
|
447
|
+
and getattr(window, "_hand_move_frame", None) == Melty.frame_count)
|
|
448
|
+
released = not os_frame._any_button_down()
|
|
449
|
+
if released and ((not hand and not move) or gestures.get(axis) is None):
|
|
450
|
+
# the hand let go and nothing of its gesture is left to apply
|
|
451
|
+
gestures.pop(axis, None)
|
|
452
|
+
return pending
|
|
453
|
+
if not hand and not move:
|
|
454
|
+
return pending
|
|
455
|
+
# (a release frame that still carries a gesture's last increment
|
|
456
|
+
# replays it - dropped, the last step landed incrementally and the
|
|
457
|
+
# OS edges stayed one step short of the return, 09-13 - and pops after)
|
|
458
|
+
i = 0 if axis == "x" else 1
|
|
459
|
+
# Every surface-bound frame rides the native origin. Its local edges
|
|
460
|
+
# replay in that space; its caller-owned position must never be restored
|
|
461
|
+
# as though it belonged to a free floating window.
|
|
462
|
+
binding = os_ctx.binding if os_ctx is not None else os_frame.frame_binding(window, axis)
|
|
463
|
+
pinned = binding is not None
|
|
464
|
+
parent = os_frame._frame_parent(window)
|
|
465
|
+
is_root = parent is None
|
|
466
|
+
# Whose coordinates move with the SURFACE: a root's (free: apply_rebase
|
|
467
|
+
# re-bases its window_pos to hold the screen; pinned: its edges ride
|
|
468
|
+
# the origin) and — the studio roots of an app — a pinned root's
|
|
469
|
+
# child with no intervening window, re-based exactly like a free root
|
|
470
|
+
# (os_frame._rebased_windows). Restored plainly, that child lost every
|
|
471
|
+
# re-base the flip's push had just earned: the solve pushed the OS
|
|
472
|
+
# edge along restored it, the push doubled frame after frame and the app
|
|
473
|
+
# grew to the whole display in four frames (Lukas 09-13). A nested
|
|
474
|
+
# window under a FREE root is parent-relative and restores plainly.
|
|
475
|
+
# Caller-controlled workspace frames also follow native layout. Their
|
|
476
|
+
# descendants restore against the measured anchor, so sticky replay does
|
|
477
|
+
# not undo the compensation applied after that layout changes.
|
|
478
|
+
rides_surface = is_root or os_frame._native_layout_parent(window) is not None
|
|
479
|
+
# The CORNER the window's coordinates hang from, in SCREEN terms: the
|
|
480
|
+
# surface's applied origin (os_ctx.base is near - unapplied + the
|
|
481
|
+
# window's screen position, os_frame.attach - shift through it; never
|
|
482
|
+
# off the OS dicts, which sit SHIFTED into window coordinates for the
|
|
483
|
+
# whole solve), or, for a pinned root's child hung from the far corner
|
|
484
|
+
# (a context menu), the OS FAR edge itself: compensate_far moves such
|
|
485
|
+
# a child by every move of that edge, and restoring its position
|
|
486
|
+
# against the origin undid the compensation - it jumped right by all
|
|
487
|
+
# it had pushed, pushed by that again, and the app ran off (09-13).
|
|
488
|
+
hangs_far = (not is_root and rides_surface and os_frame._driver_of(window, axis) == "far")
|
|
489
|
+
if os_ctx is None or not rides_surface:
|
|
490
|
+
origin_now = 0.0
|
|
491
|
+
elif not is_root and os_frame._has_measured_anchor(window):
|
|
492
|
+
base = os_frame._anchor_base(window)
|
|
493
|
+
origin_now = os_ctx.base - os_frame._screen_pos(window, axis) + base[i]
|
|
494
|
+
elif hangs_far:
|
|
495
|
+
origin_now = os_ctx.native[1][axis] + os_ctx.base # the OS far edge, screen coords
|
|
496
|
+
else:
|
|
497
|
+
origin_now = os_ctx.base - os_frame._screen_pos(window, axis)
|
|
498
|
+
gesture = gestures.get(axis)
|
|
499
|
+
if gesture is not None and gesture.get("generation") != os_frame._STATE["generation"]:
|
|
500
|
+
# The geometry feed disappeared/returned or changed shape. Its
|
|
501
|
+
# screen origin is a new coordinate space; keep the current layout
|
|
502
|
+
# and start a new gesture instead of replaying the old origin.
|
|
503
|
+
gesture = None
|
|
504
|
+
if gesture is None:
|
|
505
|
+
snap = {}
|
|
506
|
+
edges = {}
|
|
507
|
+
for edge, value in snapshot_edges(window, axis):
|
|
508
|
+
edges[id(edge)] = edge
|
|
509
|
+
snap[id(edge)] = value
|
|
510
|
+
for e in (_frame(window, axis) or ()):
|
|
511
|
+
edges[id(e)] = e
|
|
512
|
+
snap[id(e)] = e[axis]
|
|
513
|
+
pos = window.window_pos[i] if window.window_pos is not None else None
|
|
514
|
+
gesture = gestures[axis] = {
|
|
515
|
+
"generation": os_frame._STATE["generation"],
|
|
516
|
+
"edges": edges, "snap": snap, "totals": {},
|
|
517
|
+
"pos": pos, "size": float(window.width if axis == "x" else window.height),
|
|
518
|
+
"origin": origin_now,
|
|
519
|
+
"os": None,
|
|
520
|
+
}
|
|
521
|
+
if gesture["os"] is None and os_ctx is not None:
|
|
522
|
+
gesture["os"] = {id(os_ctx.sources[id(e)]): e[axis] + os_ctx.base for e in os_ctx.shifted
|
|
523
|
+
if id(e) in os_ctx.os_ids and id(e) not in os_ctx.walls}
|
|
524
|
+
if not hand and not gesture["totals"]:
|
|
525
|
+
# a pure move gesture: only the OS edges are replayed; the push
|
|
526
|
+
# block in _solve_collisions re-derives the contact from where the
|
|
527
|
+
# move put the window this frame
|
|
528
|
+
if gesture["os"]:
|
|
529
|
+
for e in os_ctx.shifted:
|
|
530
|
+
if id(os_ctx.sources[id(e)]) in gesture["os"] and id(e) not in os_ctx.walls:
|
|
531
|
+
e[axis] = gesture["os"][id(os_ctx.sources[id(e)])] - os_ctx.base
|
|
532
|
+
if released:
|
|
533
|
+
gestures.pop(axis, None)
|
|
534
|
+
return pending
|
|
535
|
+
# accumulate this frame's increments (an edge born mid-gesture joins at zero value)
|
|
536
|
+
for edge, target, _cursor in hand:
|
|
537
|
+
eid = id(edge)
|
|
538
|
+
if eid not in gesture["snap"]:
|
|
539
|
+
gesture["edges"][eid] = edge
|
|
540
|
+
gesture["snap"][eid] = edge[axis]
|
|
541
|
+
gesture["totals"][eid] = gesture["totals"].get(eid, 0.0) + (target - edge[axis])
|
|
542
|
+
# restore the snapshot
|
|
543
|
+
shift = (gesture["origin"] - origin_now) if rides_surface else 0.0
|
|
544
|
+
edge_shift = shift if pinned else 0.0
|
|
545
|
+
for eid, e in gesture["edges"].items():
|
|
546
|
+
e[axis] = gesture["snap"][eid] + edge_shift
|
|
547
|
+
if not pinned and gesture["pos"] is not None and window.window_pos is not None:
|
|
548
|
+
pos = list(window.window_pos)
|
|
549
|
+
moved = gesture["pos"] + shift - pos[i]
|
|
550
|
+
pos[i] = gesture["pos"] + shift
|
|
551
|
+
window.window_pos = tuple(pos)
|
|
552
|
+
if os_ctx is not None and moved:
|
|
553
|
+
# Replay changed the window origin. Translate the context's
|
|
554
|
+
# private edges by the same amount so their screen coordinates
|
|
555
|
+
# and the native result remain unchanged.
|
|
556
|
+
os_ctx.base += moved
|
|
557
|
+
# Keep current external walls in screen coordinates when the
|
|
558
|
+
# replay changes the origin used by attach/detach.
|
|
559
|
+
for e in os_ctx.shifted:
|
|
560
|
+
e[axis] -= moved
|
|
561
|
+
if axis == "x":
|
|
562
|
+
window.width = gesture["size"]
|
|
563
|
+
else:
|
|
564
|
+
window.height = gesture["size"]
|
|
565
|
+
if os_ctx is not None and gesture["os"]:
|
|
566
|
+
for e in os_ctx.shifted:
|
|
567
|
+
if id(os_ctx.sources[id(e)]) in gesture["os"] and id(e) not in os_ctx.walls:
|
|
568
|
+
e[axis] = gesture["os"][id(os_ctx.sources[id(e)])] - os_ctx.base
|
|
569
|
+
# absolute targets from the gesture's start
|
|
570
|
+
replayed = []
|
|
571
|
+
for item in pending:
|
|
572
|
+
if len(item) > 2 and bool(item[2]):
|
|
573
|
+
edge = item[0]
|
|
574
|
+
eid = id(edge)
|
|
575
|
+
replayed.append((edge, gesture["snap"][eid] + edge_shift + gesture["totals"][eid], True))
|
|
576
|
+
else:
|
|
577
|
+
replayed.append(item)
|
|
578
|
+
# an edge dragged early in the gesture but idle this frame keeps its total
|
|
579
|
+
active = {id(item[0]) for item in hand}
|
|
580
|
+
for eid, total in gesture["totals"].items():
|
|
581
|
+
if eid not in active and total and eid in gesture["edges"]:
|
|
582
|
+
replayed.append((gesture["edges"][eid], gesture["snap"][eid] + edge_shift + total, True))
|
|
583
|
+
if released:
|
|
584
|
+
gestures.pop(axis, None)
|
|
585
|
+
return replayed
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _solve_collisions(window, axis="x", os_ctx=None):
|
|
589
|
+
"""Apply every queued drag of ``axis`` against the FULL cell graph of
|
|
590
|
+
that axis — every registered edge list's cells at once — so contact
|
|
591
|
+
chains cross view boundaries through shared edges and nothing else.
|
|
592
|
+
Returns True if anything moved. (No standing repair pass: edges only
|
|
593
|
+
move while a drag is applied.)
|
|
594
|
+
|
|
595
|
+
``os_ctx`` (os_frame.attach, root windows only) adds the OS level to
|
|
596
|
+
the graph: the OS window's frame pair, the screen walls, and two
|
|
597
|
+
zero-floor gap cells linking this window's frame to the OS frame — so
|
|
598
|
+
a cursor-driven drag pushed through the frame moves the OS edge, and
|
|
599
|
+
the OS edge pushed into the screen is clamped like any wall. Its own
|
|
600
|
+
drags (the OS window's frame edges, foreign motion since this window
|
|
601
|
+
last saw them) ride along in the same solve. Only the HAND moves the
|
|
602
|
+
OS window: a foreign size write of this window (its content grew)
|
|
603
|
+
sees the OS edges as walls.
|
|
604
|
+
|
|
605
|
+
THE FLIP: a cursor-driven drag whose owner is blocked by a wall grows
|
|
606
|
+
that window on the OPPOSITE side by the remainder — the rule meltygui
|
|
607
|
+
windows always had against the display ("pin the bottom, let the top
|
|
608
|
+
rise"), now for the OS window too, and the only way a near edge ever
|
|
609
|
+
moves outward on its own: W's right edge blocked at the screen → W's
|
|
610
|
+
left edge moves left → pushes the OS left edge → the OS window grows
|
|
611
|
+
left (and moves) → the screen's left edge stops it."""
|
|
612
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
613
|
+
pending_attr = _REGISTRY[axis][1]
|
|
614
|
+
pending = getattr(window, pending_attr)
|
|
615
|
+
setattr(window, pending_attr, [])
|
|
616
|
+
if os_ctx is not None and os_ctx.binding is not None and os_ctx.drags:
|
|
617
|
+
# The native frame already overlaps this root's contacts through
|
|
618
|
+
# the OS edge links. Its width is a fixed value, not the far edge's
|
|
619
|
+
# coordinate while the near edge is moving. Replaying right=width
|
|
620
|
+
# after the native edge drags applies that near motion twice and
|
|
621
|
+
# bypasses the column contacts the native drag just resolved.
|
|
622
|
+
# Synchronize to the native far edge in this graph's coordinates.
|
|
623
|
+
frame = _frame(window, axis) or ()
|
|
624
|
+
if len(frame) == 2:
|
|
625
|
+
pending = [(item[0], os_ctx.os_far0 - os_ctx.base - os_ctx.binding.far_gap, *item[2:])
|
|
626
|
+
if item[0] is frame[1] and not (len(item) > 2 and bool(item[2]))
|
|
627
|
+
else item for item in pending]
|
|
628
|
+
pending = _replay_hand_drags(window, axis, pending, os_ctx) # sticky: targets from the gesture's start
|
|
629
|
+
os_items = list(os_ctx.drags) if os_ctx is not None else []
|
|
630
|
+
if not pending and not os_items and not (os_ctx is not None and os_ctx.move):
|
|
631
|
+
return False
|
|
632
|
+
fe = _frame(window, axis) or ()
|
|
633
|
+
local_graph = _window_graph(window, axis)
|
|
634
|
+
if os_ctx is not None and len(fe) == 2:
|
|
635
|
+
gap_lists, gap_specs = os_frame.gap_lists(os_ctx, fe[0], fe[1])
|
|
636
|
+
os_graph = _window_graph(window, axis, extra_lists=list(os_ctx.lists) + gap_lists,
|
|
637
|
+
extra_specs=list(os_ctx.specs) + gap_specs)
|
|
638
|
+
base_walls, os_ids = os_ctx.walls, os_ctx.os_ids
|
|
639
|
+
os_pair = os_ctx.native
|
|
640
|
+
else:
|
|
641
|
+
os_graph, base_walls, os_ids, os_pair = local_graph, frozenset(), frozenset(), None
|
|
642
|
+
# A FRAME edge drag must never shove the OTHER frame edge. The danger
|
|
643
|
+
# case is the foreign-width invariant drag (window_edge_pass queues
|
|
644
|
+
# right→window.width): when an outside writer re-stamps width below the
|
|
645
|
+
# fully-compressed pile span every frame, that drag pushes clean through
|
|
646
|
+
# the pile into the left frame edge, the rebase dumps the mismat
|
|
647
|
+
# into window_pos, width springs back, and the loop re-fires each frame
|
|
648
|
+
# - the window flies off screen with no end limit. Walling the
|
|
649
|
+
# opposite frame edge clamps the drag at the pile span instead, so the
|
|
650
|
+
# mismatch resolves by width snapping back (stable), never by sliding.
|
|
651
|
+
# INTERIOR divider drags keep an empty wall set: pushing the window's
|
|
652
|
+
# left edge with a divider (slide or grow) is normal normal and only
|
|
653
|
+
# ever moves 1:1 with the cursor.
|
|
654
|
+
frame_ids = {id(e) for e in fe}
|
|
655
|
+
# ``moved`` = THIS window's frame moved. An OS edge moving on its own
|
|
656
|
+
# (the OS window's frame, a foreign move every root folds in) is not a
|
|
657
|
+
# change of this window - and reporting it as one invalidated every
|
|
658
|
+
# view of every root on every frame of an OS-window gesture (08-27:
|
|
659
|
+
# 120 → 65 fps on the corner, 12 fps on a move). The OS level reaches
|
|
660
|
+
# this window's interior only THROUGH its frame pair, so the frame
|
|
661
|
+
# pair before/after tells.
|
|
662
|
+
frame_before = tuple(e[axis] for e in fe)
|
|
663
|
+
moved = False
|
|
664
|
+
if os_ctx is not None and os_ctx.move and os_pair is not None and len(fe) == 2:
|
|
665
|
+
# A HAND MOVE of this window (its position already applied by the
|
|
666
|
+
# wrapper's move drag): the OS edge its frame now overlaps is
|
|
667
|
+
# pushed out to it — up to the screen, where the OS edge stops
|
|
668
|
+
# and the window keeps moving (a move is not clamped: windows
|
|
669
|
+
# may be dragged partly off the display, Lukas 08-27 - not
|
|
670
|
+
# above the screen limit, clamped after the solve in _frame_pass,
|
|
671
|
+
# Toggles.Melty.window_top_hard_limit). The push is
|
|
672
|
+
# the OS edge pushed to the window's edge with the screen as the
|
|
673
|
+
# wall; the window's own edges are not part of it.
|
|
674
|
+
os_near, os_far = os_pair
|
|
675
|
+
if os_far[axis] < fe[1][axis]:
|
|
676
|
+
edge_constraints.solve_edge(os_graph, os_far, fe[1][axis], walls=base_walls, axis=axis)
|
|
677
|
+
if os_near[axis] > fe[0][axis]:
|
|
678
|
+
edge_constraints.solve_edge(os_graph, os_near, fe[0][axis], walls=base_walls, axis=axis)
|
|
679
|
+
for item in os_items + pending:
|
|
680
|
+
# Optional third slot marks a CURSOR-DRIVEN drag (the right-drag
|
|
681
|
+
# corner resize queues frame edges around it): those move 1:1 with the
|
|
682
|
+
# cursor, so the foreign-width feedback loop the walls guard against
|
|
683
|
+
# can't occur - a cursor drag on one frame edge is allowed to push
|
|
684
|
+
# the other (slide the window), just like an interior divider.
|
|
685
|
+
edge, target = item[0], item[1]
|
|
686
|
+
cursor_driven = len(item) > 2 and bool(item[2])
|
|
687
|
+
is_os = os_pair is not None and (edge is os_pair[0] or edge is os_pair[1])
|
|
688
|
+
# Only the HAND moves the OS window: a foreign size write of this
|
|
689
|
+
# window (its content grew) solves in its own graph, so it
|
|
690
|
+
# overflows the screen window as intended instead of being
|
|
691
|
+
# clamped by it or growing the studio.
|
|
692
|
+
graph = os_graph if (cursor_driven or is_os) else local_graph
|
|
693
|
+
walls = set(base_walls) # the screen never moves
|
|
694
|
+
if not cursor_driven:
|
|
695
|
+
if is_os:
|
|
696
|
+
walls |= os_ids - {id(edge)} # a foreign OS edge IS where it is; the other holds
|
|
697
|
+
elif id(edge) in frame_ids:
|
|
698
|
+
walls |= frame_ids - {id(edge)}
|
|
699
|
+
walls = frozenset(walls)
|
|
700
|
+
if cursor_driven and id(edge) in frame_ids:
|
|
701
|
+
# The cap is a STOP, not a slide: clamp the cursor's target at
|
|
702
|
+
# max_height BEFORE the solve. Solved at the raw target and
|
|
703
|
+
# pulled back by _hold_frame_max after, the residual flipped
|
|
704
|
+
# onto the opposite edge (the min hold's slide) - the window
|
|
705
|
+
# kept moving while its height stayed capped (Lukas 09-04).
|
|
706
|
+
target = _cap_frame_target(window, edge, target, axis)
|
|
707
|
+
if edge_constraints.solve_edge(graph, edge, target, walls=walls, axis=axis) and not is_os:
|
|
708
|
+
moved = True
|
|
709
|
+
if is_os and not cursor_driven:
|
|
710
|
+
# A foreign OS edge IS where it is: the pile packed against it
|
|
711
|
+
# as much as it could (the wall clamp), the rest overflows.
|
|
712
|
+
# Forced HERE, before this window's own drags run - forced
|
|
713
|
+
# after the solve (detach) it ate away the push those drags
|
|
714
|
+
# gave the same edge, so the OS edge advanced only every other
|
|
715
|
+
# frame, with a double step (one sub-pixel the begin_frame gives
|
|
716
|
+
# the far edge when a drag lands on every frame "moves").
|
|
717
|
+
edge[axis] = float(target)
|
|
718
|
+
if not cursor_driven:
|
|
719
|
+
continue
|
|
720
|
+
if id(edge) in frame_ids:
|
|
721
|
+
_hold_frame_min(window, edge, axis)
|
|
722
|
+
_hold_frame_max(window, edge, axis)
|
|
723
|
+
residual = target - edge[axis]
|
|
724
|
+
if abs(residual) <= 1e-6:
|
|
725
|
+
continue
|
|
726
|
+
pair = os_pair if is_os else (fe if len(fe) == 2 else None)
|
|
727
|
+
if pair is None:
|
|
728
|
+
continue
|
|
729
|
+
opposite = pair[0] if residual > 0 else pair[1]
|
|
730
|
+
if opposite is edge: # dragged inward, blocked: nothing to flip
|
|
731
|
+
continue
|
|
732
|
+
edge_constraints.solve_edge(graph, opposite, opposite[axis] - residual, walls=walls, axis=axis)
|
|
733
|
+
if tuple(e[axis] for e in fe) != frame_before:
|
|
734
|
+
moved = True
|
|
735
|
+
return moved
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _hold_frame_min(window, dragged, axis="x"):
|
|
739
|
+
"""A cursor-driven FRAME edge dragged past the window's min_width
|
|
740
|
+
(min_height on the row axis) pushes the OTHER frame edge along — the
|
|
741
|
+
window slides (right edge dragged left) or grows (left edge dragged
|
|
742
|
+
right) — instead of stopping short of the cursor. Without this the
|
|
743
|
+
solve left the frame pair narrower than min_width, the wrapper's
|
|
744
|
+
`width = max(width, min_width)` re-stamp (after the pass) widened it
|
|
745
|
+
again, and the next pass's foreign-width invariant dragged the cursor
|
|
746
|
+
edge BACK every frame: the right edge yanked between the cursor and
|
|
747
|
+
min_width while the left edge kept sliding, width ratcheting up
|
|
748
|
+
mid-drag. Interior edges need no cascade: the pushed edge only ever
|
|
749
|
+
moves AWAY from them."""
|
|
750
|
+
fe = _frame(window, axis)
|
|
751
|
+
if not fe:
|
|
752
|
+
return
|
|
753
|
+
near, far = fe
|
|
754
|
+
floor = float((window.min_width if axis == "x" else window.min_height) or 0)
|
|
755
|
+
if far[axis] - near[axis] >= floor:
|
|
756
|
+
return
|
|
757
|
+
if dragged is far:
|
|
758
|
+
near[axis] = far[axis] - floor
|
|
759
|
+
else:
|
|
760
|
+
far[axis] = near[axis] + floor
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _cap_frame_target(window, dragged, target, axis="x"):
|
|
764
|
+
"""`target` for a cursor-driven FRAME edge, held within the window's
|
|
765
|
+
max_height (row axis only): the far edge no farther than near + cap,
|
|
766
|
+
the near edge no nearer than far − cap. Other axes / no cap: as is."""
|
|
767
|
+
if axis != "y":
|
|
768
|
+
return target
|
|
769
|
+
cap = getattr(window, "max_height", None)
|
|
770
|
+
if not cap:
|
|
771
|
+
return target
|
|
772
|
+
fe = _frame(window, axis)
|
|
773
|
+
if not fe:
|
|
774
|
+
return target
|
|
775
|
+
near, far = fe
|
|
776
|
+
if dragged is far:
|
|
777
|
+
return min(target, near[axis] + float(cap))
|
|
778
|
+
if dragged is near:
|
|
779
|
+
return max(target, far[axis] - float(cap))
|
|
780
|
+
return target
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _hold_frame_max(window, dragged, axis="x"):
|
|
784
|
+
"""The mirror of _hold_frame_min for the window's max_height (the row
|
|
785
|
+
axis only — there is no max_width): a cursor-driven FRAME edge dragged
|
|
786
|
+
past the cap STOPS at it — the dragged edge is pulled back to
|
|
787
|
+
cap-distance from the other, which never moves (the usage picker's
|
|
788
|
+
content-height ceiling, enforced mid-drag, Lukas 09-04)."""
|
|
789
|
+
if axis != "y":
|
|
790
|
+
return
|
|
791
|
+
cap = getattr(window, "max_height", None) # test stand-ins lack the slot
|
|
792
|
+
if not cap:
|
|
793
|
+
return
|
|
794
|
+
fe = _frame(window, axis)
|
|
795
|
+
if not fe:
|
|
796
|
+
return
|
|
797
|
+
near, far = fe
|
|
798
|
+
if far[axis] - near[axis] <= cap:
|
|
799
|
+
return
|
|
800
|
+
if dragged is far:
|
|
801
|
+
far[axis] = near[axis] + float(cap)
|
|
802
|
+
else:
|
|
803
|
+
near[axis] = far[axis] - float(cap)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _drag_live():
|
|
807
|
+
"""Mirror of the frozen-blit gate's activity test in blit_offscreen: a
|
|
808
|
+
mouse drag (any button) is in flight. Programmatic edge moves (foreign
|
|
809
|
+
width writes) fall outside it, so they still invalidate normally."""
|
|
810
|
+
from meltygui.core.melty import Melty
|
|
811
|
+
return Melty.resize_gesture_live()
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _defer_freeze_settle(window, draw_state):
|
|
815
|
+
"""Record a freeze_resize view whose per-frame edge invalidate was
|
|
816
|
+
skipped mid-drag; window_edge_pass settles it once on release."""
|
|
817
|
+
pending = getattr(window, "_freeze_settle", None)
|
|
818
|
+
if pending is None:
|
|
819
|
+
pending = window._freeze_settle = {}
|
|
820
|
+
pending[id(draw_state)] = draw_state
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def release_row(draw_state, keep=()):
|
|
824
|
+
"""Drop this host's registered edge rows (both axes) — except the keyed
|
|
825
|
+
layouts named in ``keep`` (an outer layout the host built in the same
|
|
826
|
+
frame around the rows it releases). Hosts that
|
|
827
|
+
sometimes render WITHOUT columns (the code editor leaving a compare
|
|
828
|
+
split) must call this on their column-less frames: window_edge_pass
|
|
829
|
+
only evicts rows whose ds is CLOSED, and a host whose ds IS its window
|
|
830
|
+
never closes while open — the stale row keeps feeding edge_under_cursor
|
|
831
|
+
and the frame-edge solve, so right-drag resizes latch an invisible
|
|
832
|
+
stale divider and the window width freezes. ColumnLayout / RowLayout
|
|
833
|
+
re-register on construction, so releasing before building them later
|
|
834
|
+
in the same frame is safe."""
|
|
835
|
+
window = layout_window(draw_state)
|
|
836
|
+
for axis, kind in (("x", "row"), ("y", "rows")):
|
|
837
|
+
for attr in (_REGISTRY[axis][0], _REGISTRY[axis][3], _REGISTRY[axis][4]):
|
|
838
|
+
table = getattr(window, attr, None)
|
|
839
|
+
if table is None:
|
|
840
|
+
continue
|
|
841
|
+
# The host entry AND every keyed one of this host (a keyed
|
|
842
|
+
# layout's key is (kind, ds.id, key)).
|
|
843
|
+
for k in [k for k in table
|
|
844
|
+
if isinstance(k, tuple) and len(k) >= 2
|
|
845
|
+
and k[0] == kind and k[1] == draw_state.id
|
|
846
|
+
and not (len(k) > 2 and k[2] in keep)]:
|
|
847
|
+
del table[k]
|
|
848
|
+
draw_state._column_container = False
|
|
849
|
+
draw_state._row_container = False
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _has_layout_ancestor(draw_state, flag):
|
|
853
|
+
"""True when another layout container carrying ``flag``
|
|
854
|
+
(``_column_container`` / ``_row_container``) sits between this view
|
|
855
|
+
and its window. Used to decide window-frame adoption: a row with NO
|
|
856
|
+
such ancestor is the window's row — however many plain wrapper views
|
|
857
|
+
(window bodies, code_file_io, …) sit in between — and adopts the
|
|
858
|
+
window's frame edges on that axis; a row under another container of
|
|
859
|
+
its axis gets its far edges from the enclosing cell instead (passed
|
|
860
|
+
refs, or its own as the deep-nesting fallback). The climb is scoped to
|
|
861
|
+
the parent window and guards the root ds's self-parent loop. A Rows
|
|
862
|
+
host is transparent to a Columns view and vice versa: the axes don't
|
|
863
|
+
share edges."""
|
|
864
|
+
window = layout_window(draw_state)
|
|
865
|
+
if window is draw_state:
|
|
866
|
+
return False
|
|
867
|
+
node = draw_state._parent
|
|
868
|
+
while node is not None and node is not node._parent:
|
|
869
|
+
if node is window:
|
|
870
|
+
break
|
|
871
|
+
if getattr(node, flag, False):
|
|
872
|
+
return True
|
|
873
|
+
node = node._parent
|
|
874
|
+
return False
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _has_columns_ancestor(draw_state):
|
|
878
|
+
return _has_layout_ancestor(draw_state, "_column_container")
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _has_rows_ancestor(draw_state):
|
|
882
|
+
return _has_layout_ancestor(draw_state, "_row_container")
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _register_frame_handles(window):
|
|
886
|
+
"""Hit regions and cursors use the same committed rectangle as drawing."""
|
|
887
|
+
win_x, win_y = window.abs_left, window.abs_top
|
|
888
|
+
half = EDGE_GRAB_WIDTH / 2
|
|
889
|
+
for axis in _REGISTRY:
|
|
890
|
+
for k, edge in enumerate(_frame(window, axis) or ()):
|
|
891
|
+
if axis == "x":
|
|
892
|
+
x = win_x + edge[axis]
|
|
893
|
+
rect = (x-half, win_y, x+half, win_y+window.height)
|
|
894
|
+
cursor = mouse_cursor.RESIZE_EW
|
|
895
|
+
else:
|
|
896
|
+
y = win_y + edge[axis]
|
|
897
|
+
rect = (win_x, y-half, win_x+window.width, y+half)
|
|
898
|
+
cursor = mouse_cursor.RESIZE_NS
|
|
899
|
+
view_id = f"win_edge_{axis}_{k}"
|
|
900
|
+
window.on_action("left_mouse_drag", view_id=view_id, rect=rect,
|
|
901
|
+
priority_delta=1, cursor=cursor)
|
|
902
|
+
window.on_action("left_mouse_down", view_id=view_id, rect=rect,
|
|
903
|
+
priority_delta=1)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def window_edge_pass(window):
|
|
907
|
+
"""Every window's FRAME edges — left/right on the x axis, top/bottom on
|
|
908
|
+
the y axis — as draggable edge objects, run once per frame per window
|
|
909
|
+
(idempotent; called from core_render's meltygui-window path for ALL
|
|
910
|
+
windows, and from the layouts as a fallback).
|
|
911
|
+
|
|
912
|
+
Per axis the window owns two edge dicts seeded at [0, size] (window
|
|
913
|
+
coords) and registered in the same solve as every column / row edge.
|
|
914
|
+
Native resizes (corner drag, right-click draw, programmatic size
|
|
915
|
+
writes) are folded in through the invariant far.pos == window.size:
|
|
916
|
+
any foreign size change queues a drag of the far frame edge, so it runs
|
|
917
|
+
the SAME collision solve and frame and lines can never desync. After
|
|
918
|
+
the solve the window lines up with its edges exactly like cells do:
|
|
919
|
+
near edge off 0 → window_pos slides + everything re-bases; far edge off
|
|
920
|
+
the size → the size follows."""
|
|
921
|
+
from meltygui.core.melty import Melty
|
|
922
|
+
if getattr(window, "_edges_frame", None) == Melty.frame_count:
|
|
923
|
+
return
|
|
924
|
+
window._edges_frame = Melty.frame_count
|
|
925
|
+
|
|
926
|
+
if not getattr(window, "expanded", True) or not window.width or not window.height:
|
|
927
|
+
return
|
|
928
|
+
_ensure_window_state(window)
|
|
929
|
+
|
|
930
|
+
# freeze_resize views whose per-frame edge invalidates were skipped
|
|
931
|
+
# mid-drag (see the edge-solve loop below) settle with ONE invalidate on
|
|
932
|
+
# release - box-unchanged views have no size mismatch to trigger the
|
|
933
|
+
# normal settled resize recapture, so without it they'd blit stale
|
|
934
|
+
# forever.
|
|
935
|
+
pending_settle = getattr(window, "_freeze_settle", None)
|
|
936
|
+
if pending_settle and not _drag_live():
|
|
937
|
+
for ds in pending_settle.values():
|
|
938
|
+
if not getattr(ds, "closed", False) and not ds.size_change:
|
|
939
|
+
ds.invalidate(note=Note(reason="freeze settle", **_NOTE))
|
|
940
|
+
window._freeze_settle = {}
|
|
941
|
+
request_render()
|
|
942
|
+
|
|
943
|
+
moved = _frame_pass(window, "x")
|
|
944
|
+
moved = _frame_pass(window, "y") or moved
|
|
945
|
+
_register_frame_handles(window)
|
|
946
|
+
|
|
947
|
+
if moved:
|
|
948
|
+
seen = set()
|
|
949
|
+
for axis in _REGISTRY:
|
|
950
|
+
for ds, _ in _views(window, axis).values():
|
|
951
|
+
if id(ds) in seen:
|
|
952
|
+
continue
|
|
953
|
+
seen.add(id(ds))
|
|
954
|
+
# A view whose box already disagrees with its tile
|
|
955
|
+
# (size_change) is live-rendered by blit_offscreen as-is -
|
|
956
|
+
# cached image and cached masks are both refused mid-resize,
|
|
957
|
+
# and the tile is rebuilt once on mouse release.
|
|
958
|
+
# Invalidating it here would only queue per-frame tile
|
|
959
|
+
# copies at the wrong size. Only views whose own box is
|
|
960
|
+
# UNCHANGED while their edges move (interior layout drags)
|
|
961
|
+
# still need the explicit invalidate or they blit stale.
|
|
962
|
+
# freeze_resize views are the exception BOTH ways: mid-drag
|
|
963
|
+
# they WANT to blit stale (that's the freeze contract -
|
|
964
|
+
# invalidating would force a live body re-render every drag
|
|
965
|
+
# frame, since a box-unchanged view never hits the size
|
|
966
|
+
# size-mismatch gate), so their invalidate is deferred to the
|
|
967
|
+
# release settle above.
|
|
968
|
+
if not ds.size_change:
|
|
969
|
+
if getattr(ds, "freeze_resize", False) and _drag_live():
|
|
970
|
+
_defer_freeze_settle(window, ds)
|
|
971
|
+
else:
|
|
972
|
+
ds.invalidate(note=Note(reason="edge solve", **_NOTE))
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _hand_moved(window, frame):
|
|
976
|
+
"""Was ``window`` moved by hand this frame — itself (the move drag
|
|
977
|
+
stamps ``_hand_move_frame``) or through an ANCESTOR it rides with (a
|
|
978
|
+
nested window's window_pos is parent-relative: dragging the parent
|
|
979
|
+
moves the child on screen, and the child's frame must push the OS
|
|
980
|
+
edge it reaches exactly as the parent's does)."""
|
|
981
|
+
node, depth = window, 0
|
|
982
|
+
while node is not None and depth < 64:
|
|
983
|
+
if getattr(node, "_hand_move_frame", None) == frame:
|
|
984
|
+
return True
|
|
985
|
+
# An ANCESTOR resized by hand this frame (_frame_pass stamps
|
|
986
|
+
# _hand_resize_frame) moves the cell the child hangs from, so the
|
|
987
|
+
# child rides exactly as under a hand move and must collide with
|
|
988
|
+
# the OS edges the same way. The window's OWN resize is not a move
|
|
989
|
+
# of it (its frame drags solve in its pass already). A frame-pinned
|
|
990
|
+
# root's resize already collided with the child on the OS graph.
|
|
991
|
+
if (node is not window and getattr(node, "_hand_resize_frame", None) == frame
|
|
992
|
+
and not getattr(node, "_frame_pinned", False)):
|
|
993
|
+
return True
|
|
994
|
+
node = getattr(node, "parent_window", None)
|
|
995
|
+
depth += 1
|
|
996
|
+
return False
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _frame_pass(window, axis):
|
|
1000
|
+
"""One axis of window_edge_pass: seed / register the frame pair, fold
|
|
1001
|
+
the foreign size change in, floor the window's minimum at the pile,
|
|
1002
|
+
offer the frame drag handles, solve, and line the window up with its
|
|
1003
|
+
frame edges. Returns True when any edge of the axis moved."""
|
|
1004
|
+
from meltygui.core.melty import Melty
|
|
1005
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1006
|
+
frame_attr = _REGISTRY[axis][2]
|
|
1007
|
+
size = window.width if axis == "x" else window.height
|
|
1008
|
+
fe = getattr(window, frame_attr, None)
|
|
1009
|
+
if not fe:
|
|
1010
|
+
fe = [{axis: 0.0}, {axis: float(size)}]
|
|
1011
|
+
setattr(window, frame_attr, fe)
|
|
1012
|
+
views, specs = _views(window, axis), _specs(window, axis)
|
|
1013
|
+
views[window.id] = (window, fe)
|
|
1014
|
+
# The frame pair is a cell too (it keeps the window's own span in the
|
|
1015
|
+
# graph): floored at the window's minimum (never below the flat floor)
|
|
1016
|
+
# and uncapped - the stamps on the far edge belong to the LAST column /
|
|
1017
|
+
# row, never to the frame. The floor is what an OS edge pushing the
|
|
1018
|
+
# window meets (os_frame): the window compresses to it, then slides.
|
|
1019
|
+
declared = float((window.min_width if axis == "x" else window.min_height) or 0)
|
|
1020
|
+
specs[window.id] = ([max(_axis_min(axis), declared)], [None])
|
|
1021
|
+
near, far = fe
|
|
1022
|
+
|
|
1023
|
+
bands = _bands(window, axis)
|
|
1024
|
+
for key, (ds, _) in list(views.items()):
|
|
1025
|
+
# A window body owns its own layouts. Evict registrations left on
|
|
1026
|
+
# its former parent after reparenting - a new ownership change.
|
|
1027
|
+
if ds is not window and (getattr(ds, "closed", False)
|
|
1028
|
+
or getattr(ds, "closable", False)):
|
|
1029
|
+
del views[key]
|
|
1030
|
+
specs.pop(key, None)
|
|
1031
|
+
bands.pop(key, None)
|
|
1032
|
+
|
|
1033
|
+
# Foreign size change since last frame → the far frame edge follows it
|
|
1034
|
+
# THROUGH the collision solve. Third slot None = NOT a cursor drag (the
|
|
1035
|
+
# OS-edge hooks below skip it; _solve_collisions keeps its position).
|
|
1036
|
+
if abs(far[axis] - size) > 0.5:
|
|
1037
|
+
_pending(window, axis).append((far, float(size), None))
|
|
1038
|
+
|
|
1039
|
+
# The frame can never out-compress the window: keep min_width /
|
|
1040
|
+
# min_height at the fully-compressed span so a pending shrink always
|
|
1041
|
+
# triggers its collision pass instead of fighting the resize latch.
|
|
1042
|
+
# Raise-only, re-stamped every frame (the wrapper rewrites the minimum
|
|
1043
|
+
# from resolved kwargs each frame).
|
|
1044
|
+
# The compressed span is the LONGEST chain of cell floors from the near
|
|
1045
|
+
# frame edge to the far one - NOT the sum over every edge, which
|
|
1046
|
+
# double-counts layouts stacked in different rows.
|
|
1047
|
+
graph = _window_graph(window, axis)
|
|
1048
|
+
span = graph.chain(near, far)
|
|
1049
|
+
need = snap_int(near[axis] + max(_axis_min(axis), span or 0.0))
|
|
1050
|
+
if axis == "x":
|
|
1051
|
+
if (window.min_width or 0) < need:
|
|
1052
|
+
window.min_width = need
|
|
1053
|
+
elif (window.min_height or 0) < need:
|
|
1054
|
+
window.min_height = need
|
|
1055
|
+
|
|
1056
|
+
# Delivered events refer to the previous committed hit regions. Consume
|
|
1057
|
+
# them before solving; register the next regions only after both axes.
|
|
1058
|
+
seen_handles = set()
|
|
1059
|
+
total = "total_dx" if axis == "x" else "total_dy"
|
|
1060
|
+
for k, e in enumerate(fe):
|
|
1061
|
+
view_id = f"win_edge_{axis}_{k}"
|
|
1062
|
+
drag = window.get_action("left_mouse_drag", view_id=view_id)
|
|
1063
|
+
press = window.get_action("left_mouse_down", view_id=view_id)
|
|
1064
|
+
if press:
|
|
1065
|
+
Melty.resize_press_frame = Melty.frame_count
|
|
1066
|
+
if not drag:
|
|
1067
|
+
continue
|
|
1068
|
+
handle = f"win_{axis}_{k}"
|
|
1069
|
+
seen_handles.add(handle)
|
|
1070
|
+
inc = _drag_inc(window, handle, drag, total=total)
|
|
1071
|
+
if inc:
|
|
1072
|
+
_pending(window, axis).append((e, e[axis] + inc, True))
|
|
1073
|
+
totals = getattr(window, "_drag_totals", None)
|
|
1074
|
+
if totals:
|
|
1075
|
+
# Prune ONLY this axis's "win_<axis>_*" namespace: when a layout
|
|
1076
|
+
# host ds IS the window (code editor compare split), its divider
|
|
1077
|
+
# handles live in this same dict - deleting them here resets the
|
|
1078
|
+
# divider's drag baseline every frame, so each frame re-applies
|
|
1079
|
+
# the FULL total and the edge flings away from the cursor. The other
|
|
1080
|
+
# axis's frame handles are pruned by its own pass.
|
|
1081
|
+
prefix = f"win_{axis}_"
|
|
1082
|
+
for h in [h for h in totals
|
|
1083
|
+
if isinstance(h, str) and h.startswith(prefix)
|
|
1084
|
+
and h not in seen_handles]:
|
|
1085
|
+
del totals[h]
|
|
1086
|
+
|
|
1087
|
+
# Native/display contact contributes private edges in this window's
|
|
1088
|
+
# coordinates. Free frames have contact gaps; surface-bound frames have
|
|
1089
|
+
# fixed gaps. Only the completed solve publishes native geometry.
|
|
1090
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
1091
|
+
# The app root's handles resize the GLFW frame. Run that drag through
|
|
1092
|
+
# the OS graph (which contains its floating children), then pack this
|
|
1093
|
+
# root's columns into the resulting size. A local solve alone let the
|
|
1094
|
+
# root push the same OS edge back out in its own pass.
|
|
1095
|
+
binding = os_frame.frame_binding(window, axis)
|
|
1096
|
+
if (binding is not None and os_frame._enabled()
|
|
1097
|
+
and os_frame._STATE["frame"] == Melty.frame_count):
|
|
1098
|
+
pending = _pending(window, axis)
|
|
1099
|
+
frame_drags = [item for item in pending
|
|
1100
|
+
if len(item) > 2 and item[2] and any(item[0] is e for e in fe)]
|
|
1101
|
+
if frame_drags:
|
|
1102
|
+
# A fixed-gap frame edge and its native edge are the same degree
|
|
1103
|
+
# of freedom. Delegate that input; interior edges remain local.
|
|
1104
|
+
for edge, target, _cursor in frame_drags:
|
|
1105
|
+
target = _cap_frame_target(window, edge, target, axis)
|
|
1106
|
+
os_frame.queue_drag(axis, 0 if edge is near else 1, target - edge[axis])
|
|
1107
|
+
pending[:] = [item for item in pending if not any(item is drag for drag in frame_drags)]
|
|
1108
|
+
os_frame.solve(bindings=(binding,))
|
|
1109
|
+
os_near, os_far = os_frame.edges(axis)
|
|
1110
|
+
size = binding.content_size(os_far[axis] - os_near[axis])
|
|
1111
|
+
setattr(window, "width" if axis == "x" else "height", snap_int(size))
|
|
1112
|
+
pending.append((far, size, None))
|
|
1113
|
+
# a cursor-driven drag of THIS window's frame (handle / corner right-drag;
|
|
1114
|
+
# a foreign size write queues None): a hand resize, stamped for the
|
|
1115
|
+
# nested windows that hang off the moved corner (_hand_moved)
|
|
1116
|
+
hand_resize = any(len(item) < 3 or bool(item[2]) for item in _pending(window, axis))
|
|
1117
|
+
import meltygui.core.diagnostics.resize_trace as resize_trace
|
|
1118
|
+
os_ctx = os_frame.attach(window, axis, has_pending=bool(_pending(window, axis)),
|
|
1119
|
+
hand_move=_hand_moved(window, Melty.frame_count), binding=binding)
|
|
1120
|
+
try:
|
|
1121
|
+
trace_solve = bool(_pending(window, axis) or os_ctx is not None)
|
|
1122
|
+
if trace_solve:
|
|
1123
|
+
resize_trace.record("solve-before", window, axis=axis,
|
|
1124
|
+
edges=resize_trace.edges(window, axis),
|
|
1125
|
+
pending=[(id(item[0]), item[1], item[2] if len(item) > 2 else None)
|
|
1126
|
+
for item in _pending(window, axis)],
|
|
1127
|
+
os_base=os_ctx.base if os_ctx is not None else None)
|
|
1128
|
+
moved = _solve_collisions(window, axis, os_ctx)
|
|
1129
|
+
if hand_resize and moved:
|
|
1130
|
+
window._hand_resize_frame = Melty.frame_count
|
|
1131
|
+
except Exception:
|
|
1132
|
+
resize_trace.record("solve-error", window, axis=axis, error=True)
|
|
1133
|
+
raise
|
|
1134
|
+
else:
|
|
1135
|
+
# Only a completed solve publishes native geometry. The context owns
|
|
1136
|
+
# private edge copies, so an exception cannot corrupt another window.
|
|
1137
|
+
if os_ctx is not None:
|
|
1138
|
+
os_frame.detach(window, axis, os_ctx)
|
|
1139
|
+
|
|
1140
|
+
# The display's TOP is a hard limit for a hand move
|
|
1141
|
+
# (Toggles.Melty.window_top_hard_limit): the solve above took the OS edge
|
|
1142
|
+
# as far as the screen lets it, the remainder - the window's top still
|
|
1143
|
+
# above the display top - is clamped by sliding the window back down.
|
|
1144
|
+
# Written to window_pos directly (a near-edge shift through the frame
|
|
1145
|
+
# pair would be a RESIZE: interior edges hold their screen position),
|
|
1146
|
+
# so it is a pure move; the press baseline is untouched, the window
|
|
1147
|
+
# re-tracks the hand if the pointer is moved. Nested windows too, pinned
|
|
1148
|
+
# or not (Lukas 08-28): window_pos is an additive offset on top of the
|
|
1149
|
+
# parent / OS anchor, so the shift holds - the child slides down inside
|
|
1150
|
+
# the parent, the parent is never moved for it.
|
|
1151
|
+
if (axis == "y" and Toggles.Melty.window_top_hard_limit
|
|
1152
|
+
and _hand_moved(window, Melty.frame_count)):
|
|
1153
|
+
limit = os_frame.display_top()
|
|
1154
|
+
top = float(window.abs_top or 0)
|
|
1155
|
+
if top < limit - 1e-6:
|
|
1156
|
+
pos = window.window_pos or (0, 0)
|
|
1157
|
+
window.window_pos = (pos[0], pos[1] + (limit - top))
|
|
1158
|
+
|
|
1159
|
+
# Line the WINDOW up with its frame edges - the same rule cells follow:
|
|
1160
|
+
# near edge off 0 → window_pos slides and every edge re-bases so
|
|
1161
|
+
# interior lines hold their screen position; far edge off the size →
|
|
1162
|
+
# the size snaps. (The OS near edge's own motion re-bases every root
|
|
1163
|
+
# when the surface actually moves - os_frame.apply_rebase, next frame
|
|
1164
|
+
# below - not here.)
|
|
1165
|
+
d_near = near[axis]
|
|
1166
|
+
if d_near:
|
|
1167
|
+
pos = window.window_pos or (0, 0)
|
|
1168
|
+
# The native body stays at its surface content inset. Its native
|
|
1169
|
+
# frame carries the near-edge motion; only local layout edges need
|
|
1170
|
+
# rebasing here. Moving the body too makes content jump until ack.
|
|
1171
|
+
if binding is None:
|
|
1172
|
+
window.window_pos = ((pos[0] + d_near, pos[1]) if axis == "x"
|
|
1173
|
+
else (pos[0], pos[1] + d_near))
|
|
1174
|
+
if axis == "x":
|
|
1175
|
+
window.width = snap_int(window.width - d_near)
|
|
1176
|
+
else:
|
|
1177
|
+
window.height = snap_int(window.height - d_near)
|
|
1178
|
+
for e in _all_edges(window, axis):
|
|
1179
|
+
e[axis] -= d_near
|
|
1180
|
+
moved = True
|
|
1181
|
+
size = window.width if axis == "x" else window.height
|
|
1182
|
+
if abs(far[axis] - size) > 0.5:
|
|
1183
|
+
size = snap_int(far[axis])
|
|
1184
|
+
if axis == "x":
|
|
1185
|
+
window.width = size
|
|
1186
|
+
else:
|
|
1187
|
+
window.height = size
|
|
1188
|
+
moved = True
|
|
1189
|
+
# Kill snap drift so the invariant check doesn't re-fire every frame.
|
|
1190
|
+
far[axis] = float(size)
|
|
1191
|
+
if trace_solve:
|
|
1192
|
+
resize_trace.record("solve-after", window, axis=axis, moved=moved,
|
|
1193
|
+
edges=resize_trace.edges(window, axis))
|
|
1194
|
+
return moved
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def reframe_window(window, d_left, d_right, extra_edges=()):
|
|
1198
|
+
"""Move the window's LEFT frame edge by ``d_left`` and its RIGHT frame
|
|
1199
|
+
edge by ``d_right`` (screen px, negative = leftward) mid-body, keeping
|
|
1200
|
+
every registered interior edge at its SCREEN position — the same
|
|
1201
|
+
"line the window up with its frame edges" rebase window_edge_pass does
|
|
1202
|
+
post-solve, callable by a body that must change the frame itself (the
|
|
1203
|
+
code editor growing left to keep its main pane in place when a compare
|
|
1204
|
+
split opens). ``extra_edges`` are edge dicts not currently registered
|
|
1205
|
+
(a row released at the top of the body and re-registered later) that
|
|
1206
|
+
must ride along too. Frame edges end up at [0, width] so the next pass
|
|
1207
|
+
sees no foreign change and re-solves nothing. Registered views are
|
|
1208
|
+
invalidated like an edge-solve move (freeze_resize views included:
|
|
1209
|
+
this is a one-shot layout change, not a drag)."""
|
|
1210
|
+
if not d_left and not d_right:
|
|
1211
|
+
return
|
|
1212
|
+
_ensure_window_state(window)
|
|
1213
|
+
fe = getattr(window, "_frame_edges", None)
|
|
1214
|
+
if not fe:
|
|
1215
|
+
fe = [{"x": 0.0}, {"x": float(window.width or 0)}]
|
|
1216
|
+
window._frame_edges = fe
|
|
1217
|
+
pos = window.window_pos or (0, 0)
|
|
1218
|
+
window.window_pos = (pos[0] + d_left, pos[1])
|
|
1219
|
+
window.width = snap_int((window.width or 0) - d_left + d_right)
|
|
1220
|
+
if d_left:
|
|
1221
|
+
seen = set()
|
|
1222
|
+
for e in list(_all_edges(window)) + list(extra_edges):
|
|
1223
|
+
if e is fe[0] or e is fe[1] or id(e) in seen:
|
|
1224
|
+
continue
|
|
1225
|
+
seen.add(id(e))
|
|
1226
|
+
e["x"] -= d_left
|
|
1227
|
+
fe[0]["x"] = 0.0
|
|
1228
|
+
fe[1]["x"] = float(window.width)
|
|
1229
|
+
for ds, _ in window._edge_views.values():
|
|
1230
|
+
if not ds.size_change:
|
|
1231
|
+
ds.invalidate(note=Note(reason="reframe window", **_NOTE))
|
|
1232
|
+
request_render()
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def reframe_axis(window, axis, d_near):
|
|
1236
|
+
"""Move the window's NEAR frame edge (left for "x", top for "y") by
|
|
1237
|
+
``d_near`` px (negative = outward) keeping every registered interior
|
|
1238
|
+
edge at its SCREEN position — reframe_window's rule on either axis
|
|
1239
|
+
with the far edge fixed. The glue of the OS-edge handoff (os_frame):
|
|
1240
|
+
the compositor moved the studio's left/top edge, every root window was
|
|
1241
|
+
re-based by the same amount, and the glued window's near edge follows
|
|
1242
|
+
the OS edge back out while its interior stays put."""
|
|
1243
|
+
if not d_near:
|
|
1244
|
+
return
|
|
1245
|
+
_ensure_window_state(window)
|
|
1246
|
+
frame_attr = _REGISTRY[axis][2]
|
|
1247
|
+
size = window.width if axis == "x" else window.height
|
|
1248
|
+
fe = getattr(window, frame_attr, None)
|
|
1249
|
+
if not fe:
|
|
1250
|
+
fe = [{axis: 0.0}, {axis: float(size or 0)}]
|
|
1251
|
+
setattr(window, frame_attr, fe)
|
|
1252
|
+
pos = window.window_pos or (0, 0)
|
|
1253
|
+
if axis == "x":
|
|
1254
|
+
window.window_pos = (pos[0] + d_near, pos[1])
|
|
1255
|
+
window.width = snap_int((window.width or 0) - d_near)
|
|
1256
|
+
size = window.width
|
|
1257
|
+
else:
|
|
1258
|
+
window.window_pos = (pos[0], pos[1] + d_near)
|
|
1259
|
+
window.height = snap_int((window.height or 0) - d_near)
|
|
1260
|
+
size = window.height
|
|
1261
|
+
seen = set()
|
|
1262
|
+
for e in _all_edges(window, axis):
|
|
1263
|
+
if e is fe[0] or e is fe[1] or id(e) in seen:
|
|
1264
|
+
continue
|
|
1265
|
+
seen.add(id(e))
|
|
1266
|
+
e[axis] -= d_near
|
|
1267
|
+
fe[0][axis] = 0.0
|
|
1268
|
+
fe[1][axis] = float(size)
|
|
1269
|
+
for ds, _ in _views(window, axis).values():
|
|
1270
|
+
if not ds.size_change:
|
|
1271
|
+
ds.invalidate(note=Note(reason="reframe axis", **_NOTE))
|
|
1272
|
+
request_render()
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _across_band(ds, axis):
|
|
1276
|
+
"""The span a view's edges of ``axis`` cover on the OTHER axis (absolute
|
|
1277
|
+
screen coords): its visible clip when it has one, else its box from the
|
|
1278
|
+
last measured band extent. An x-edge row spans a y band; a y-edge
|
|
1279
|
+
column of rows spans an x band."""
|
|
1280
|
+
clip = getattr(ds, "abs_clip_rect", None)
|
|
1281
|
+
if axis == "x":
|
|
1282
|
+
if clip:
|
|
1283
|
+
return clip[1], clip[3]
|
|
1284
|
+
top = ds.abs_top
|
|
1285
|
+
return top, top + max(getattr(ds, "_edge_lines_height", 0.0),
|
|
1286
|
+
MIN_ROW_HEIGHT)
|
|
1287
|
+
if clip:
|
|
1288
|
+
return clip[0], clip[2]
|
|
1289
|
+
left = ds.abs_left
|
|
1290
|
+
return left, left + max(getattr(ds, "_edge_lines_width", 0.0),
|
|
1291
|
+
MIN_COLUMN_WIDTH)
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _edge_under_cursor(window, axis, along, across_abs, before=False):
|
|
1295
|
+
"""The edge of ``axis`` a right-drag resize should move: the nearest
|
|
1296
|
+
edge strictly AHEAD of the cursor (``along``, window coords on the
|
|
1297
|
+
axis) — or strictly BEHIND it with ``before=True`` — among the views
|
|
1298
|
+
whose band on the other axis contains ``across_abs`` (absolute), i.e.
|
|
1299
|
+
the far edge of the INNERMOST cell under the cursor. Falls back to the
|
|
1300
|
+
window's frame edge on that side (every window registers its pair over
|
|
1301
|
+
the full window) — None only if no edges exist yet."""
|
|
1302
|
+
_ensure_window_state(window)
|
|
1303
|
+
best, best_pos = None, None
|
|
1304
|
+
bands = _bands(window, axis)
|
|
1305
|
+
other = "y" if axis == "x" else "x"
|
|
1306
|
+
origin = window.abs_top if axis == "x" else window.abs_left
|
|
1307
|
+
for key, (ds, edge_list) in _views(window, axis).items():
|
|
1308
|
+
band = bands.get(key)
|
|
1309
|
+
if band is not None:
|
|
1310
|
+
# A keyed layout's own across-axis pair (live edge dicts), not
|
|
1311
|
+
# the host's clip - its siblings share that clip.
|
|
1312
|
+
lo, hi = origin + band[0][other], origin + band[1][other]
|
|
1313
|
+
else:
|
|
1314
|
+
lo, hi = _across_band(ds, axis)
|
|
1315
|
+
if not (lo - 1 <= across_abs <= hi + 1):
|
|
1316
|
+
continue
|
|
1317
|
+
for e in edge_list:
|
|
1318
|
+
pos = e[axis]
|
|
1319
|
+
if before:
|
|
1320
|
+
if pos < along - 0.5 and (best_pos is None or pos > best_pos):
|
|
1321
|
+
best, best_pos = e, pos
|
|
1322
|
+
elif pos > along + 0.5 and (best_pos is None or pos < best_pos):
|
|
1323
|
+
best, best_pos = e, pos
|
|
1324
|
+
fe = _frame(window, axis)
|
|
1325
|
+
if best is None:
|
|
1326
|
+
if fe:
|
|
1327
|
+
best = fe[0] if before else fe[1]
|
|
1328
|
+
elif fe and id(best) in _detached_outer_edges(window, axis):
|
|
1329
|
+
# The innermost cell's far edge is a layout's DETACHED outer edge:
|
|
1330
|
+
# to the hand that cell ends at the edge, so the resize takes
|
|
1331
|
+
# the FRAME edge on that side (the gap cell in _window_graph then
|
|
1332
|
+
# carries the drag back into the layout when the frame expands
|
|
1333
|
+
# it). Latched detached, the drag only ever compressed the layout
|
|
1334
|
+
# and the window never shrank (see chat sidebar, Lukas 09-13).
|
|
1335
|
+
best = fe[0] if before else fe[1]
|
|
1336
|
+
return best
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def edge_under_cursor(window, cursor_x_window, cursor_y_abs, left=False):
|
|
1340
|
+
"""The column edge a right-drag resize should move, given the drag-start
|
|
1341
|
+
cursor (``cursor_x_window`` in window coords — offset from window.abs_left
|
|
1342
|
+
— and ``cursor_y_abs`` in absolute screen coords).
|
|
1343
|
+
|
|
1344
|
+
Returns the nearest edge dict strictly to the RIGHT of the cursor among
|
|
1345
|
+
the rows whose visible band vertically contains the cursor — i.e. the
|
|
1346
|
+
right edge of the INNERMOST column under the cursor. With ``left=True``
|
|
1347
|
+
(the double-right-drag top-left corner resize) it's the nearest edge
|
|
1348
|
+
strictly to the LEFT instead. Every window registers its own frame edges
|
|
1349
|
+
(``window.id`` entry, full-window span), so a window with NO columns — or
|
|
1350
|
+
a drag in the outermost column — lands on the window's frame edge on
|
|
1351
|
+
that side and the frame resizes exactly as a plain resize.
|
|
1352
|
+
Returns None only if no edges exist yet."""
|
|
1353
|
+
return _edge_under_cursor(window, "x", cursor_x_window, cursor_y_abs,
|
|
1354
|
+
before=left)
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def row_edge_under_cursor(window, cursor_y_window, cursor_x_abs, above=False):
|
|
1358
|
+
"""edge_under_cursor's row-axis twin: the nearest row edge strictly
|
|
1359
|
+
BELOW the drag-start cursor (``cursor_y_window`` in window coords —
|
|
1360
|
+
offset from window.abs_top; ``cursor_x_abs`` absolute) among the rows
|
|
1361
|
+
views whose band horizontally contains it — the bottom edge of the
|
|
1362
|
+
INNERMOST row under the cursor; ``above=True`` (top-left corner mode)
|
|
1363
|
+
takes the nearest edge strictly ABOVE. A window with no rows — or a
|
|
1364
|
+
drag in the outermost row — lands on the window's top/bottom frame
|
|
1365
|
+
edge, so the frame resizes exactly as a plain resize."""
|
|
1366
|
+
return _edge_under_cursor(window, "y", cursor_y_window, cursor_x_abs,
|
|
1367
|
+
before=above)
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _grab_zone(edges, k, axis="x"):
|
|
1371
|
+
"""Grab span for edge k of this view's list along its axis:
|
|
1372
|
+
EDGE_GRAB_WIDTH centered on the line, but split at the midpoint toward
|
|
1373
|
+
each neighbouring edge so adjacent zones never overlap."""
|
|
1374
|
+
pos = edges[k][axis]
|
|
1375
|
+
lo, hi = pos - EDGE_GRAB_WIDTH / 2, pos + EDGE_GRAB_WIDTH / 2
|
|
1376
|
+
if k > 0:
|
|
1377
|
+
lo = max(lo, (edges[k - 1][axis] + pos) / 2)
|
|
1378
|
+
if k < len(edges) - 1:
|
|
1379
|
+
hi = min(hi, (pos + edges[k + 1][axis]) / 2)
|
|
1380
|
+
return lo, hi
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _drag_inc(draw_state, handle, drag, total="total_dx"):
|
|
1384
|
+
"""Per-frame pixel delta for a drag handle, from the drag's running
|
|
1385
|
+
total (``total_dx``, or ``total_dy`` for a row edge) against the last
|
|
1386
|
+
seen total. State is PER-HANDLE (dict), never a shared slot: with a
|
|
1387
|
+
shared slot, two events alive in the same frame alternate ownership and
|
|
1388
|
+
each re-fires its full total against a zero baseline — moving edges
|
|
1389
|
+
that were never dragged. Entries are pruned by the caller when their
|
|
1390
|
+
handle has no event, so a new gesture always starts from a clean
|
|
1391
|
+
baseline."""
|
|
1392
|
+
totals = getattr(draw_state, "_drag_totals", None)
|
|
1393
|
+
if totals is None:
|
|
1394
|
+
totals = draw_state._drag_totals = {}
|
|
1395
|
+
last = totals.get(handle, 0.0)
|
|
1396
|
+
now = getattr(drag, total)
|
|
1397
|
+
totals[handle] = now
|
|
1398
|
+
return now - last
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
class ColumnLayout:
|
|
1402
|
+
"""Manual-cell access to the shared edge system, for renderers that draw
|
|
1403
|
+
their own cells instead of routing values through draw_columns (e.g. the
|
|
1404
|
+
code editor's structured|text tabs).
|
|
1405
|
+
|
|
1406
|
+
cols = ColumnLayout(draw_state, n_cols, column_edges=column_edges,
|
|
1407
|
+
column_widths=column_widths)
|
|
1408
|
+
for idx in range(n_cols):
|
|
1409
|
+
with cols.cell(idx) as width:
|
|
1410
|
+
any_renderer(..., width=width)
|
|
1411
|
+
cols.finish()
|
|
1412
|
+
|
|
1413
|
+
Declare ``column_edges=None`` as a named param on the host render_func
|
|
1414
|
+
and pass its resolved value through — ColumnLayout stamps
|
|
1415
|
+
draw_state.column_edges on seed/adoption changes, so auto-state persists
|
|
1416
|
+
the edge dicts exactly like draw_columns. Edge ownership, registration
|
|
1417
|
+
on the root window, drag handles and the collision solve are the same
|
|
1418
|
+
machinery; only the cell CONTENT is the caller's."""
|
|
1419
|
+
|
|
1420
|
+
def __init__(self, draw_state, n_cols, column_edges=None,
|
|
1421
|
+
column_widths=None, left_edge=None, right_edge=None,
|
|
1422
|
+
resizable=True, padding=6.0, border_color=(0.0, 0.0, 0.0, 0.9),
|
|
1423
|
+
padding_y=None, column_mins=None, column_maxes=None,
|
|
1424
|
+
key=None, band=None, persist=True):
|
|
1425
|
+
self.draw_state = draw_state
|
|
1426
|
+
self.n_cols = n_cols
|
|
1427
|
+
n_lines = self.n_lines = n_cols + 1
|
|
1428
|
+
# ``key`` - used when a host builds SEVERAL row layouts on its
|
|
1429
|
+
# draw_state (a tile tree goes flat): each gets its own registry
|
|
1430
|
+
# entry, on_action ids and drag baselines instead of clobbering
|
|
1431
|
+
# the last one's. ``band`` - the layout's own vertical extent as a
|
|
1432
|
+
# pair of ROW edge indices ``(top_edge, bottom_edge)`` (window
|
|
1433
|
+
# coords, read live): the grab zones and edge_under_cursor clip
|
|
1434
|
+
# to it rather than to the host's clip / measured row height.
|
|
1435
|
+
# ``persist=False`` hands the edges list back on ``self.edges``
|
|
1436
|
+
# without stamping ``draw_state.column_edges`` - for a host that
|
|
1437
|
+
# keeps the edges in its own storage (the tile tree).
|
|
1438
|
+
self.key = key
|
|
1439
|
+
self.band = band
|
|
1440
|
+
self.persist = persist
|
|
1441
|
+
# Cells inset by `padding` on every side and the whole row carries
|
|
1442
|
+
# ONE filled rounded band (drawn before the cells, so their rounded
|
|
1443
|
+
# backgrounds read as holes in it): the band shows through the
|
|
1444
|
+
# padding and fills the space between columns, so the row reads as
|
|
1445
|
+
# a single rounded rectangle. border_color=None (or padding 0)
|
|
1446
|
+
# disables it. `padding_y` overrides the VERTICAL inset alone
|
|
1447
|
+
# (default: same as padding) - a bandless host (the code editor's
|
|
1448
|
+
# compare split) keeps the horizontal padding around its dividers
|
|
1449
|
+
# without pushing the cells down below the row origin.
|
|
1450
|
+
# ``column_mins`` - per-column minimum widths (None entries fall
|
|
1451
|
+
# back to MIN_COLUMN_WIDTH), stamped as "min" on each column's
|
|
1452
|
+
# RIGHT edge so the column collision solve, the frame-fit clamp and
|
|
1453
|
+
# the window's min-width floor all honour them (see _edge_min).
|
|
1454
|
+
# ``column_maxes`` - per-column maximum widths (None = unbounded),
|
|
1455
|
+
# stamped as "max" the same way: a column at its cap drags its far
|
|
1456
|
+
# edge along instead of opening further, capped neighbours travel
|
|
1457
|
+
# as one, and a persisted over-cap column is packed back down on
|
|
1458
|
+
# construction (see _edge_max / _drag_edge / _clamp_interior).
|
|
1459
|
+
self.padding = float(padding)
|
|
1460
|
+
self.padding_y = float(padding if padding_y is None else padding_y)
|
|
1461
|
+
self.border_color = border_color
|
|
1462
|
+
self._cell_radius = {}
|
|
1463
|
+
|
|
1464
|
+
window = layout_window(draw_state)
|
|
1465
|
+
_ensure_window_state(window)
|
|
1466
|
+
window_edge_pass(window) # idempotent; usually ran via the wrapper
|
|
1467
|
+
self.window = window
|
|
1468
|
+
self.win_x = window.abs_left
|
|
1469
|
+
|
|
1470
|
+
origin = imgui.get_cursor_screen_pos()
|
|
1471
|
+
self.top = origin[1]
|
|
1472
|
+
self._bottom = self.top
|
|
1473
|
+
|
|
1474
|
+
# Mark this host so descendants' adoption checks can see it.
|
|
1475
|
+
draw_state._column_container = True
|
|
1476
|
+
|
|
1477
|
+
# Far edges belong to the CONTAINER: the enclosing cell when given;
|
|
1478
|
+
# else the WINDOW frame whenever no other columns container sits
|
|
1479
|
+
# above this view (plain wrappers like window bodies / code_file_io
|
|
1480
|
+
# are exceptions - their host IS the window content, so the row IS
|
|
1481
|
+
# the window's row); else own edges (deep-nesting fallback).
|
|
1482
|
+
if left_edge is None and right_edge is None and not _has_columns_ancestor(draw_state):
|
|
1483
|
+
frame_edges = getattr(window, "_frame_edges", None)
|
|
1484
|
+
if frame_edges:
|
|
1485
|
+
left_edge, right_edge = frame_edges
|
|
1486
|
+
|
|
1487
|
+
stored = column_edges if isinstance(column_edges, list) else []
|
|
1488
|
+
ok = (len(stored) == n_lines and
|
|
1489
|
+
all(isinstance(e, dict) and "x" in e for e in stored))
|
|
1490
|
+
seed_valid = True
|
|
1491
|
+
if ok:
|
|
1492
|
+
edges = list(stored)
|
|
1493
|
+
else:
|
|
1494
|
+
if left_edge is not None and right_edge is not None:
|
|
1495
|
+
base, extent = left_edge["x"], right_edge["x"] - left_edge["x"]
|
|
1496
|
+
else:
|
|
1497
|
+
base = origin[0] - self.win_x
|
|
1498
|
+
extent = float(draw_state.content_width or 0)
|
|
1499
|
+
# A seed taken before the container has laid out (content_width
|
|
1500
|
+
# ~0) stays TRANSIENT: render with it this frame, don't persist,
|
|
1501
|
+
# so a later frame re-seeds at the new extent instead of
|
|
1502
|
+
# locking up an all-minimum-width pile.
|
|
1503
|
+
# A specified width counts in full: a seed taken while the
|
|
1504
|
+
# container is too narrow to honour it (an OS window's first
|
|
1505
|
+
# frames at the compositor's default size, before the launch
|
|
1506
|
+
# fit passes) would compress that column to its floor and
|
|
1507
|
+
# persist it so (09-12).
|
|
1508
|
+
spec = list(column_widths or [])[:n_cols]
|
|
1509
|
+
spec += [None] * (n_cols - len(spec))
|
|
1510
|
+
min_total = sum(max(float(w), _column_floor(column_mins, i)) if w is not None
|
|
1511
|
+
else _column_floor(column_mins, i)
|
|
1512
|
+
for i, w in enumerate(spec))
|
|
1513
|
+
seed_valid = extent > min_total
|
|
1514
|
+
extent = max(extent, min_total)
|
|
1515
|
+
edges = _seed_edges(column_widths, n_cols, extent, base=base,
|
|
1516
|
+
column_mins=column_mins,
|
|
1517
|
+
column_maxes=column_maxes)
|
|
1518
|
+
owned = [True] * n_lines
|
|
1519
|
+
if left_edge is not None:
|
|
1520
|
+
edges[0] = left_edge
|
|
1521
|
+
owned[0] = False
|
|
1522
|
+
if right_edge is not None:
|
|
1523
|
+
edges[-1] = right_edge
|
|
1524
|
+
owned[-1] = False
|
|
1525
|
+
# Per-column minimums and maximums ride the edge dicts (re-stamped
|
|
1526
|
+
# every construction, so a hotswapped value applies live; columns
|
|
1527
|
+
# without one keep the flat MIN_COLUMN_WIDTH floor / no cap).
|
|
1528
|
+
for i in range(n_cols):
|
|
1529
|
+
floor = (column_mins[i] if column_mins and i < len(column_mins)
|
|
1530
|
+
else None)
|
|
1531
|
+
if floor:
|
|
1532
|
+
edges[i + 1]["min"] = float(floor)
|
|
1533
|
+
else:
|
|
1534
|
+
edges[i + 1].pop("min", None)
|
|
1535
|
+
cap = _column_cap(column_maxes, i, _column_floor(column_mins, i))
|
|
1536
|
+
if cap is not None:
|
|
1537
|
+
edges[i + 1]["max"] = cap
|
|
1538
|
+
else:
|
|
1539
|
+
edges[i + 1].pop("max", None)
|
|
1540
|
+
# Interior dividers always stay inside the frame - see
|
|
1541
|
+
# _clamp_interior. (Pixel widths otherwise: a frame resize only
|
|
1542
|
+
# affects the divider it touches.)
|
|
1543
|
+
_clamp_interior(edges)
|
|
1544
|
+
if persist and seed_valid and (not ok or any(a is not b for a, b in zip(stored, edges))):
|
|
1545
|
+
# Stamp so auto-state persists the list; in-place x mutations on
|
|
1546
|
+
# the dicts persist without re-stamping.
|
|
1547
|
+
draw_state.column_edges = edges
|
|
1548
|
+
self.edges = edges
|
|
1549
|
+
self.owned = owned
|
|
1550
|
+
# False while the seed was TRANSIENT (container not laid out yet):
|
|
1551
|
+
# a persist=False host should not keep these edges either.
|
|
1552
|
+
self.seed_valid = seed_valid
|
|
1553
|
+
|
|
1554
|
+
# Keyed ("row", id), NOT bare draw_state.id: when the host ds IS the
|
|
1555
|
+
# window (a window body that draws its own cells, e.g. the code
|
|
1556
|
+
# editor's compare split), draw_state.id == window.id and this entry
|
|
1557
|
+
# and window_edge_pass's frame-edge entry (window.id) clobber each
|
|
1558
|
+
# other - the interior edges then never reach _all_edges, so
|
|
1559
|
+
# _solve_collisions doesn't find a dragged edge and silently drops the
|
|
1560
|
+
# drag (the divider reads as "not draggable"). Keys are opaque
|
|
1561
|
+
# (consumers iterate them); the eviction pass matches by ds.
|
|
1562
|
+
registry_key = layout_key("row", draw_state, key)
|
|
1563
|
+
window._edge_views[registry_key] = (draw_state, edges)
|
|
1564
|
+
if band is not None:
|
|
1565
|
+
_bands(window, "x")[registry_key] = band
|
|
1566
|
+
else:
|
|
1567
|
+
_bands(window, "x").pop(registry_key, None)
|
|
1568
|
+
# The cells' floors & caps, owned by THIS view (not _REGISTRY): the
|
|
1569
|
+
# solve reads these, not the far-edge state, so a nested layout
|
|
1570
|
+
# ending on the same edge can't read (or wipe) this row's specs.
|
|
1571
|
+
_specs(window, "x")[registry_key] = (
|
|
1572
|
+
[_column_floor(column_mins, i) for i in range(n_cols)],
|
|
1573
|
+
[_column_cap(column_maxes, i, _column_floor(column_mins, i))
|
|
1574
|
+
for i in range(n_cols)])
|
|
1575
|
+
|
|
1576
|
+
# Grab handles for OWNED edges (foreign far edges already have the
|
|
1577
|
+
# container's handles on the same line).
|
|
1578
|
+
self.active_edge = None
|
|
1579
|
+
# True when the cursor sits in one of this row's column edge grab
|
|
1580
|
+
# zones - i.e. exactly when a left_mouse_drag resize would trigger.
|
|
1581
|
+
# Drives the band highlight below. Recomputed per-frame: while the row
|
|
1582
|
+
# is bounding-hovered core_render re-renders the tile every frame, so
|
|
1583
|
+
# this needs no stored state or invalidation of its own.
|
|
1584
|
+
self.edge_hovered = False
|
|
1585
|
+
if resizable:
|
|
1586
|
+
if band is not None:
|
|
1587
|
+
win_y = window.abs_top
|
|
1588
|
+
grab_top = win_y + band[0]["y"]
|
|
1589
|
+
grab_bottom = win_y + band[1]["y"]
|
|
1590
|
+
else:
|
|
1591
|
+
grab_top = self.top
|
|
1592
|
+
grab_bottom = self.top + max(
|
|
1593
|
+
getattr(draw_state, "_edge_lines_height", 0.0),
|
|
1594
|
+
MIN_ROW_HEIGHT)
|
|
1595
|
+
handle_tag = "" if key is None else f"_{key}"
|
|
1596
|
+
seen_handles = set()
|
|
1597
|
+
for k in range(n_lines):
|
|
1598
|
+
if not owned[k]:
|
|
1599
|
+
continue
|
|
1600
|
+
lo, hi = _grab_zone(edges, k)
|
|
1601
|
+
rect = (self.win_x + lo, grab_top,
|
|
1602
|
+
self.win_x + hi, grab_bottom)
|
|
1603
|
+
if draw_state.hover_eligible(rect=rect):
|
|
1604
|
+
self.edge_hovered = True
|
|
1605
|
+
drag = draw_state.on_action("left_mouse_drag",
|
|
1606
|
+
view_id=f"col_edge{handle_tag}_{k}",
|
|
1607
|
+
rect=rect, priority_delta=1,
|
|
1608
|
+
cursor=mouse_cursor.RESIZE_EW)
|
|
1609
|
+
press = draw_state.on_action("left_mouse_down",
|
|
1610
|
+
view_id=f"col_edge{handle_tag}_{k}",
|
|
1611
|
+
rect=rect, priority_delta=1)
|
|
1612
|
+
if press:
|
|
1613
|
+
# Resize press, before any drag motion: freeze hosts
|
|
1614
|
+
# snap their clean pre-drag capture this frame
|
|
1615
|
+
# (mark_start_offscreen).
|
|
1616
|
+
from meltygui.core.melty import Melty
|
|
1617
|
+
Melty.resize_press_frame = Melty.frame_count
|
|
1618
|
+
|
|
1619
|
+
if not drag:
|
|
1620
|
+
continue
|
|
1621
|
+
self.active_edge = k
|
|
1622
|
+
# Handles: the int edge index for the default layout, a
|
|
1623
|
+
# ("col", key, k) tuple for a keyed one - own namespace
|
|
1624
|
+
# per layout so the prune below never touches a sibling's
|
|
1625
|
+
# baseline.
|
|
1626
|
+
handle = k if key is None else ("col", key, k)
|
|
1627
|
+
seen_handles.add(handle)
|
|
1628
|
+
inc = _drag_inc(draw_state, handle, drag)
|
|
1629
|
+
if inc:
|
|
1630
|
+
# cursor-driven (third slot): a divider pushed past the
|
|
1631
|
+
# pile pushes the frame edge AND, through it, the OS
|
|
1632
|
+
# edge (an app's root) - see _frame_pass's handles
|
|
1633
|
+
window._pending_drags.append(
|
|
1634
|
+
(edges[k], edges[k]["x"] + inc, True))
|
|
1635
|
+
|
|
1636
|
+
totals = getattr(draw_state, "_drag_totals", None)
|
|
1637
|
+
if totals:
|
|
1638
|
+
# Mirror of the window_edge_pass prune scoping: our handles
|
|
1639
|
+
# are the int edge indices (or this key's "col" tuples);
|
|
1640
|
+
# leave the window's "win_*" baselines (and a RowLayout's
|
|
1641
|
+
# "row_*") alone - one dict when the host view IS the window
|
|
1642
|
+
# or hosts both layouts.
|
|
1643
|
+
if key is None:
|
|
1644
|
+
stale = [h for h in totals
|
|
1645
|
+
if isinstance(h, int) and h not in seen_handles]
|
|
1646
|
+
else:
|
|
1647
|
+
stale = [h for h in totals
|
|
1648
|
+
if isinstance(h, tuple) and h[:2] == ("col", key)
|
|
1649
|
+
and h not in seen_handles]
|
|
1650
|
+
for h in stale:
|
|
1651
|
+
del totals[h]
|
|
1652
|
+
if self.active_edge is not None:
|
|
1653
|
+
# size-changing views already update live (see the re-solve
|
|
1654
|
+
# gate in window_edge_pass). freeze_resize hosts skip the
|
|
1655
|
+
# per-frame invalidate too - mid-drag they serve the frozen
|
|
1656
|
+
# tile and update once on release (window_edge_pass).
|
|
1657
|
+
if not draw_state.size_change:
|
|
1658
|
+
if getattr(draw_state, "freeze_resize", False):
|
|
1659
|
+
_defer_freeze_settle(window, draw_state)
|
|
1660
|
+
else:
|
|
1661
|
+
draw_state.invalidate(note=Note(reason="edge drag",
|
|
1662
|
+
**_NOTE))
|
|
1663
|
+
# request_render()
|
|
1664
|
+
|
|
1665
|
+
# The host's VISIBLE box (live clip, screen coords) - the band's
|
|
1666
|
+
# authority for adopted sides, so content margins / the scrollbar
|
|
1667
|
+
# reserve never shave it and the padding reads even all around.
|
|
1668
|
+
self.clip = Core.melty.get_clip_rect() or draw_state.abs_clip_rect
|
|
1669
|
+
|
|
1670
|
+
# ----- the row band: one filled rounded rectangle spanning the
|
|
1671
|
+
# effective bounds, drawn BEFORE the cells so they render on top -
|
|
1672
|
+
# their rounded backgrounds are the holes, the band shows through
|
|
1673
|
+
# the padding and fills the space between columns. ADOPTED far
|
|
1674
|
+
# edges are the container's boundary, so the band takes the
|
|
1675
|
+
# container's visible box (the clip) there - a owned row's band
|
|
1676
|
+
# lands exactly in the panel band's middle. -----
|
|
1677
|
+
if self.padding > 0 and self.border_color is not None:
|
|
1678
|
+
radius = getattr(draw_state, "_band_radius", 6.0) + self.padding
|
|
1679
|
+
# Highlight the band when a divider drag is in progress
|
|
1680
|
+
# (self.active_edge) or the cursor sits over a draggable edge
|
|
1681
|
+
# (self.edge_hovered) - i.e. exactly where a resize drag is firing
|
|
1682
|
+
# or would. Both were computed against the drag grab zones above.
|
|
1683
|
+
band_color = (HIGHLIGHT_TINT
|
|
1684
|
+
if self.edge_hovered or self.active_edge is not None
|
|
1685
|
+
else self.border_color)
|
|
1686
|
+
draw_list = imgui.get_window_draw_list()
|
|
1687
|
+
draw_list.channels_set_current(Core.melty.get_channel() - 1)
|
|
1688
|
+
draw_list.add_rect_filled(
|
|
1689
|
+
snap_int(self.win_x + self._band_left()),
|
|
1690
|
+
snap_int(self.top),
|
|
1691
|
+
snap_int(self.win_x + self._band_right()),
|
|
1692
|
+
snap_int(self._band_bottom()),
|
|
1693
|
+
pack_color(*band_color),
|
|
1694
|
+
rounding=radius)
|
|
1695
|
+
draw_list.channels_set_current(Core.melty.get_channel())
|
|
1696
|
+
|
|
1697
|
+
def width(self, idx):
|
|
1698
|
+
return self.edges[idx + 1]["x"] - self.edges[idx]["x"]
|
|
1699
|
+
|
|
1700
|
+
def _band_left(self):
|
|
1701
|
+
"""Band left bound. An ADOPTED far edge sits on the container's
|
|
1702
|
+
boundary, where margins/rounding would shave the band — so the band
|
|
1703
|
+
takes the container's VISIBLE box (the clip) on that side instead.
|
|
1704
|
+
An owned edge is the row's own line and the band spans exactly to
|
|
1705
|
+
it."""
|
|
1706
|
+
if self.owned[0]:
|
|
1707
|
+
return self.edges[0]["x"]
|
|
1708
|
+
if self.clip is not None:
|
|
1709
|
+
return self.clip[0] - self.win_x
|
|
1710
|
+
return self.edges[0]["x"] + self.padding
|
|
1711
|
+
|
|
1712
|
+
def _band_right(self):
|
|
1713
|
+
if self.owned[-1]:
|
|
1714
|
+
return self.edges[-1]["x"]
|
|
1715
|
+
if self.clip is not None:
|
|
1716
|
+
return self.clip[2] - self.win_x
|
|
1717
|
+
return self.edges[-1]["x"] - self.padding
|
|
1718
|
+
|
|
1719
|
+
def _band_bottom(self):
|
|
1720
|
+
"""Band bottom (SCREEN y): the host's visible viewport — its clip —
|
|
1721
|
+
not the measured content height, so a short row still frames the
|
|
1722
|
+
whole pane, there's no settle lag, and a cell holding a
|
|
1723
|
+
height-filling child (a Rows) has room to fill. No clip: the
|
|
1724
|
+
measured height."""
|
|
1725
|
+
if self.clip is not None:
|
|
1726
|
+
return self.clip[3]
|
|
1727
|
+
return self.top + max(self.draw_state.height or 0.0, MIN_ROW_HEIGHT)
|
|
1728
|
+
|
|
1729
|
+
def _bound_left(self, idx):
|
|
1730
|
+
return self._band_left() if idx == 0 else self.edges[idx]["x"]
|
|
1731
|
+
|
|
1732
|
+
def _bound_right(self, idx):
|
|
1733
|
+
return (self._band_right() if idx == self.n_cols - 1
|
|
1734
|
+
else self.edges[idx + 1]["x"])
|
|
1735
|
+
|
|
1736
|
+
def inner_width(self, idx):
|
|
1737
|
+
"""Content width of column idx: the effective span minus padding."""
|
|
1738
|
+
return max(0.0, self._bound_right(idx) - self._bound_left(idx)
|
|
1739
|
+
- 2 * self.padding)
|
|
1740
|
+
|
|
1741
|
+
def cell_rect(self, idx, height=None):
|
|
1742
|
+
"""The screen-space (x, y, w, h) content box `cell(idx, height)`
|
|
1743
|
+
clips to — for a caller decorating a cell from OUTSIDE it, e.g. a
|
|
1744
|
+
card shadow (cast beyond the box, it would be scissored away by
|
|
1745
|
+
the cell's own clip). Without an explicit ``height`` the box runs
|
|
1746
|
+
to the band bottom (the host's visible viewport), the same
|
|
1747
|
+
authority the band paints to."""
|
|
1748
|
+
pad = self.padding
|
|
1749
|
+
pad_y = self.padding_y
|
|
1750
|
+
x0 = snap_int(self.win_x + self._bound_left(idx) + pad)
|
|
1751
|
+
y0 = snap_int(self.top + pad_y)
|
|
1752
|
+
if height is not None:
|
|
1753
|
+
y1 = snap_int(self.top) + snap_int(height) - snap_int(pad_y)
|
|
1754
|
+
else:
|
|
1755
|
+
y1 = snap_int(self._band_bottom()) - snap_int(pad_y)
|
|
1756
|
+
return x0, y0, snap_int(self.inner_width(idx)), y1 - y0
|
|
1757
|
+
|
|
1758
|
+
@contextmanager
|
|
1759
|
+
def cell(self, idx, height=None):
|
|
1760
|
+
"""Position the cursor at column idx's content origin (inset by
|
|
1761
|
+
padding from the effective bounds), clip to the content box, and
|
|
1762
|
+
yield the content width. Render the cell inside the with-block.
|
|
1763
|
+
`height` is the FULL cell band including padding — the content box
|
|
1764
|
+
is inset from it."""
|
|
1765
|
+
pad = self.padding
|
|
1766
|
+
pad_y = self.padding_y
|
|
1767
|
+
left_b = self._bound_left(idx)
|
|
1768
|
+
inner_w = self.inner_width(idx)
|
|
1769
|
+
x0, y0, box_w, box_h = self.cell_rect(idx, height)
|
|
1770
|
+
imgui.set_cursor_screen_pos((self.win_x + left_b + pad,
|
|
1771
|
+
self.top + pad_y))
|
|
1772
|
+
Core.melty.push_clip((x0, y0, x0 + box_w, y0 + box_h))
|
|
1773
|
+
# The group makes the cell origin the LINE START of everything
|
|
1774
|
+
# inside: without it only the first item sits at x0 because imgui's
|
|
1775
|
+
# newline returns the cursor to the imgui window's content x, so a
|
|
1776
|
+
# cell stacking multiple rows (eg compare split's inner column) drew
|
|
1777
|
+
# every row after the first left of the cell's clip and lost its
|
|
1778
|
+
# leading pixels (the row chevrons).
|
|
1779
|
+
imgui.begin_group()
|
|
1780
|
+
try:
|
|
1781
|
+
yield inner_w
|
|
1782
|
+
finally:
|
|
1783
|
+
imgui.end_group()
|
|
1784
|
+
Core.melty.pop_clip()
|
|
1785
|
+
bottom = imgui.get_cursor_screen_pos()[1]
|
|
1786
|
+
self._bottom = max(self._bottom, bottom + pad_y)
|
|
1787
|
+
|
|
1788
|
+
def note_child(self, idx, child_ds):
|
|
1789
|
+
"""Optional: record column idx's child draw_state so the band's
|
|
1790
|
+
outer rounding can follow the children's actual corner radius
|
|
1791
|
+
(default 6 when never noted)."""
|
|
1792
|
+
if child_ds is not None:
|
|
1793
|
+
radius = getattr(child_ds, "corner_radius", None)
|
|
1794
|
+
if radius is not None:
|
|
1795
|
+
self._cell_radius[idx] = float(radius)
|
|
1796
|
+
|
|
1797
|
+
def finish(self):
|
|
1798
|
+
"""Measure the row — the UNIFORM band height the panel draws at next
|
|
1799
|
+
frame — stamp the band rounding from the noted child corners, and
|
|
1800
|
+
leave the flow cursor below the row."""
|
|
1801
|
+
ds = self.draw_state
|
|
1802
|
+
new_h = max(self._bottom - self.top, MIN_ROW_HEIGHT)
|
|
1803
|
+
prev_h = getattr(ds, "_edge_lines_height", None)
|
|
1804
|
+
ds._edge_lines_height = new_h
|
|
1805
|
+
ds._band_radius = (max(self._cell_radius.values())
|
|
1806
|
+
if self._cell_radius else 6.0)
|
|
1807
|
+
if prev_h is None:
|
|
1808
|
+
# FIRST measure only: the band rounding and grab zones drew with
|
|
1809
|
+
# defaults this frame; catch up once. A HEIGHT CHANGE never
|
|
1810
|
+
# invalidates here - the band's pixels come from the row's clip
|
|
1811
|
+
# (or draw_state.height in the no-clip fallback), not from the
|
|
1812
|
+
# measured height, and the measured height only feeds shadow
|
|
1813
|
+
# rects (grab zones, edge_under_cursor bands) that read the fresh
|
|
1814
|
+
# stamp on the next body run without a repaint. The old height-delta
|
|
1815
|
+
# settle force-climbed every ancestor tile on any >1px height
|
|
1816
|
+
# wiggle (typing in an editor cell), recomposing unrelated columns
|
|
1817
|
+
# up the ancestor chain.
|
|
1818
|
+
if not ds.size_change:
|
|
1819
|
+
ds.invalidate(note=Note(reason="row band settle", **_NOTE))
|
|
1820
|
+
request_render()
|
|
1821
|
+
imgui.set_cursor_screen_pos((self.win_x + self.edges[0]["x"],
|
|
1822
|
+
self._bottom))
|
|
1823
|
+
imgui.dummy(0, 0)
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
class RowLayout:
|
|
1827
|
+
"""ColumnLayout's row-axis twin: manual-cell access to the shared edge
|
|
1828
|
+
system for cells stacked top to bottom.
|
|
1829
|
+
|
|
1830
|
+
rows = RowLayout(draw_state, n_rows, row_edges=row_edges,
|
|
1831
|
+
row_heights=row_heights)
|
|
1832
|
+
for idx in range(n_rows):
|
|
1833
|
+
with rows.cell(idx) as height:
|
|
1834
|
+
any_renderer(..., height=height, width=rows.inner_width())
|
|
1835
|
+
rows.finish()
|
|
1836
|
+
|
|
1837
|
+
Declare ``row_edges=None`` as a named param on the host render_func and
|
|
1838
|
+
pass its resolved value through — RowLayout stamps draw_state.row_edges
|
|
1839
|
+
on seed/adoption changes, so auto-state persists the ``{"y": …}`` edge
|
|
1840
|
+
dicts exactly like draw_rows. Edges register on the root window's
|
|
1841
|
+
``_row_views`` and solve in the y-axis flat pass; the window's top and
|
|
1842
|
+
bottom frame edges are the far edges of a window-direct rows view, so a
|
|
1843
|
+
top/bottom frame drag squeezes the first/last row and an interior edge
|
|
1844
|
+
pushed into the frame slides or grows the window — the same physics
|
|
1845
|
+
columns run along x. The orthogonal extent is simpler than a column's:
|
|
1846
|
+
a row's WIDTH is pinned by the host's visible box, so no measure step
|
|
1847
|
+
is needed to size the cells."""
|
|
1848
|
+
|
|
1849
|
+
def __init__(self, draw_state, n_rows, row_edges=None, row_heights=None,
|
|
1850
|
+
top_edge=None, bottom_edge=None, resizable=True,
|
|
1851
|
+
padding=6.0, border_color=(0.0, 0.0, 0.0, 0.9),
|
|
1852
|
+
padding_x=None, row_mins=None, row_maxes=None,
|
|
1853
|
+
key=None, band=None, persist=True):
|
|
1854
|
+
self.draw_state = draw_state
|
|
1855
|
+
self.n_rows = n_rows
|
|
1856
|
+
n_lines = self.n_lines = n_rows + 1
|
|
1857
|
+
# ``key`` / ``band`` / ``persist`` - as on ColumnLayout; ``band``
|
|
1858
|
+
# here is the layout's horizontal extent, a pair of COLUMN edge
|
|
1859
|
+
# dicts ``(left_edge, right_edge)``.
|
|
1860
|
+
self.key = key
|
|
1861
|
+
self.band = band
|
|
1862
|
+
self.persist = persist
|
|
1863
|
+
# Same band + padding contract as ColumnLayout; ``padding_x``
|
|
1864
|
+
# overrides the HORIZONTAL inset alone. ``row_mins`` / ``row_maxes``
|
|
1865
|
+
# stamp "min" / "max" on each row's BOTTOM edge (see _edge_min /
|
|
1866
|
+
# _edge_max).
|
|
1867
|
+
self.padding = float(padding)
|
|
1868
|
+
self.padding_x = float(padding if padding_x is None else padding_x)
|
|
1869
|
+
self.border_color = border_color
|
|
1870
|
+
self._cell_radius = {}
|
|
1871
|
+
|
|
1872
|
+
window = layout_window(draw_state)
|
|
1873
|
+
_ensure_window_state(window)
|
|
1874
|
+
window_edge_pass(window) # idempotent; usually ran via the wrapper
|
|
1875
|
+
self.window = window
|
|
1876
|
+
self.win_x, self.win_y = window.abs_left, window.abs_top
|
|
1877
|
+
|
|
1878
|
+
# The host's VISIBLE box (or clip, screen coords): the rows's full
|
|
1879
|
+
# horizontal extent, and the authority of the adopted top/bottom.
|
|
1880
|
+
self.clip = Core.melty.get_clip_rect() or draw_state.abs_clip_rect
|
|
1881
|
+
origin = imgui.get_cursor_screen_pos()
|
|
1882
|
+
# Where the flow cursor stood: an adopted top edge (the window frame
|
|
1883
|
+
# top sits at the header's top) never starts the first row above
|
|
1884
|
+
# this.
|
|
1885
|
+
self.origin_y = origin[1]
|
|
1886
|
+
if self.clip is not None:
|
|
1887
|
+
self.left, self.right = self.clip[0], self.clip[2]
|
|
1888
|
+
else:
|
|
1889
|
+
self.left = origin[0]
|
|
1890
|
+
self.right = origin[0] + float(draw_state.content_width or 0)
|
|
1891
|
+
self._right = self.left
|
|
1892
|
+
|
|
1893
|
+
# Mark the host so descendants' adoption climbs can ignore it.
|
|
1894
|
+
draw_state._row_container = True
|
|
1895
|
+
|
|
1896
|
+
# Far edges belong to the CONTAINER: the enclosing cell when given;
|
|
1897
|
+
# else the WINDOW's top/bottom frame when no other rows
|
|
1898
|
+
# container sits above this view; else own edges (fallback).
|
|
1899
|
+
if top_edge is None and bottom_edge is None and not _has_rows_ancestor(draw_state):
|
|
1900
|
+
frame_rows = _frame(window, "y")
|
|
1901
|
+
if frame_rows:
|
|
1902
|
+
top_edge, bottom_edge = frame_rows
|
|
1903
|
+
|
|
1904
|
+
stored = row_edges if isinstance(row_edges, list) else []
|
|
1905
|
+
ok = (len(stored) == n_lines and
|
|
1906
|
+
all(isinstance(e, dict) and "y" in e for e in stored))
|
|
1907
|
+
seed_valid = True
|
|
1908
|
+
if ok:
|
|
1909
|
+
edges = list(stored)
|
|
1910
|
+
else:
|
|
1911
|
+
if top_edge is not None and bottom_edge is not None:
|
|
1912
|
+
# Share over the VISIBLE span: the adopted top may sit above
|
|
1913
|
+
# the flow origin (window frame top → header top), and the
|
|
1914
|
+
# shares only split the room the rows actually get.
|
|
1915
|
+
base = max(top_edge["y"], self.origin_y - self.win_y)
|
|
1916
|
+
extent = bottom_edge["y"] - base
|
|
1917
|
+
else:
|
|
1918
|
+
base = self.origin_y - self.win_y
|
|
1919
|
+
extent = float(draw_state.height or 0)
|
|
1920
|
+
min_total = sum(_row_floor(row_mins, i) for i in range(n_rows))
|
|
1921
|
+
seed_valid = extent > min_total
|
|
1922
|
+
extent = max(extent, min_total)
|
|
1923
|
+
edges = _seed_edges(row_heights, n_rows, extent, base=base,
|
|
1924
|
+
column_mins=row_mins, column_maxes=row_maxes,
|
|
1925
|
+
axis="y")
|
|
1926
|
+
owned = [True] * n_lines
|
|
1927
|
+
if top_edge is not None:
|
|
1928
|
+
edges[0] = top_edge
|
|
1929
|
+
owned[0] = False
|
|
1930
|
+
if bottom_edge is not None:
|
|
1931
|
+
edges[-1] = bottom_edge
|
|
1932
|
+
owned[-1] = False
|
|
1933
|
+
self.edges = edges
|
|
1934
|
+
self.owned = owned
|
|
1935
|
+
self.seed_valid = seed_valid
|
|
1936
|
+
for i in range(n_rows):
|
|
1937
|
+
floor = row_mins[i] if row_mins and i < len(row_mins) else None
|
|
1938
|
+
if floor:
|
|
1939
|
+
edges[i + 1]["min"] = float(floor)
|
|
1940
|
+
else:
|
|
1941
|
+
edges[i + 1].pop("min", None)
|
|
1942
|
+
cap = _column_cap(row_maxes, i, _row_floor(row_mins, i))
|
|
1943
|
+
if cap is not None:
|
|
1944
|
+
edges[i + 1]["max"] = cap
|
|
1945
|
+
else:
|
|
1946
|
+
edges[i + 1].pop("max", None)
|
|
1947
|
+
# The LEAD: an adopted top edge can sit well above where the first
|
|
1948
|
+
# row begins (window frame top → the header and anything drawn
|
|
1949
|
+
# before the rows). The flat solve measures the first row from the
|
|
1950
|
+
# edge, so its floor is raised by that lead - the VISIBLE first row
|
|
1951
|
+
# keeps its minimum when the window frame edge is dragged, and the
|
|
1952
|
+
# frame-fit clamp places it correctly.
|
|
1953
|
+
lead = self._band_top() - edges[0]["y"]
|
|
1954
|
+
if lead > 0.5:
|
|
1955
|
+
edges[1]["min"] = _row_floor(row_mins, 0) + lead
|
|
1956
|
+
_clamp_interior(edges, axis="y")
|
|
1957
|
+
if persist and seed_valid and (not ok or any(a is not b for a, b in zip(stored, edges))):
|
|
1958
|
+
draw_state.row_edges = edges
|
|
1959
|
+
|
|
1960
|
+
# Keyed ("rows", id) - see ColumnLayout's ("row", id) note: a host
|
|
1961
|
+
# ds that IS the window must not clobber the spec entry.
|
|
1962
|
+
registry_key = layout_key("rows", draw_state, key)
|
|
1963
|
+
_views(window, "y")[registry_key] = (draw_state, edges)
|
|
1964
|
+
if band is not None:
|
|
1965
|
+
_bands(window, "y")[registry_key] = band
|
|
1966
|
+
else:
|
|
1967
|
+
_bands(window, "y").pop(registry_key, None)
|
|
1968
|
+
floors = [_row_floor(row_mins, i) for i in range(n_rows)]
|
|
1969
|
+
if lead > 0.5:
|
|
1970
|
+
floors[0] += lead
|
|
1971
|
+
_specs(window, "y")[registry_key] = (
|
|
1972
|
+
floors,
|
|
1973
|
+
[_column_cap(row_maxes, i, _row_floor(row_mins, i))
|
|
1974
|
+
for i in range(n_rows)])
|
|
1975
|
+
|
|
1976
|
+
self.active_edge = None
|
|
1977
|
+
self.edge_hovered = False
|
|
1978
|
+
if resizable:
|
|
1979
|
+
if band is not None:
|
|
1980
|
+
grab_left = self.win_x + band[0]["x"]
|
|
1981
|
+
grab_right = self.win_x + band[1]["x"]
|
|
1982
|
+
else:
|
|
1983
|
+
grab_left, grab_right = self.left, self.right
|
|
1984
|
+
handle_tag = "" if key is None else f"_{key}"
|
|
1985
|
+
seen_handles = set()
|
|
1986
|
+
for k in range(n_lines):
|
|
1987
|
+
if not owned[k]:
|
|
1988
|
+
continue
|
|
1989
|
+
lo, hi = _grab_zone(edges, k, axis="y")
|
|
1990
|
+
rect = (grab_left, self.win_y + lo, grab_right, self.win_y + hi)
|
|
1991
|
+
if draw_state.hover_eligible(rect=rect):
|
|
1992
|
+
self.edge_hovered = True
|
|
1993
|
+
drag = draw_state.on_action("left_mouse_drag",
|
|
1994
|
+
view_id=f"row_edge{handle_tag}_{k}",
|
|
1995
|
+
rect=rect, priority_delta=1,
|
|
1996
|
+
cursor=mouse_cursor.RESIZE_NS)
|
|
1997
|
+
press = draw_state.on_action("left_mouse_down",
|
|
1998
|
+
view_id=f"row_edge{handle_tag}_{k}",
|
|
1999
|
+
rect=rect, priority_delta=1)
|
|
2000
|
+
if press:
|
|
2001
|
+
from meltygui.core.melty import Melty
|
|
2002
|
+
Melty.resize_press_frame = Melty.frame_count
|
|
2003
|
+
if not drag:
|
|
2004
|
+
continue
|
|
2005
|
+
self.active_edge = k
|
|
2006
|
+
handle = f"row_{k}" if key is None else ("rows", key, k)
|
|
2007
|
+
seen_handles.add(handle)
|
|
2008
|
+
inc = _drag_inc(draw_state, handle, drag, total="total_dy")
|
|
2009
|
+
if inc:
|
|
2010
|
+
_pending(window, "y").append(
|
|
2011
|
+
(edges[k], edges[k]["y"] + inc, True)) # cursor-driven, as for columns
|
|
2012
|
+
|
|
2013
|
+
totals = getattr(draw_state, "_drag_totals", None)
|
|
2014
|
+
if totals:
|
|
2015
|
+
# Own namespace only ("row_*", or this key's "rows"
|
|
2016
|
+
# prefix): a ColumnLayout on the same host keeps its int
|
|
2017
|
+
# keys, the window its "win_*".
|
|
2018
|
+
if key is None:
|
|
2019
|
+
stale = [h for h in totals
|
|
2020
|
+
if isinstance(h, str) and h.startswith("row_")
|
|
2021
|
+
and h not in seen_handles]
|
|
2022
|
+
else:
|
|
2023
|
+
stale = [h for h in totals
|
|
2024
|
+
if isinstance(h, tuple) and h[:2] == ("rows", key)
|
|
2025
|
+
and h not in seen_handles]
|
|
2026
|
+
for h in stale:
|
|
2027
|
+
del totals[h]
|
|
2028
|
+
if self.active_edge is not None:
|
|
2029
|
+
if not draw_state.size_change:
|
|
2030
|
+
if getattr(draw_state, "freeze_resize", False):
|
|
2031
|
+
_defer_freeze_settle(window, draw_state)
|
|
2032
|
+
else:
|
|
2033
|
+
draw_state.invalidate(note=Note(reason="edge drag",
|
|
2034
|
+
**_NOTE))
|
|
2035
|
+
|
|
2036
|
+
# ----- the column band: a filled rounded rectangle, the cells'
|
|
2037
|
+
# rounded backgrounds its holes (see ColumnLayout) -----
|
|
2038
|
+
if self.padding > 0 and self.border_color is not None:
|
|
2039
|
+
radius = getattr(draw_state, "_band_radius", 6.0) + self.padding
|
|
2040
|
+
band_color = (HIGHLIGHT_TINT
|
|
2041
|
+
if self.edge_hovered or self.active_edge is not None
|
|
2042
|
+
else self.border_color)
|
|
2043
|
+
draw_list = imgui.get_window_draw_list()
|
|
2044
|
+
draw_list.channels_set_current(Core.melty.get_channel() - 1)
|
|
2045
|
+
draw_list.add_rect_filled(
|
|
2046
|
+
snap_int(self.left),
|
|
2047
|
+
snap_int(self.win_y + self._band_top()),
|
|
2048
|
+
snap_int(self.right),
|
|
2049
|
+
snap_int(self.win_y + self._band_bottom()),
|
|
2050
|
+
pack_color(*band_color),
|
|
2051
|
+
rounding=radius)
|
|
2052
|
+
draw_list.channels_set_current(Core.melty.get_channel())
|
|
2053
|
+
|
|
2054
|
+
def height(self, idx):
|
|
2055
|
+
return self.edges[idx + 1]["y"] - self.edges[idx]["y"]
|
|
2056
|
+
|
|
2057
|
+
def _band_top(self):
|
|
2058
|
+
"""Band top bound (window coords). An owned edge is the row's own
|
|
2059
|
+
line; an ADOPTED one is the container's boundary, so the band
|
|
2060
|
+
starts at the flow origin — never above the container's visible
|
|
2061
|
+
box."""
|
|
2062
|
+
if self.owned[0]:
|
|
2063
|
+
return self.edges[0]["y"]
|
|
2064
|
+
top = self.origin_y - self.win_y
|
|
2065
|
+
if self.clip is not None:
|
|
2066
|
+
top = max(top, self.clip[1] - self.win_y)
|
|
2067
|
+
return top
|
|
2068
|
+
|
|
2069
|
+
def _band_bottom(self):
|
|
2070
|
+
"""Band bottom bound (window coords): the adopted edge's line, but
|
|
2071
|
+
never past the container's visible box (a window-direct view ends
|
|
2072
|
+
at the window's content clip, a cell-nested one at its cell)."""
|
|
2073
|
+
if self.owned[-1]:
|
|
2074
|
+
return self.edges[-1]["y"]
|
|
2075
|
+
bottom = self.edges[-1]["y"]
|
|
2076
|
+
if self.clip is not None:
|
|
2077
|
+
bottom = min(bottom, self.clip[3] - self.win_y)
|
|
2078
|
+
else:
|
|
2079
|
+
bottom -= self.padding
|
|
2080
|
+
return bottom
|
|
2081
|
+
|
|
2082
|
+
def _bound_top(self, idx):
|
|
2083
|
+
return self._band_top() if idx == 0 else self.edges[idx]["y"]
|
|
2084
|
+
|
|
2085
|
+
def _bound_bottom(self, idx):
|
|
2086
|
+
return (self._band_bottom() if idx == self.n_rows - 1
|
|
2087
|
+
else self.edges[idx + 1]["y"])
|
|
2088
|
+
|
|
2089
|
+
def inner_height(self, idx):
|
|
2090
|
+
"""Content height of row idx: the effective span minus padding."""
|
|
2091
|
+
return max(0.0, self._bound_bottom(idx) - self._bound_top(idx)
|
|
2092
|
+
- 2 * self.padding)
|
|
2093
|
+
|
|
2094
|
+
def inner_width(self):
|
|
2095
|
+
"""Content width every row gets: the visible box minus the
|
|
2096
|
+
horizontal padding."""
|
|
2097
|
+
return max(0.0, self.right - self.left - 2 * self.padding_x)
|
|
2098
|
+
|
|
2099
|
+
def cell_rect(self, idx, width=None):
|
|
2100
|
+
"""The screen-space (x, y, w, h) content box `cell(idx, width)`
|
|
2101
|
+
clips to. ``width`` is the FULL cell band including padding; by
|
|
2102
|
+
default the box runs to the visible box's right."""
|
|
2103
|
+
pad = self.padding
|
|
2104
|
+
pad_x = self.padding_x
|
|
2105
|
+
x0 = snap_int(self.left + pad_x)
|
|
2106
|
+
y0 = snap_int(self.win_y + self._bound_top(idx) + pad)
|
|
2107
|
+
if width is not None:
|
|
2108
|
+
x1 = snap_int(self.left) + snap_int(width) - snap_int(pad_x)
|
|
2109
|
+
else:
|
|
2110
|
+
x1 = snap_int(self.right) - snap_int(pad_x)
|
|
2111
|
+
y1 = snap_int(self.win_y + self._bound_bottom(idx)) - snap_int(pad)
|
|
2112
|
+
return x0, y0, x1 - x0, y1 - y0
|
|
2113
|
+
|
|
2114
|
+
@contextmanager
|
|
2115
|
+
def cell(self, idx, width=None):
|
|
2116
|
+
"""Position the cursor at row idx's content origin, clip to the
|
|
2117
|
+
content box, and yield the content HEIGHT (pass it as ``height=``
|
|
2118
|
+
to the child, with ``width=inner_width()`` — a passed height pins
|
|
2119
|
+
the child fixed-size, so its width must be pinned too)."""
|
|
2120
|
+
x0, y0, box_w, box_h = self.cell_rect(idx, width)
|
|
2121
|
+
imgui.set_cursor_screen_pos((x0, y0))
|
|
2122
|
+
Core.melty.push_clip((x0, y0, x0 + box_w, y0 + box_h))
|
|
2123
|
+
imgui.begin_group()
|
|
2124
|
+
try:
|
|
2125
|
+
yield self.inner_height(idx)
|
|
2126
|
+
finally:
|
|
2127
|
+
imgui.end_group()
|
|
2128
|
+
Core.melty.pop_clip()
|
|
2129
|
+
right = imgui.get_item_rect_max()[0]
|
|
2130
|
+
self._right = max(self._right, right + self.padding_x)
|
|
2131
|
+
|
|
2132
|
+
def note_child(self, idx, child_ds):
|
|
2133
|
+
if child_ds is not None:
|
|
2134
|
+
radius = getattr(child_ds, "corner_radius", None)
|
|
2135
|
+
if radius is not None:
|
|
2136
|
+
self._cell_radius[idx] = float(radius)
|
|
2137
|
+
|
|
2138
|
+
def finish(self):
|
|
2139
|
+
"""Stamp the measured band width (edge_under_cursor's fallback
|
|
2140
|
+
band) and the band rounding, and leave the flow cursor below the
|
|
2141
|
+
last row."""
|
|
2142
|
+
ds = self.draw_state
|
|
2143
|
+
prev_w = getattr(ds, "_edge_lines_width", None)
|
|
2144
|
+
ds._edge_lines_width = max(self._right - self.left, MIN_COLUMN_WIDTH)
|
|
2145
|
+
ds._band_radius = (max(self._cell_radius.values())
|
|
2146
|
+
if self._cell_radius else 6.0)
|
|
2147
|
+
if prev_w is None:
|
|
2148
|
+
if not ds.size_change:
|
|
2149
|
+
ds.invalidate(note=Note(reason="row band settle", **_NOTE))
|
|
2150
|
+
request_render()
|
|
2151
|
+
imgui.set_cursor_screen_pos((self.left,
|
|
2152
|
+
self.win_y + self._band_bottom()))
|
|
2153
|
+
imgui.dummy(0, 0)
|