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,161 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import meltygui_imgui as imgui
|
|
5
|
+
from meltygui.hdr_color import pack_color
|
|
6
|
+
|
|
7
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
8
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
9
|
+
from meltygui.utils.render_utils import LSDView
|
|
10
|
+
from meltygui.utils.render_utils import print_stack_trace
|
|
11
|
+
from meltygui.utils.render_utils import push_style_color
|
|
12
|
+
from meltygui.utils.render_utils import pop_style_color
|
|
13
|
+
from meltygui.utils.render_utils import print_colored_traceback
|
|
14
|
+
from meltygui.core.melty import Melty
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_cursor_pos_y(pos_y):
|
|
18
|
+
current_y = imgui.get_cursor_pos_y()
|
|
19
|
+
imgui.set_cursor_pos_y(pos_y)
|
|
20
|
+
height = imgui.get_cursor_pos_y() - current_y
|
|
21
|
+
if Toggles.show_line_breaks:
|
|
22
|
+
if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
|
|
23
|
+
print_stack_trace(skip=-2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_cursor_pos_x(pos_x):
|
|
27
|
+
current_x = imgui.get_cursor_pos_x()
|
|
28
|
+
imgui.set_cursor_pos_x(pos_x)
|
|
29
|
+
width = imgui.get_cursor_pos_x() - current_x
|
|
30
|
+
if Toggles.show_line_breaks:
|
|
31
|
+
if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
|
|
32
|
+
print_stack_trace(skip=-2)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_cursor_screen_pos(pos):
|
|
36
|
+
imgui.set_cursor_screen_pos(pos)
|
|
37
|
+
if Toggles.show_line_breaks:
|
|
38
|
+
if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
|
|
39
|
+
print_stack_trace(skip=-2)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_cursor_screen_position(pos):
|
|
43
|
+
imgui.set_cursor_screen_position(pos)
|
|
44
|
+
if Toggles.show_line_breaks:
|
|
45
|
+
if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
|
|
46
|
+
print_stack_trace(skip=-2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_cursor_pos(pos):
|
|
50
|
+
current_pos = imgui.get_cursor_pos()
|
|
51
|
+
imgui.set_cursor_pos(pos)
|
|
52
|
+
if Toggles.show_line_breaks:
|
|
53
|
+
if draw_rect(width=5, height=None, color=(1.0, 0.0, 1.0, 0.8)):
|
|
54
|
+
print_stack_trace(skip=-2)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def spacing():
|
|
58
|
+
imgui.spacing()
|
|
59
|
+
if Toggles.show_line_breaks:
|
|
60
|
+
if draw_rect(width=3, height=None, color=(0, 0.1, 0.7, 0.8)):
|
|
61
|
+
print_stack_trace(skip=-2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def indent(indent_size=None, attr_name=None):
|
|
65
|
+
if Toggles.show_line_breaks:
|
|
66
|
+
if draw_rect(width=3, height=None, color=(0.1, 0.1, 1.0, 0.8)):
|
|
67
|
+
print_stack_trace(skip=-2)
|
|
68
|
+
if attr_name is not None:
|
|
69
|
+
print(f"=======Indenting for {attr_name}=========")
|
|
70
|
+
|
|
71
|
+
if not (is_hovered(width=5, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
|
|
72
|
+
imgui.indent(indent_size)
|
|
73
|
+
else:
|
|
74
|
+
imgui.indent(indent_size)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def same_line(spacing=None):
|
|
78
|
+
if Toggles.show_line_breaks:
|
|
79
|
+
if draw_rect(width=3, height=None, color=(1.0, 0.5, 0.1, 0.5)):
|
|
80
|
+
print_stack_trace(skip=-2)
|
|
81
|
+
if not (is_hovered(width=3, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
|
|
82
|
+
if spacing is None:
|
|
83
|
+
imgui.same_line()
|
|
84
|
+
else:
|
|
85
|
+
imgui.same_line(spacing=spacing)
|
|
86
|
+
else:
|
|
87
|
+
if spacing is None:
|
|
88
|
+
imgui.same_line()
|
|
89
|
+
else:
|
|
90
|
+
imgui.same_line(spacing=spacing)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def new_line():
|
|
94
|
+
if Toggles.show_line_breaks:
|
|
95
|
+
if draw_rect(width=3, height=None, color=(0.5, 0, 0, 0.5)):
|
|
96
|
+
print_stack_trace(skip=-2)
|
|
97
|
+
if not (is_hovered(width=3, height=None) and imgui.is_mouse_down(imgui.MOUSE_BUTTON_MIDDLE)):
|
|
98
|
+
imgui.new_line()
|
|
99
|
+
else:
|
|
100
|
+
imgui.new_line()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def copy_attributes(source, target):
|
|
105
|
+
"""
|
|
106
|
+
Copies attributes from source to target, excluding private attributes.
|
|
107
|
+
If an attribute exists in both, it will be overwritten in the target.
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(source, DictConversion) and not isinstance(target, DictConversion):
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for attr_name, attr_value in source.__dict__.items():
|
|
113
|
+
setattr(target, attr_name, attr_value)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_hovered(x=None, y=None, width=5, height=None):
|
|
117
|
+
padding = imgui.get_style().window_padding
|
|
118
|
+
if x is None:
|
|
119
|
+
x = imgui.get_cursor_screen_pos()[0] - padding[0]
|
|
120
|
+
if y is None:
|
|
121
|
+
y = imgui.get_cursor_screen_pos()[1] - padding[1]
|
|
122
|
+
if height is None:
|
|
123
|
+
line_height = imgui.get_text_line_height()
|
|
124
|
+
height = line_height + padding[1] * 2
|
|
125
|
+
return imgui.is_mouse_hovering_rect(x, y, x + max(width, 10), y + height)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_dict_as_table(input_value=None, config=None):
|
|
129
|
+
unique = config.unique
|
|
130
|
+
|
|
131
|
+
num_columns = 2 # For key / value pair
|
|
132
|
+
imgui.columns(num_columns, f"dict_columns##{unique}", True)
|
|
133
|
+
for key, value in input_value.items():
|
|
134
|
+
imgui.text(f"{key}")
|
|
135
|
+
imgui.next_column()
|
|
136
|
+
imgui.text(f"{value}")
|
|
137
|
+
imgui.next_column()
|
|
138
|
+
|
|
139
|
+
imgui.columns(1) # Reset to single column layout
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def draw_rect(x=None, y=None, width=5, height=None, color=(1, 1, 1, 1)):
|
|
143
|
+
padding = imgui.get_style().window_padding
|
|
144
|
+
if x is None:
|
|
145
|
+
x = imgui.get_cursor_screen_pos()[0] - padding[0]
|
|
146
|
+
if y is None:
|
|
147
|
+
y = imgui.get_cursor_screen_pos()[1] - padding[1]
|
|
148
|
+
if height is None:
|
|
149
|
+
line_height = imgui.get_text_line_height()
|
|
150
|
+
height = line_height + padding[1] * 2
|
|
151
|
+
imgui.get_foreground_draw_list().add_rect_filled(
|
|
152
|
+
x, y, x + width, y + height, pack_color(*color))
|
|
153
|
+
|
|
154
|
+
clicked = False
|
|
155
|
+
if imgui.is_mouse_hovering_rect(x, y, x + max(width, 10), y + height):
|
|
156
|
+
white = (1.0, 1.0, 1.0, 1.0)
|
|
157
|
+
imgui.get_foreground_draw_list().add_rect(
|
|
158
|
+
x, y, x + width, y + height, pack_color(*white))
|
|
159
|
+
if imgui.is_mouse_clicked(imgui.MOUSE_BUTTON_LEFT):
|
|
160
|
+
clicked = True
|
|
161
|
+
return clicked
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Dropdown core functions and supporting definitions."""
|
|
2
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
3
|
+
from meltygui.core.melty import Melty
|
|
4
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _close_menu(draw_state, state):
|
|
8
|
+
"""Close whatever menu is showing: release the popover slot, collapse
|
|
9
|
+
that menu's paths, hand text focus back to whoever had it."""
|
|
10
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
11
|
+
|
|
12
|
+
if state.open_title is not None:
|
|
13
|
+
menu_state = state.menus.get(state.open_title)
|
|
14
|
+
_dd_close(menu_state)
|
|
15
|
+
# A picked action may remove this state before its next draw call.
|
|
16
|
+
# Close its surface now rather than waiting for closed=True next frame.
|
|
17
|
+
if menu_state is not None and menu_state._menu_ds is not None:
|
|
18
|
+
menu_state._menu_ds.closed = True
|
|
19
|
+
state.open_title = None
|
|
20
|
+
if Melty.popover_focused_ds is draw_state:
|
|
21
|
+
Melty.popover_focused_ds = None
|
|
22
|
+
if Melty.text_focused_ds is draw_state:
|
|
23
|
+
previous = state._prev_text_focus
|
|
24
|
+
Melty.text_focused_ds = previous if previous is not draw_state else None
|
|
25
|
+
if previous is not None:
|
|
26
|
+
Melty._text_focus_grant_frame = Melty.frame_count
|
|
27
|
+
state._prev_text_focus = None
|
|
28
|
+
draw_state.invalidate()
|
|
29
|
+
request_render()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ds_in_subtree(node, ancestor, max_depth=64):
|
|
33
|
+
"""Use the same window ancestry as focus dispatch, including popovers."""
|
|
34
|
+
return ancestor.id in Core.melty.ancestor_closure([node])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dd_set_cursor(root_state, cursor_path, is_branch, menu_ds=None):
|
|
38
|
+
"""Point the highlight at `cursor_path` and derive the open path from it: a
|
|
39
|
+
branch expands its own sub-menu, a leaf collapses back to its parent level.
|
|
40
|
+
This is the single writer for both hover and keyboard, so they stay in sync.
|
|
41
|
+
|
|
42
|
+
When the OPEN path changes, the containing menu's row tiles must re-run:
|
|
43
|
+
a branch row stamps `closed` on its submenu window only when its body
|
|
44
|
+
actually runs, and the blit cache has no kwargs key — a clean row tile
|
|
45
|
+
blit-skips even though its open_path input changed, leaving the submenu
|
|
46
|
+
stamped open forever (one leaked submenu per branch row swept). Keyboard
|
|
47
|
+
nav never leaked because begin_frame cascades invalidate_up from the menu
|
|
48
|
+
tile on every nav key; this is the hover-side equivalent. `menu_ds` is the
|
|
49
|
+
level's menu-window draw_state (row tiles are its direct tile children)."""
|
|
50
|
+
from meltygui.model.dropdown_model import _dd_as_tuple
|
|
51
|
+
|
|
52
|
+
if root_state is None:
|
|
53
|
+
return
|
|
54
|
+
new_cursor = tuple(cursor_path)
|
|
55
|
+
new_open = tuple(cursor_path) if is_branch else tuple(cursor_path[:-1])
|
|
56
|
+
old_open = _dd_as_tuple(root_state.open_path)
|
|
57
|
+
if (new_cursor == _dd_as_tuple(root_state.cursor_path)
|
|
58
|
+
and new_open == old_open):
|
|
59
|
+
return
|
|
60
|
+
root_state.cursor_path = new_cursor
|
|
61
|
+
root_state.open_path = new_open
|
|
62
|
+
if new_open != old_open:
|
|
63
|
+
_dd_invalidate_rows(root_state, menu_ds)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _dd_invalidate_rows(root_state, menu_ds=None):
|
|
67
|
+
"""Cascade-invalidate a menu window's tile subtree so every row body
|
|
68
|
+
re-runs and re-stamps `closed` on its submenu from the current open_path.
|
|
69
|
+
Without a menu_ds at hand (close paths), resolve the root menu through the
|
|
70
|
+
search box tile — its parent_window is the root menu. bypass_clip so a
|
|
71
|
+
branch row scrolled out of the menu viewport is cleaned too (otherwise
|
|
72
|
+
scrolling it back in would revive its stale-open submenu). Submenu windows
|
|
73
|
+
are tile ROOTS (not tile children of their spawner rows), so this reaches
|
|
74
|
+
rows, not window interiors — which is all closing needs."""
|
|
75
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
76
|
+
|
|
77
|
+
if menu_ds is None and root_state is not None:
|
|
78
|
+
box_tile = getattr(root_state, "_search_box_tile", None)
|
|
79
|
+
if box_tile is not None:
|
|
80
|
+
box_ds = Melty.cache.key_to_draw_state.get(box_tile)
|
|
81
|
+
menu_ds = getattr(box_ds, "parent_window", None) if box_ds is not None else None
|
|
82
|
+
if menu_ds is None or getattr(menu_ds, "_tile_id", None) is None:
|
|
83
|
+
return
|
|
84
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
85
|
+
Melty.cache.invalidate_up(menu_ds._tile_id, force=True, bypass_clip=True,
|
|
86
|
+
note=Note(name="dd open_path change", tint=(1, 0.6, 0.2)))
|
|
87
|
+
request_render()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _dd_scroll_cursor_into_view(menu_ds, row_index, row0_offset=0.0, pitch=None):
|
|
91
|
+
"""Nudge a menu window's scroll_offset the minimal amount so keyboard-cursor
|
|
92
|
+
row `row_index` is fully visible. Rows are a fixed _DD_ROW_H pitch drawn
|
|
93
|
+
flush from the content origin in the manual-loop path; `row0_offset` covers
|
|
94
|
+
a level that draws chrome above its rows. Stateless — pure geometry from
|
|
95
|
+
the live draw_state, clamped to the wrapper-published _max_scroll_y so this
|
|
96
|
+
writer never fights core_render's own clamp."""
|
|
97
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
98
|
+
|
|
99
|
+
if menu_ds is None or row_index is None or row_index < 0:
|
|
100
|
+
return
|
|
101
|
+
pitch = Toggles.Dropdown.row_height if pitch is None else pitch # the usage picker's rows are taller
|
|
102
|
+
view_h = menu_ds.abs_clipped_height - menu_ds.header_height - menu_ds.footer_height
|
|
103
|
+
sx, sy = menu_ds.scroll_offset
|
|
104
|
+
row_top = row0_offset + row_index * pitch
|
|
105
|
+
row_bot = row_top + pitch
|
|
106
|
+
new_sy = sy
|
|
107
|
+
if view_h > 0 and row_bot > new_sy + view_h: # below the viewport: minimal scroll down
|
|
108
|
+
new_sy = row_bot - view_h
|
|
109
|
+
max_y = menu_ds._max_scroll_y
|
|
110
|
+
if max_y is not None:
|
|
111
|
+
new_sy = min(new_sy, max_y)
|
|
112
|
+
if row_top < new_sy: # above the viewport (top-centers a tiny view)
|
|
113
|
+
new_sy = row_top
|
|
114
|
+
new_sy = max(0.0, new_sy)
|
|
115
|
+
if new_sy != sy:
|
|
116
|
+
menu_ds.scroll_offset = (sx, new_sy)
|
|
117
|
+
# Own the repaint edge: scroll is BAKED into the blit capture at render
|
|
118
|
+
# time and only the wheel-event callback repaints on scroll change, so a
|
|
119
|
+
# clean tile would replay the old-offset capture (the caller's repaint gate
|
|
120
|
+
# doesn't always fire - e.g. a snap-to-top when the filtered list is
|
|
121
|
+
# value-identical). Change-edge-gated by `new_sy != sy` above, never
|
|
122
|
+
# per-frame. bypass_clip: the popup can protrude outside its parent.
|
|
123
|
+
if menu_ds._tile_id is not None:
|
|
124
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
125
|
+
Melty.cache.invalidate_up(menu_ds._tile_id, force=True, bypass_clip=True,
|
|
126
|
+
note=Note(name="dd scroll-into-view", tint=(1, 0.6, 0.2)))
|
|
127
|
+
request_render()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _dd_pick(root_state, path):
|
|
131
|
+
"""Keep a selection until the owning dropdown consumes it.
|
|
132
|
+
|
|
133
|
+
Submenus render as independent floating windows, often after their parent
|
|
134
|
+
has already drawn. A one-frame return value cannot reliably cross that
|
|
135
|
+
boundary; the shared dropdown state owns the pending selection.
|
|
136
|
+
"""
|
|
137
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
138
|
+
|
|
139
|
+
root_state._picked_path = tuple(path)
|
|
140
|
+
root_state._pending_pick = tuple(path)
|
|
141
|
+
owner = Melty.popover_focused_ds
|
|
142
|
+
if owner is not None:
|
|
143
|
+
owner.invalidate_up()
|
|
144
|
+
request_render()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _dd_close(root_state):
|
|
148
|
+
"""Reset popover state on close: collapse the open/cursor paths, clear the
|
|
149
|
+
search query, and release the search box's text focus if it held it."""
|
|
150
|
+
from meltygui.model.dropdown_model import _dd_as_tuple
|
|
151
|
+
|
|
152
|
+
if root_state is None:
|
|
153
|
+
return
|
|
154
|
+
menu = getattr(root_state, "_menu_ds", None)
|
|
155
|
+
if menu is not None:
|
|
156
|
+
menu.closed = True
|
|
157
|
+
root_state._pending_pick = None
|
|
158
|
+
open_was = _dd_as_tuple(root_state.open_path)
|
|
159
|
+
root_state.open_path = ()
|
|
160
|
+
root_state.cursor_path = ()
|
|
161
|
+
root_state.search_query = ""
|
|
162
|
+
root_state.search = ""
|
|
163
|
+
root_state._focus_search = 0
|
|
164
|
+
root_state._had_focus = False
|
|
165
|
+
# A branch row left stamped open would blit-skip on reopen and revive its
|
|
166
|
+
# submenu window even though open_path was reset - dirty the rows now (the
|
|
167
|
+
# flags persist while the menu is hidden) so the first reopen render
|
|
168
|
+
# re-stamps every submenu closed.
|
|
169
|
+
if open_was:
|
|
170
|
+
_dd_invalidate_rows(root_state)
|
|
171
|
+
box_tile = getattr(root_state, "_search_box_tile", None)
|
|
172
|
+
tf = Melty.text_focused_ds
|
|
173
|
+
if tf is not None and box_tile is not None and getattr(tf, "_tile_id", None) == box_tile:
|
|
174
|
+
Melty.text_focused_ds = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dd_handle_keys(collection, root_state, search="", text_focused=False):
|
|
178
|
+
"""Arrow-key navigation while the popover is open. Up/Down move within the
|
|
179
|
+
current level, Right (or Enter on a branch) descends, Left collapses to the
|
|
180
|
+
parent, Enter on a leaf picks it. Returns the picked leaf value, or
|
|
181
|
+
UNSET_VALUE when nothing was chosen this frame. Reads the GLFW-callback key
|
|
182
|
+
queue so it works without the menu being hovered."""
|
|
183
|
+
from meltygui.core.conversion.cache_tree import UNSET_VALUE
|
|
184
|
+
from meltygui.model.dropdown_model import _dd_as_tuple
|
|
185
|
+
from meltygui.model.dropdown_model import _dd_rows_at
|
|
186
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
187
|
+
import meltygui.core.windowing.window_api as glfw
|
|
188
|
+
|
|
189
|
+
if root_state is None:
|
|
190
|
+
return UNSET_VALUE
|
|
191
|
+
keys = list(Core.melty.frame_key_events)
|
|
192
|
+
|
|
193
|
+
def pressed(*codes):
|
|
194
|
+
return any(k in codes for k, _ in keys)
|
|
195
|
+
|
|
196
|
+
down = pressed(glfw.KEY_DOWN)
|
|
197
|
+
up = pressed(glfw.KEY_UP)
|
|
198
|
+
right = pressed(glfw.KEY_RIGHT)
|
|
199
|
+
left = pressed(glfw.KEY_LEFT)
|
|
200
|
+
enter = pressed(glfw.KEY_ENTER, glfw.KEY_KP_ENTER)
|
|
201
|
+
# While typing a query, Left/Right are the search box's text cursor, not tree
|
|
202
|
+
# navigation, so the dropdown doesn't double-act on them.
|
|
203
|
+
if text_focused and search:
|
|
204
|
+
left = right = False
|
|
205
|
+
if not (down or up or right or left or enter):
|
|
206
|
+
return UNSET_VALUE
|
|
207
|
+
|
|
208
|
+
# A nav key fired: switch to keyboard-select mode so hover stops moving the
|
|
209
|
+
# cursor until the mouse actually moves again (cleared in draw_dropdown).
|
|
210
|
+
root_state._kbd_mode = True
|
|
211
|
+
|
|
212
|
+
# Navigation level = the cursor's parent; rows = its (search-filtered)
|
|
213
|
+
# siblings. Fall back to the root level if the cursor path went stale.
|
|
214
|
+
cursor = _dd_as_tuple(getattr(root_state, "cursor_path", ()))
|
|
215
|
+
level = cursor[:-1]
|
|
216
|
+
rows = _dd_rows_at(collection, level, search)
|
|
217
|
+
if not rows:
|
|
218
|
+
level = ()
|
|
219
|
+
rows = _dd_rows_at(collection, level, search)
|
|
220
|
+
cursor = ()
|
|
221
|
+
if not rows:
|
|
222
|
+
return UNSET_VALUE
|
|
223
|
+
|
|
224
|
+
level_keys = [r[0] for r in rows]
|
|
225
|
+
had_cursor = bool(cursor) and cursor[-1] in level_keys
|
|
226
|
+
idx = level_keys.index(cursor[-1]) if had_cursor else 0
|
|
227
|
+
|
|
228
|
+
# Left collapses the current sub-menu and highlights its parent row.
|
|
229
|
+
if left and level:
|
|
230
|
+
root_state.cursor_path = tuple(level)
|
|
231
|
+
root_state.open_path = tuple(level[:-1])
|
|
232
|
+
request_render()
|
|
233
|
+
return UNSET_VALUE
|
|
234
|
+
|
|
235
|
+
# From no selection, the first Up/Down just focuses on the first row; otherwise
|
|
236
|
+
# it steps (wrapping). Right/Enter act on whatever row is current.
|
|
237
|
+
if had_cursor:
|
|
238
|
+
if down:
|
|
239
|
+
idx = (idx + 1) % len(rows)
|
|
240
|
+
elif up:
|
|
241
|
+
idx = (idx - 1) % len(rows)
|
|
242
|
+
key, value, label, is_branch = rows[idx]
|
|
243
|
+
new_cursor = tuple(level) + (key,)
|
|
244
|
+
|
|
245
|
+
# Right / Enter on a branch descends into its first visible child.
|
|
246
|
+
if (right or enter) and is_branch:
|
|
247
|
+
kids = _dd_rows_at(collection, new_cursor, search)
|
|
248
|
+
if kids:
|
|
249
|
+
ck, cv, _cl, cbr = kids[0]
|
|
250
|
+
_dd_set_cursor(root_state, new_cursor + (ck,), cbr)
|
|
251
|
+
request_render()
|
|
252
|
+
return UNSET_VALUE
|
|
253
|
+
|
|
254
|
+
if enter and not is_branch:
|
|
255
|
+
_dd_set_cursor(root_state, new_cursor, False)
|
|
256
|
+
root_state._picked_path = tuple(new_cursor)
|
|
257
|
+
return value
|
|
258
|
+
|
|
259
|
+
_dd_set_cursor(root_state, new_cursor, is_branch)
|
|
260
|
+
request_render()
|
|
261
|
+
return UNSET_VALUE
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _dd_update_menu_size(state, menu_ds, min_width=None, max_height=None):
|
|
265
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
266
|
+
from meltygui.view.dropdown_view import _dd_menu_fit
|
|
267
|
+
|
|
268
|
+
current = (menu_ds.width, menu_ds.height)
|
|
269
|
+
last_fit = getattr(state, "_menu_fit", None)
|
|
270
|
+
if (getattr(menu_ds, "_initial_window_size", None) is not None
|
|
271
|
+
and last_fit is not None and current != last_fit and all(current)):
|
|
272
|
+
state.menu_size = current
|
|
273
|
+
elif getattr(state, "menu_size", None) is None:
|
|
274
|
+
fit = _dd_menu_fit(menu_ds, min_width=min_width, max_height=max_height)
|
|
275
|
+
if fit is not None and fit != current:
|
|
276
|
+
menu_ds.width, menu_ds.height = fit
|
|
277
|
+
request_render()
|
|
278
|
+
state._menu_fit = (menu_ds.width, menu_ds.height)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""One-dimensional edge constraints, independent of views and native backends.
|
|
2
|
+
|
|
3
|
+
Cells connect shared edge objects with a minimum and optional maximum span.
|
|
4
|
+
Motion propagates only through those connections. The caller supplies walls and
|
|
5
|
+
chooses which contacts belong to an interaction; overlapping coordinates alone
|
|
6
|
+
do not create a collision.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
class EdgeGraph:
|
|
10
|
+
"""Adjacency over cells: ``ahead[id(near)]`` → ``[(far, floor, cap)]``,
|
|
11
|
+
``behind[id(far)]`` → ``[(near, floor, cap)]``, ``nodes`` id → edge."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, cells):
|
|
14
|
+
self.ahead, self.behind, self.nodes = {}, {}, {}
|
|
15
|
+
for near, far, floor, cap in cells:
|
|
16
|
+
self.nodes[id(near)] = near
|
|
17
|
+
self.nodes[id(far)] = far
|
|
18
|
+
self.ahead.setdefault(id(near), []).append((far, floor, cap))
|
|
19
|
+
self.behind.setdefault(id(far), []).append((near, floor, cap))
|
|
20
|
+
|
|
21
|
+
def chain(self, start, goal, walls=frozenset(), forward=True,
|
|
22
|
+
capped_only=False):
|
|
23
|
+
"""Weight of the binding chain of cells from ``start`` to ``goal``:
|
|
24
|
+
the LONGEST sum of floors (a push chain — ``capped_only=False``) or
|
|
25
|
+
the SHORTEST sum of caps over capped cells only (a pull chain —
|
|
26
|
+
``capped_only=True``), walking cells ahead (``forward``) or behind.
|
|
27
|
+
None when no chain links them. Chains never pass through another
|
|
28
|
+
wall: a wall never moves, so nothing propagates past it."""
|
|
29
|
+
links = self.ahead if forward else self.behind
|
|
30
|
+
pick = min if capped_only else max
|
|
31
|
+
memo, on_stack = {}, set()
|
|
32
|
+
|
|
33
|
+
def best(edge):
|
|
34
|
+
edge_id = id(edge)
|
|
35
|
+
if edge_id == id(goal):
|
|
36
|
+
return 0.0
|
|
37
|
+
if edge_id in walls or edge_id in on_stack:
|
|
38
|
+
return None
|
|
39
|
+
if edge_id in memo:
|
|
40
|
+
return memo[edge_id]
|
|
41
|
+
on_stack.add(edge_id)
|
|
42
|
+
found = None
|
|
43
|
+
for other, floor, cap in links.get(edge_id, ()):
|
|
44
|
+
if capped_only:
|
|
45
|
+
if cap is None:
|
|
46
|
+
continue
|
|
47
|
+
weight = cap
|
|
48
|
+
else:
|
|
49
|
+
weight = floor
|
|
50
|
+
rest = best(other)
|
|
51
|
+
if rest is None:
|
|
52
|
+
continue
|
|
53
|
+
total = weight + rest
|
|
54
|
+
found = total if found is None else pick(found, total)
|
|
55
|
+
on_stack.discard(edge_id)
|
|
56
|
+
memo[edge_id] = found
|
|
57
|
+
return found
|
|
58
|
+
|
|
59
|
+
return best(start)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def solve_edge(graph, edge, target, walls=frozenset(), axis="x"):
|
|
63
|
+
"""Move ``edge`` to ``target`` through the cell graph. Edges are
|
|
64
|
+
independent objects: no other edge moves unless the moving edge (or
|
|
65
|
+
one it already carried) makes CONTACT through a cell — two kinds, one
|
|
66
|
+
per side of the moving edge:
|
|
67
|
+
|
|
68
|
+
PUSH, ahead: the cell in front closes to its floor (_edge_min) and
|
|
69
|
+
its far edge is shoved on ahead.
|
|
70
|
+
PULL, behind: the cell it leaves behind opens to its cap (_edge_max)
|
|
71
|
+
and its far edge is dragged along behind.
|
|
72
|
+
|
|
73
|
+
Either chain runs cell by cell — a pushed edge closes the next cell, a
|
|
74
|
+
pulled edge opens the next — and stops at the first cell with slack;
|
|
75
|
+
consecutive capped cells therefore travel as one, exactly as
|
|
76
|
+
consecutive floor-packed cells do. A cell with no cap never pulls. An
|
|
77
|
+
edge several cells share (a nested layout's far edge) carries every
|
|
78
|
+
cell it bounds.
|
|
79
|
+
|
|
80
|
+
``walls`` is a set of edge ids the cascade must NOT move. Contact stops
|
|
81
|
+
dead at a wall: the *dragged* edge itself is clamped so the pile packs
|
|
82
|
+
against the wall at its floors (push side) or stretches to its summed
|
|
83
|
+
caps (pull side) instead of the chain shoving the wall along. Used by
|
|
84
|
+
_solve_collisions to keep one FRAME edge from moving the other (breaks
|
|
85
|
+
the foreign-width feedback loop — see there; its mirror image is a
|
|
86
|
+
fully-capped row, which refuses a foreign widening the same way);
|
|
87
|
+
interior divider drags pass no walls, so a divider can still push or
|
|
88
|
+
pull a frame edge and slide/grow/shrink the window 1:1 with the
|
|
89
|
+
cursor. Returns True if anything moved."""
|
|
90
|
+
old = edge[axis]
|
|
91
|
+
if target == old or id(edge) not in graph.nodes:
|
|
92
|
+
return False
|
|
93
|
+
forward = target > old
|
|
94
|
+
# Wall clamps first: ahead through the floors, behind through the caps
|
|
95
|
+
# (a pull chain can only reach a wall over capped cells). The
|
|
96
|
+
# binding chain per wall is the longest floor chain / shortest cap
|
|
97
|
+
# chain - the one that would move the wall first.
|
|
98
|
+
for wall_id in walls:
|
|
99
|
+
wall = graph.nodes.get(wall_id)
|
|
100
|
+
if wall is None or wall is edge:
|
|
101
|
+
continue
|
|
102
|
+
push = graph.chain(edge, wall, walls, forward=forward)
|
|
103
|
+
if push is not None:
|
|
104
|
+
target = (min(target, wall[axis] - push) if forward
|
|
105
|
+
else max(target, wall[axis] + push))
|
|
106
|
+
pull = graph.chain(edge, wall, walls, forward=not forward,
|
|
107
|
+
capped_only=True)
|
|
108
|
+
if pull is not None:
|
|
109
|
+
target = (min(target, wall[axis] + pull) if forward
|
|
110
|
+
else max(target, wall[axis] - pull))
|
|
111
|
+
if target == old:
|
|
112
|
+
return False
|
|
113
|
+
edge[axis] = float(target)
|
|
114
|
+
# Propagate contact. Every relaxation moves an edge the way the drag
|
|
115
|
+
# went and never back, so this is a monotone worklist that settles on
|
|
116
|
+
# its own; the guard only ever trips on a cyclic (corrupt) cell graph.
|
|
117
|
+
pending = [edge]
|
|
118
|
+
guard = 64 * (len(graph.nodes) + 1)
|
|
119
|
+
while pending and guard > 0:
|
|
120
|
+
guard -= 1
|
|
121
|
+
current = pending.pop()
|
|
122
|
+
current_id = id(current)
|
|
123
|
+
if forward:
|
|
124
|
+
for far, floor, _cap in graph.ahead.get(current_id, ()): # push ahead
|
|
125
|
+
if id(far) in walls:
|
|
126
|
+
continue
|
|
127
|
+
need = current[axis] + floor
|
|
128
|
+
if far[axis] < need:
|
|
129
|
+
far[axis] = need
|
|
130
|
+
pending.append(far)
|
|
131
|
+
for near, _floor, cap in graph.behind.get(current_id, ()): # pull behind
|
|
132
|
+
if cap is None or id(near) in walls:
|
|
133
|
+
continue
|
|
134
|
+
need = current[axis] - cap
|
|
135
|
+
if near[axis] < need:
|
|
136
|
+
near[axis] = need
|
|
137
|
+
pending.append(near)
|
|
138
|
+
else:
|
|
139
|
+
for near, floor, _cap in graph.behind.get(current_id, ()): # push ahead
|
|
140
|
+
if id(near) in walls:
|
|
141
|
+
continue
|
|
142
|
+
need = current[axis] - floor
|
|
143
|
+
if near[axis] > need:
|
|
144
|
+
near[axis] = need
|
|
145
|
+
pending.append(near)
|
|
146
|
+
for far, _floor, cap in graph.ahead.get(current_id, ()): # pull behind
|
|
147
|
+
if cap is None or id(far) in walls:
|
|
148
|
+
continue
|
|
149
|
+
need = current[axis] + cap
|
|
150
|
+
if far[axis] > need:
|
|
151
|
+
far[axis] = need
|
|
152
|
+
pending.append(far)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|