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,591 @@
|
|
|
1
|
+
"""Tiled window manager: a Blender-style tree of splits drawn flat.
|
|
2
|
+
|
|
3
|
+
The layout is a TREE of ``Split`` nodes (an axis + children) ending in
|
|
4
|
+
``Tile`` leaves. Plain functions lay out the split tree on one host draw_state;
|
|
5
|
+
each leaf's editor is a normal render_func with independent injected state.
|
|
6
|
+
One ``Split`` is ONE ``ColumnLayout`` (axis "x") or
|
|
7
|
+
``RowLayout`` (axis "y") call over its children; a child of the OPPOSITE
|
|
8
|
+
axis is the only thing that recurses, and a Split never holds a Split of
|
|
9
|
+
its own axis (``normalize`` keeps that invariant, mirroring Blender's
|
|
10
|
+
areas: same-axis neighbours are siblings in one flat list).
|
|
11
|
+
|
|
12
|
+
Every node renders into a FRAME of four shared edge dicts
|
|
13
|
+
``(left, right, top, bottom)`` — the enclosing cell's pair on the split's
|
|
14
|
+
axis and the pass-through pair on the other — so a Rows in a Columns cell
|
|
15
|
+
collides with the outer rows exactly like draw_columns / draw_rows do.
|
|
16
|
+
The interior edges of a Split live IN THE TREE (``node.edges``, the
|
|
17
|
+
n-1 dicts between its children, window coords); its far edges are never
|
|
18
|
+
stored, they are adopted by reference from the frame every frame. The
|
|
19
|
+
layouts register on the host window with ``key=path`` and ``band=`` (see
|
|
20
|
+
columns.py), so one draw_state hosts the whole tree and every divider,
|
|
21
|
+
including the window frame, drags through the same collision solve.
|
|
22
|
+
|
|
23
|
+
Data ops (``split_tile`` / ``join_tiles`` / ``normalize``) are pure list
|
|
24
|
+
edits on the tree; ``resolve_frames`` walks the stored edges from a root
|
|
25
|
+
frame without rendering, so they can place a new edge at a midpoint.
|
|
26
|
+
|
|
27
|
+
Gestures (Blender's): a left-drag from a tile's CORNER inward splits it —
|
|
28
|
+
a mostly-horizontal drag cuts a vertical divider (axis "x"), a vertical
|
|
29
|
+
one a horizontal divider (axis "y"); the new tile sits on the corner's
|
|
30
|
+
side and the new edge follows the cursor for the rest of the drag through
|
|
31
|
+
the same collision solve as any divider (``tile_corner_gesture``). The
|
|
32
|
+
live gesture lives in the injected ``TileManagerState``. An outward drag
|
|
33
|
+
previews joining a leaf sibling across its whole edge; release removes
|
|
34
|
+
that neighbour and expands the dragged-from tile. Returning inside the
|
|
35
|
+
source or leaving the neighbour cancels the join.
|
|
36
|
+
"""
|
|
37
|
+
import meltygui_imgui as imgui
|
|
38
|
+
|
|
39
|
+
from meltygui.model.tile_model import Tile
|
|
40
|
+
from meltygui.model.tile_model import Split
|
|
41
|
+
|
|
42
|
+
import meltygui.core.input.mouse_cursor as mouse_cursor
|
|
43
|
+
from meltygui.hdr_color import pack_color
|
|
44
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
45
|
+
from meltygui.core.cache.tile_cache import snap_int
|
|
46
|
+
from meltygui.core.layout.column_core import ColumnLayout
|
|
47
|
+
from meltygui.core.layout.column_core import RowLayout
|
|
48
|
+
from meltygui.core.layout.column_core import MIN_COLUMN_WIDTH
|
|
49
|
+
from meltygui.core.layout.column_core import MIN_ROW_HEIGHT
|
|
50
|
+
from meltygui.core.layout.column_core import _ensure_window_state
|
|
51
|
+
from meltygui.core.layout.column_core import _pending
|
|
52
|
+
from meltygui.core.layout.column_core import _views
|
|
53
|
+
from meltygui.core.layout.column_core import _specs
|
|
54
|
+
from meltygui.core.layout.column_core import _bands
|
|
55
|
+
from meltygui.core.layout.column_core import frame_edges
|
|
56
|
+
from meltygui.core.layout.column_core import layout_window
|
|
57
|
+
from meltygui.core.layout.column_core import _drag_inc
|
|
58
|
+
from meltygui.core.layout.column_core import _grab_zone
|
|
59
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
60
|
+
from meltygui.core.rendering.core_decoration import no_save
|
|
61
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
62
|
+
|
|
63
|
+
_NOTE = dict(name="draw_tiles", tint=(0.55, 0.85, 0.45))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@no_save("gesture")
|
|
67
|
+
class TileManagerState(DictConversion):
|
|
68
|
+
"""Injected state of a tile-manager host (declare
|
|
69
|
+
``tile_state: TileManagerState = None`` on the render_func). ``gesture``
|
|
70
|
+
holds the captured corner and either the new split edge or a join
|
|
71
|
+
preview target, until the button is released. It is never persisted."""
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.gesture = None
|
|
76
|
+
self.content_top = {"y": 0.0}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# The four corners of a tile: (name, on the left?, on the top?).
|
|
80
|
+
CORNERS = (("nw", True, True), ("ne", False, True),
|
|
81
|
+
("sw", True, False), ("se", False, False))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def other_axis(axis):
|
|
85
|
+
return "y" if axis == "x" else "x"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def frame_pair(frame, axis):
|
|
89
|
+
"""The frame's two edges on ``axis``: (left, right) or (top, bottom)."""
|
|
90
|
+
left, right, top, bottom = frame
|
|
91
|
+
return (left, right) if axis == "x" else (top, bottom)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def child_frame(frame, axis, near, far):
|
|
95
|
+
"""The frame a child gets: the parent's frame with the ``axis`` pair
|
|
96
|
+
replaced by the child's cell edges, the other pair passed through."""
|
|
97
|
+
left, right, top, bottom = frame
|
|
98
|
+
if axis == "x":
|
|
99
|
+
return near, far, top, bottom
|
|
100
|
+
return left, right, near, far
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def full_edges(node, frame):
|
|
104
|
+
"""A Split's complete edge list — far edges from the frame, interior
|
|
105
|
+
edges from the tree — the list its layout is built over."""
|
|
106
|
+
near, far = frame_pair(frame, node.axis)
|
|
107
|
+
return [near, *node.edges, far]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def node_at(tree, path):
|
|
111
|
+
"""The node at ``path`` (a tuple of child indices from the root)."""
|
|
112
|
+
node = tree
|
|
113
|
+
for index in path:
|
|
114
|
+
node = node.children[index]
|
|
115
|
+
return node
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def walk(tree, path=()):
|
|
119
|
+
"""Every ``(path, node)`` of the tree, parents before children, seeded
|
|
120
|
+
or not."""
|
|
121
|
+
yield path, tree
|
|
122
|
+
if isinstance(tree, Split):
|
|
123
|
+
for index, child in enumerate(tree.children):
|
|
124
|
+
yield from walk(child, path + (index,))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_frames(tree, root_frame, path=()):
|
|
128
|
+
"""Walk the tree WITHOUT rendering, yielding ``(path, node, frame)``
|
|
129
|
+
for every node from the stored edges alone. A Split whose interior
|
|
130
|
+
edges have not been seeded yet (fewer than n-1) is yielded, but its
|
|
131
|
+
children are skipped — their frames don't exist until a render seeds
|
|
132
|
+
them."""
|
|
133
|
+
yield path, tree, root_frame
|
|
134
|
+
if not isinstance(tree, Split):
|
|
135
|
+
return
|
|
136
|
+
children = tree.children
|
|
137
|
+
if len(tree.edges) != max(len(children) - 1, 0):
|
|
138
|
+
return
|
|
139
|
+
edges = full_edges(tree, root_frame)
|
|
140
|
+
for index, child in enumerate(children):
|
|
141
|
+
frame = child_frame(root_frame, tree.axis, edges[index], edges[index + 1])
|
|
142
|
+
yield from resolve_frames(child, frame, path + (index,))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def frame_rect(frame, window):
|
|
146
|
+
"""A frame as an absolute screen rect ``(x0, y0, x1, y1)``."""
|
|
147
|
+
left, right, top, bottom = frame
|
|
148
|
+
return (window.abs_left + left["x"], window.abs_top + top["y"],
|
|
149
|
+
window.abs_left + right["x"], window.abs_top + bottom["y"])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def tile_rect(frame, draw_state, gap=4.0):
|
|
153
|
+
"""A tile's painted box (absolute, ints): its frame inset by ``gap``
|
|
154
|
+
/ 2 and clipped to the host's visible box, or None when nothing of it
|
|
155
|
+
shows."""
|
|
156
|
+
window = layout_window(draw_state)
|
|
157
|
+
x0, y0, x1, y1 = frame_rect(frame, window)
|
|
158
|
+
clip = Core.melty.get_clip_rect() or draw_state.abs_clip_rect
|
|
159
|
+
if getattr(draw_state, "closable", False):
|
|
160
|
+
content = draw_state.get_content_rect()
|
|
161
|
+
clip = content if clip is None else (
|
|
162
|
+
max(clip[0], content[0]), max(clip[1], content[1]),
|
|
163
|
+
min(clip[2], content[2]), min(clip[3], content[3]))
|
|
164
|
+
half = gap / 2
|
|
165
|
+
x0, y0 = snap_int(x0 + half), snap_int(y0 + half)
|
|
166
|
+
x1, y1 = snap_int(x1 - half), snap_int(y1 - half)
|
|
167
|
+
if clip is not None:
|
|
168
|
+
x0, y0 = max(x0, clip[0]), max(y0, clip[1])
|
|
169
|
+
x1, y1 = min(x1, clip[2]), min(y1, clip[3])
|
|
170
|
+
if x1 <= x0 or y1 <= y0:
|
|
171
|
+
return None
|
|
172
|
+
return x0, y0, x1, y1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def corner_rect(rect, on_left, on_top, size):
|
|
176
|
+
"""The ``size`` px square in one corner of ``rect``."""
|
|
177
|
+
x0, y0, x1, y1 = rect
|
|
178
|
+
cx0 = x0 if on_left else x1 - size
|
|
179
|
+
cy0 = y0 if on_top else y1 - size
|
|
180
|
+
return cx0, cy0, cx0 + size, cy0 + size
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def draw_tile(tile, frame, draw_state, path=(), tree=None, root_frame=None,
|
|
184
|
+
tile_state=None, gap=4.0):
|
|
185
|
+
"""Paint one leaf — no background (``draw_split_dividers`` draws the
|
|
186
|
+
lines between tiles), only a corner grip where hovered — and run the
|
|
187
|
+
corner split gesture when ``tree`` /
|
|
188
|
+
``root_frame`` / ``tile_state`` are given (``draw_tiles`` passes
|
|
189
|
+
them). Returns True when the gesture changed the tree."""
|
|
190
|
+
# [tint=(1.0, 0.8, 0.3)]
|
|
191
|
+
corner_size = 14.0
|
|
192
|
+
corner_hover_color = (1.0, 1.0, 1.0, 0.35)
|
|
193
|
+
|
|
194
|
+
rect = tile_rect(frame, draw_state, gap=gap)
|
|
195
|
+
if rect is None:
|
|
196
|
+
return False
|
|
197
|
+
draw_list = imgui.get_window_draw_list()
|
|
198
|
+
|
|
199
|
+
changed = False
|
|
200
|
+
for name, on_left, on_top in CORNERS:
|
|
201
|
+
grip = corner_rect(rect, on_left, on_top, corner_size)
|
|
202
|
+
if draw_state.hover_eligible(rect=grip):
|
|
203
|
+
draw_list.add_rect_filled(*grip, pack_color(*corner_hover_color))
|
|
204
|
+
if tree is not None and root_frame is not None and tile_state is not None:
|
|
205
|
+
changed = tile_corner_gesture(
|
|
206
|
+
tile, frame, draw_state, path, tree, root_frame, tile_state,
|
|
207
|
+
name, on_left, on_top, grip) or changed
|
|
208
|
+
return changed
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def tile_corner_gesture(tile, frame, draw_state, path, tree, root_frame,
|
|
212
|
+
tile_state, corner, on_left, on_top, grip):
|
|
213
|
+
"""One corner's split / join gesture. The grip is an ``on_action`` sub-rect
|
|
214
|
+
above the edge grab zones (the two overlap at a tile's corners).
|
|
215
|
+
A drag that travels ``split_threshold`` px INTO the tile splits it:
|
|
216
|
+
the dominant direction picks the axis, the new tile goes on the
|
|
217
|
+
corner's side, the new edge lands under the cursor. From then until
|
|
218
|
+
release every frame queues a cursor-driven drag of that edge on the
|
|
219
|
+
window's solve, so it follows the pointer and pushes neighbours like
|
|
220
|
+
any divider. The view id keys on the tile OBJECT, not its path — the
|
|
221
|
+
split shifts the path while the drag is still captured. Returns True
|
|
222
|
+
on the frame the tree changed."""
|
|
223
|
+
# [tint=(1.0, 0.8, 0.3)]
|
|
224
|
+
split_threshold = 8.0
|
|
225
|
+
|
|
226
|
+
view_id = f"tile_corner_{id(tile)}_{corner}"
|
|
227
|
+
draw_state.on_action("left_mouse_down", view_id=view_id, rect=grip,
|
|
228
|
+
priority_delta=2, cursor=mouse_cursor.RESIZE_ALL)
|
|
229
|
+
drag = draw_state.on_action("left_mouse_drag", view_id=view_id, rect=grip,
|
|
230
|
+
priority_delta=2)
|
|
231
|
+
gesture = tile_state.gesture
|
|
232
|
+
window = layout_window(draw_state)
|
|
233
|
+
|
|
234
|
+
if gesture is not None and gesture["corner"] == view_id:
|
|
235
|
+
if gesture.get("kind") == "join":
|
|
236
|
+
if drag is None:
|
|
237
|
+
tile_state.gesture = None
|
|
238
|
+
target = gesture["target"]
|
|
239
|
+
target_path = locate(tree, target) if target is not None else None
|
|
240
|
+
source_path = locate(tree, tile)
|
|
241
|
+
if target_path is not None and source_path is not None:
|
|
242
|
+
direction = -1 if target_path[-1] > source_path[-1] else +1
|
|
243
|
+
join_tiles(tree, target_path, direction)
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
tile_state.gesture = {**gesture, "target": join_target(tree, path, root_frame, window, drag)}
|
|
247
|
+
return False
|
|
248
|
+
if drag is None:
|
|
249
|
+
tile_state.gesture = None
|
|
250
|
+
draw_state._drag_totals.pop(view_id, None)
|
|
251
|
+
return False
|
|
252
|
+
# Use the same relative increments as every divider. An absolute cursor
|
|
253
|
+
# target re-applied at a wall accumulates the blocked distance on every
|
|
254
|
+
# frame of sticky replay, even when the hand is stationary.
|
|
255
|
+
axis = gesture["axis"]
|
|
256
|
+
increment = _drag_inc(draw_state, view_id, drag,
|
|
257
|
+
total="total_dx" if axis == "x" else "total_dy")
|
|
258
|
+
if increment:
|
|
259
|
+
_ensure_window_state(window)
|
|
260
|
+
edge = gesture["edge"]
|
|
261
|
+
_pending(window, axis).append((edge, edge[axis] + increment, True))
|
|
262
|
+
draw_state.invalidate(note=Note(reason="tile split drag", **_NOTE))
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
if drag is None or gesture is not None:
|
|
266
|
+
return False
|
|
267
|
+
dx, dy = drag.total_dx, drag.total_dy
|
|
268
|
+
if max(abs(dx), abs(dy)) < split_threshold:
|
|
269
|
+
return False
|
|
270
|
+
axis = "x" if abs(dx) >= abs(dy) else "y"
|
|
271
|
+
inward = (dx > 0) == on_left if axis == "x" else (dy > 0) == on_top
|
|
272
|
+
if not inward:
|
|
273
|
+
target = join_target(tree, path, root_frame, window, drag)
|
|
274
|
+
if target is not None:
|
|
275
|
+
tile_state.gesture = {"kind": "join", "corner": view_id, "target": target}
|
|
276
|
+
return False
|
|
277
|
+
before = on_left if axis == "x" else on_top
|
|
278
|
+
near, far = frame_pair(frame, axis)
|
|
279
|
+
floor = MIN_COLUMN_WIDTH if axis == "x" else MIN_ROW_HEIGHT
|
|
280
|
+
along = (drag.x - window.abs_left) if axis == "x" else (drag.y - window.abs_top)
|
|
281
|
+
lo, hi = near[axis] + floor, far[axis] - floor
|
|
282
|
+
if hi < lo:
|
|
283
|
+
return False # too small to split
|
|
284
|
+
at = min(max(float(along), lo), hi)
|
|
285
|
+
new_path = split_tile(tree, path, axis, root_frame, at=at, before=before)
|
|
286
|
+
new_node = node_at(tree, new_path[:-1])
|
|
287
|
+
edge_index = new_path[-1] if before else new_path[-1] - 1
|
|
288
|
+
tile_state.gesture = {"corner": view_id, "axis": axis,
|
|
289
|
+
"edge": new_node.edges[edge_index],
|
|
290
|
+
"new_tile": node_at(tree, new_path)}
|
|
291
|
+
_drag_inc(draw_state, view_id, drag,
|
|
292
|
+
total="total_dx" if axis == "x" else "total_dy")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def join_target(tree, path, root_frame, window, drag):
|
|
297
|
+
"""Leaf sibling under the pointer across a whole shared edge.
|
|
298
|
+
|
|
299
|
+
Join a subdivided neighbour's own tiles first. Moving back into the
|
|
300
|
+
source or leaving the neighbour cancels the preview.
|
|
301
|
+
"""
|
|
302
|
+
if not path:
|
|
303
|
+
return None
|
|
304
|
+
parent = node_at(tree, path[:-1])
|
|
305
|
+
frames = {path: frame for path, _node, frame in resolve_frames(tree, root_frame)}
|
|
306
|
+
for direction in (-1, +1):
|
|
307
|
+
neighbour = path[-1] + direction
|
|
308
|
+
if not 0 <= neighbour < len(parent.children):
|
|
309
|
+
continue
|
|
310
|
+
candidate = parent.children[neighbour]
|
|
311
|
+
if isinstance(candidate, Split):
|
|
312
|
+
continue
|
|
313
|
+
frame = frames.get(path[:-1] + (neighbour,))
|
|
314
|
+
if frame is None:
|
|
315
|
+
continue
|
|
316
|
+
left, top, right, bottom = frame_rect(frame, window)
|
|
317
|
+
if left < drag.x < right and top < drag.y < bottom:
|
|
318
|
+
return candidate
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def draw_join_preview(tree, root_frame, draw_state, tile_state):
|
|
323
|
+
gesture = tile_state.gesture
|
|
324
|
+
if gesture is None or gesture.get("kind") != "join" or gesture["target"] is None:
|
|
325
|
+
return
|
|
326
|
+
# [tint=(1.0, 0.55, 0.15)]
|
|
327
|
+
preview_color = (1.0, 0.55, 0.15, 0.42)
|
|
328
|
+
for _path, tile, frame in resolve_frames(tree, root_frame):
|
|
329
|
+
if tile is gesture["target"]:
|
|
330
|
+
rect = tile_rect(frame, draw_state)
|
|
331
|
+
if rect is not None:
|
|
332
|
+
draw_list = imgui.get_window_draw_list()
|
|
333
|
+
draw_list.add_rect_filled(*rect, pack_color(*preview_color))
|
|
334
|
+
draw_list.add_text(rect[0] + 8, rect[1] + 28,
|
|
335
|
+
pack_color(1.0, 1.0, 1.0, 1.0), "Release to merge")
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def draw_split_dividers(layout, axis, frame, draw_state):
|
|
340
|
+
"""Draw one Split's interior edges as lines across its band: black at
|
|
341
|
+
rest, highlighted while the cursor is in that edge's grab zone (the
|
|
342
|
+
same zone the layout drags from) or the edge is being dragged."""
|
|
343
|
+
# [tint=(1.0, 0.8, 0.3)]
|
|
344
|
+
# Change the divider look here: resting / hovered colour and line width.
|
|
345
|
+
divider_color = (0.0, 0.0, 0.0, 1.0)
|
|
346
|
+
divider_hover_color = (0.35, 0.65, 1.0, 1.0)
|
|
347
|
+
divider_thickness = 2.0
|
|
348
|
+
|
|
349
|
+
window = layout_window(draw_state)
|
|
350
|
+
x0, y0, x1, y1 = frame_rect(frame, window)
|
|
351
|
+
origin = window.abs_left if axis == "x" else window.abs_top
|
|
352
|
+
edges = layout.edges
|
|
353
|
+
draw_list = imgui.get_window_draw_list()
|
|
354
|
+
for k in range(1, len(edges) - 1):
|
|
355
|
+
lo, hi = _grab_zone(edges, k, axis=axis)
|
|
356
|
+
line = snap_int(origin + edges[k][axis])
|
|
357
|
+
if axis == "x":
|
|
358
|
+
grab = (origin + lo, y0, origin + hi, y1)
|
|
359
|
+
ends = (line, y0, line, y1)
|
|
360
|
+
else:
|
|
361
|
+
grab = (x0, origin + lo, x1, origin + hi)
|
|
362
|
+
ends = (x0, line, x1, line)
|
|
363
|
+
hot = layout.active_edge == k or draw_state.hover_eligible(rect=grab)
|
|
364
|
+
draw_list.add_line(*ends,
|
|
365
|
+
pack_color(*(divider_hover_color if hot else divider_color)),
|
|
366
|
+
divider_thickness)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def draw_tile_node(node, frame, draw_state, path=(), tree=None,
|
|
370
|
+
root_frame=None, tile_state=None, gap=4.0):
|
|
371
|
+
"""Render one node into ``frame``. A Tile paints (and runs its corner
|
|
372
|
+
gestures when ``tree`` / ``root_frame`` / ``tile_state`` are given); a
|
|
373
|
+
Split builds ONE layout over its children (columns for axis "x", rows
|
|
374
|
+
for "y") keyed by ``path`` so many layouts share the host draw_state,
|
|
375
|
+
writes the seeded / clamped interior edges back into the tree, and
|
|
376
|
+
recurses into each child with that child's cell edges as its frame.
|
|
377
|
+
Returns True when a gesture changed the tree — the children are
|
|
378
|
+
snapshotted first, so a split mid-walk renders on the next frame."""
|
|
379
|
+
if not isinstance(node, Split):
|
|
380
|
+
return draw_tile(node, frame, draw_state, path=path, tree=tree,
|
|
381
|
+
root_frame=root_frame, tile_state=tile_state, gap=gap)
|
|
382
|
+
children = node.children
|
|
383
|
+
if not children:
|
|
384
|
+
return False
|
|
385
|
+
axis = node.axis
|
|
386
|
+
near, far = frame_pair(frame, axis)
|
|
387
|
+
across = frame_pair(frame, other_axis(axis))
|
|
388
|
+
stored = full_edges(node, frame)
|
|
389
|
+
if axis == "x":
|
|
390
|
+
layout = ColumnLayout(draw_state, len(children), column_edges=stored,
|
|
391
|
+
left_edge=near, right_edge=far, band=across,
|
|
392
|
+
key=path, persist=False, padding=0.0,
|
|
393
|
+
border_color=None)
|
|
394
|
+
else:
|
|
395
|
+
layout = RowLayout(draw_state, len(children), row_edges=stored,
|
|
396
|
+
top_edge=near, bottom_edge=far, band=across,
|
|
397
|
+
key=path, persist=False, padding=0.0,
|
|
398
|
+
border_color=None)
|
|
399
|
+
if layout.seed_valid:
|
|
400
|
+
interior = layout.edges[1:-1]
|
|
401
|
+
if any(a is not b for a, b in zip(node.edges, interior)) \
|
|
402
|
+
or len(node.edges) != len(interior):
|
|
403
|
+
node.edges = interior
|
|
404
|
+
edges = layout.edges
|
|
405
|
+
draw_split_dividers(layout, axis, frame, draw_state)
|
|
406
|
+
changed = False
|
|
407
|
+
for index, child in enumerate(list(children)):
|
|
408
|
+
if draw_tile_node(child, child_frame(frame, axis, edges[index], edges[index + 1]),
|
|
409
|
+
draw_state, path + (index,), tree=tree,
|
|
410
|
+
root_frame=root_frame, tile_state=tile_state, gap=gap):
|
|
411
|
+
changed = True
|
|
412
|
+
break # the tree moved under us
|
|
413
|
+
return changed
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def draw_tiles(tree, draw_state, tile_state=None, gap=4.0,
|
|
417
|
+
multi_instance_renderers=(), content_top=None):
|
|
418
|
+
"""Render a whole tile tree over the host window's frame: the root
|
|
419
|
+
adopts the window's four frame edges (``frame_edges``), so the window
|
|
420
|
+
frame and every divider move through one collision solve. Call from a
|
|
421
|
+
render_func body with its injected ``tile_state`` (without one the
|
|
422
|
+
tiles draw and their edges drag, but corners don't split). Returns
|
|
423
|
+
True when the layout, editor selection or editor content changed.
|
|
424
|
+
``multi_instance_renderers`` is supplied by the hosting render_func's
|
|
425
|
+
injected parameter of the same name. ``content_top`` optionally reserves
|
|
426
|
+
space above the tiles (an absolute screen y coordinate). The root must be a Split."""
|
|
427
|
+
window = layout_window(draw_state)
|
|
428
|
+
root_frame = frame_edges(window)
|
|
429
|
+
if content_top is not None:
|
|
430
|
+
# Keep the body boundary stable while menus above it change height.
|
|
431
|
+
top = tile_state.content_top if tile_state is not None else {}
|
|
432
|
+
top["y"] = content_top - window.abs_top
|
|
433
|
+
root_frame = (*root_frame[:2], top, root_frame[3])
|
|
434
|
+
changed = draw_tile_node(tree, root_frame, draw_state, (), tree=tree,
|
|
435
|
+
root_frame=root_frame, tile_state=tile_state,
|
|
436
|
+
gap=gap)
|
|
437
|
+
if changed:
|
|
438
|
+
# Retire layouts whose paths/axes moved before the next collision solve.
|
|
439
|
+
for axis in ("x", "y"):
|
|
440
|
+
for key, entry in list(_views(window, axis).items()):
|
|
441
|
+
if isinstance(key, tuple) and len(key) == 3 and entry[0] is draw_state:
|
|
442
|
+
del _views(window, axis)[key]
|
|
443
|
+
_specs(window, axis).pop(key, None)
|
|
444
|
+
_bands(window, axis).pop(key, None)
|
|
445
|
+
# Render content after topology edits, using the final leaf frames. The
|
|
446
|
+
# conversion's existing identity scopes views independently of tree paths.
|
|
447
|
+
from meltygui.view.tile_view import draw_tile_content
|
|
448
|
+
cursor = imgui.get_cursor_screen_pos()
|
|
449
|
+
for _path, tile, frame in resolve_frames(tree, root_frame):
|
|
450
|
+
if isinstance(tile, Split):
|
|
451
|
+
continue
|
|
452
|
+
if (tile_state is not None and tile_state.gesture is not None
|
|
453
|
+
and tile_state.gesture.get("new_tile") is tile):
|
|
454
|
+
# Instantiate the new editor at the committed split size, not the
|
|
455
|
+
# tiny first drag frame; otherwise its own column widths persist
|
|
456
|
+
# that transient geometry while the corner is still moving.
|
|
457
|
+
continue
|
|
458
|
+
rect = tile_rect(frame, draw_state, gap)
|
|
459
|
+
if rect is None:
|
|
460
|
+
continue
|
|
461
|
+
left, top, right, bottom = rect
|
|
462
|
+
# Leave the corner grips free to receive split/join gestures.
|
|
463
|
+
grip_inset = 14.0
|
|
464
|
+
if right - left <= 2 * grip_inset or bottom - top <= 2 * grip_inset:
|
|
465
|
+
continue
|
|
466
|
+
imgui.set_cursor_screen_pos((left + grip_inset, top + grip_inset))
|
|
467
|
+
content_changed, _ = draw_tile_content(
|
|
468
|
+
tile, width=right - left - 2 * grip_inset,
|
|
469
|
+
height=bottom - top - 2 * grip_inset,
|
|
470
|
+
multi_instance_renderers=multi_instance_renderers,
|
|
471
|
+
layout_frame=frame,
|
|
472
|
+
)
|
|
473
|
+
changed |= content_changed
|
|
474
|
+
imgui.set_cursor_screen_pos(cursor)
|
|
475
|
+
if tile_state is not None:
|
|
476
|
+
draw_join_preview(tree, root_frame, draw_state, tile_state)
|
|
477
|
+
left, right, top, bottom = root_frame
|
|
478
|
+
imgui.dummy(max(0.0, right["x"] - left["x"]), max(0.0, bottom["y"] - top["y"]))
|
|
479
|
+
return changed
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# ---------------------------------------------------------------------------
|
|
483
|
+
# Data interface - pure edits on the tree
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
def midpoint(near, far, axis):
|
|
487
|
+
return {axis: (near[axis] + far[axis]) / 2}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def split_tile(tree, path, axis, root_frame, new_tile=None, at=None,
|
|
491
|
+
before=False):
|
|
492
|
+
"""Split the tile at ``path`` along ``axis`` — Blender's area split.
|
|
493
|
+
The new tile lands after the old one (right of / below it), or before
|
|
494
|
+
it with ``before=True``, with a new edge at ``at`` (window coords on
|
|
495
|
+
that axis) or at the midpoint of the tile's frame (frames come from
|
|
496
|
+
``resolve_frames`` over ``root_frame``). Same axis as the parent: a
|
|
497
|
+
new sibling in the parent's flat list. Otherwise the leaf becomes a
|
|
498
|
+
two-child Split of ``axis``. Returns the new tile's path."""
|
|
499
|
+
if not path:
|
|
500
|
+
raise ValueError("tile layouts require a Split root containing the leaf")
|
|
501
|
+
frames = {p: f for p, _n, f in resolve_frames(tree, root_frame)}
|
|
502
|
+
frame = frames[path]
|
|
503
|
+
near, far = frame_pair(frame, axis)
|
|
504
|
+
edge = midpoint(near, far, axis) if at is None else {axis: float(at)}
|
|
505
|
+
tile = node_at(tree, path)
|
|
506
|
+
if new_tile is None:
|
|
507
|
+
new_tile = Tile(name=f"{tile.name}'", tint=tile.tint,
|
|
508
|
+
render_func=tile.render_func, input_value=tile.input_value)
|
|
509
|
+
pair = [new_tile, tile] if before else [tile, new_tile]
|
|
510
|
+
new_offset = 0 if before else 1
|
|
511
|
+
parent = node_at(tree, path[:-1]) if path else None
|
|
512
|
+
if parent is not None and parent.axis == axis:
|
|
513
|
+
index = path[-1]
|
|
514
|
+
parent.children[index:index + 1] = pair
|
|
515
|
+
parent.edges.insert(index, edge)
|
|
516
|
+
return path[:-1] + (index + new_offset,)
|
|
517
|
+
split = Split(axis=axis, children=pair, edges=[edge])
|
|
518
|
+
parent.children[path[-1]] = split
|
|
519
|
+
return path + (new_offset,)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def join_tiles(tree, path, direction=+1):
|
|
523
|
+
"""Join the tile at ``path`` into its sibling ``direction`` away (+1 =
|
|
524
|
+
the next, -1 = the previous) — Blender's area join: the tile and the
|
|
525
|
+
edge between them go, the neighbour takes the room. Returns the
|
|
526
|
+
neighbour's path after normalization."""
|
|
527
|
+
if direction not in (-1, +1):
|
|
528
|
+
raise ValueError("join direction must be -1 or +1")
|
|
529
|
+
if not path:
|
|
530
|
+
raise ValueError("cannot join the root")
|
|
531
|
+
parent = node_at(tree, path[:-1])
|
|
532
|
+
index = path[-1]
|
|
533
|
+
neighbour = index + direction
|
|
534
|
+
if not 0 <= neighbour < len(parent.children):
|
|
535
|
+
raise ValueError("no neighbour to join into")
|
|
536
|
+
if isinstance(parent.children[index], Split) or isinstance(parent.children[neighbour], Split):
|
|
537
|
+
raise ValueError("join requires two leaf tiles sharing a whole edge")
|
|
538
|
+
seeded = len(parent.edges) == len(parent.children) - 1
|
|
539
|
+
parent.children.pop(index)
|
|
540
|
+
if seeded:
|
|
541
|
+
parent.edges.pop(min(index, neighbour))
|
|
542
|
+
kept = neighbour if neighbour < index else index
|
|
543
|
+
kept_node = parent.children[kept]
|
|
544
|
+
normalize(tree)
|
|
545
|
+
return locate(tree, kept_node)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def locate(tree, target):
|
|
549
|
+
"""The path of ``target`` (by identity) in the tree, or None."""
|
|
550
|
+
for path, node in walk(tree):
|
|
551
|
+
if node is target:
|
|
552
|
+
return path
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def normalize(tree):
|
|
557
|
+
"""Restore the invariants after an edit: a Split with ONE child is
|
|
558
|
+
replaced by that child (the root keeps its Split shell, so the caller's
|
|
559
|
+
object identity holds), and a Split child of its parent's axis is
|
|
560
|
+
spliced into the parent's list, edges included. Returns the tree."""
|
|
561
|
+
changed = True
|
|
562
|
+
while changed:
|
|
563
|
+
changed = False
|
|
564
|
+
for _path, node in list(walk(tree)):
|
|
565
|
+
if not isinstance(node, Split):
|
|
566
|
+
continue
|
|
567
|
+
children = node.children
|
|
568
|
+
for index, child in enumerate(list(children)):
|
|
569
|
+
if not isinstance(child, Split):
|
|
570
|
+
continue
|
|
571
|
+
if len(child.children) == 1:
|
|
572
|
+
children[index] = child.children[0]
|
|
573
|
+
changed = True
|
|
574
|
+
elif child.axis == node.axis:
|
|
575
|
+
children[index:index + 1] = child.children
|
|
576
|
+
# The child's interior edges join the node's list at
|
|
577
|
+
# the slot before this cell's far edge.
|
|
578
|
+
node.edges[index:index] = child.edges
|
|
579
|
+
changed = True
|
|
580
|
+
if changed:
|
|
581
|
+
break
|
|
582
|
+
if changed:
|
|
583
|
+
break
|
|
584
|
+
# The root itself: one Split child of the root collapses into it.
|
|
585
|
+
if isinstance(tree, Split) and len(tree.children) == 1 \
|
|
586
|
+
and isinstance(tree.children[0], Split):
|
|
587
|
+
only = tree.children[0]
|
|
588
|
+
tree.axis = only.axis
|
|
589
|
+
tree.children = only.children
|
|
590
|
+
tree.edges = only.edges
|
|
591
|
+
return tree
|