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,2394 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import meltygui.core.windowing.window_api as glfw
|
|
6
|
+
import meltygui_imgui as imgui
|
|
7
|
+
from meltygui.hdr_color import pack_color
|
|
8
|
+
|
|
9
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
10
|
+
from meltygui.core.runtime.toggles import shadow_depth_at
|
|
11
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
12
|
+
from meltygui.core.windowing.glfw_utils import print_stack_trace
|
|
13
|
+
from meltygui.core.conversion.cache_tree import CacheTree
|
|
14
|
+
from meltygui.core.conversion.cache_tree import UNSET_VALUE
|
|
15
|
+
from meltygui.core.rendering.core_decoration import no_save
|
|
16
|
+
from meltygui.core.rendering.core_decoration import exclude
|
|
17
|
+
from meltygui.core.rendering.core_decoration import deep_refresh
|
|
18
|
+
from meltygui.core.rendering.core_decoration import no_save_exclude
|
|
19
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
20
|
+
from meltygui.core.rendering.core_decoration import defaults
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SynthColors(DictConversion):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.letter_to_color = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CSTDrawBits:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.path_key: tuple[tuple[str, int | None], ...] = ()
|
|
33
|
+
self.module_id: str = ""
|
|
34
|
+
self.root_gen: int = 0
|
|
35
|
+
self.anchor: tuple = ()
|
|
36
|
+
self.text_buf: str = "" # generic edit buffer
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DragMode(Enum):
|
|
40
|
+
NONE = 'none'
|
|
41
|
+
WINDOW = 'move'
|
|
42
|
+
RESIZE_BR = 'resize_br'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Anchor(Enum):
|
|
46
|
+
TOP_LEFT = 'top_left'
|
|
47
|
+
TOP_CENTER = 'top_center'
|
|
48
|
+
TOP_RIGHT = 'top_right'
|
|
49
|
+
CENTER_LEFT = 'center_left'
|
|
50
|
+
CENTER = 'center'
|
|
51
|
+
CENTER_RIGHT = 'center_right'
|
|
52
|
+
BOTTOM_LEFT = 'bottom_left'
|
|
53
|
+
BOTTOM_CENTER = 'bottom_center'
|
|
54
|
+
BOTTOM_RIGHT = 'bottom_right'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Anchor classification used for both anchor_offset (window placement) and
|
|
58
|
+
# clip-rect pinning. Anything not in left/right is horizontally centered;
|
|
59
|
+
# anything not in top/bottom is vertically centered.
|
|
60
|
+
class Pin(Enum):
|
|
61
|
+
"""Symbolic pin targets for ``pin_to_clip`` when the caller doesn't have a
|
|
62
|
+
draw_state reference handy. Resolved to the declaring view's relatives at
|
|
63
|
+
render time: PARENT -> ``_parent`` (immediate render-tree parent), WINDOW ->
|
|
64
|
+
``parent_window`` (enclosing Melty window)."""
|
|
65
|
+
PARENT = 'parent'
|
|
66
|
+
GRANDPARENT = 'grandparent'
|
|
67
|
+
WINDOW = 'window'
|
|
68
|
+
# The active clip rect (scissor test) rather than a view's box. Clamped to
|
|
69
|
+
# the parent window so corners track the visible edge - e.g. bottom becomes
|
|
70
|
+
# min(parent_bottom, clip_bottom). Useful for overlays that must fit inside
|
|
71
|
+
# a scrolled/clipped region.
|
|
72
|
+
CLIP = 'clip'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
LEFT_ANCHORS = (Anchor.TOP_LEFT, Anchor.CENTER_LEFT, Anchor.BOTTOM_LEFT)
|
|
76
|
+
RIGHT_ANCHORS = (Anchor.TOP_RIGHT, Anchor.CENTER_RIGHT, Anchor.BOTTOM_RIGHT)
|
|
77
|
+
TOP_ANCHORS = (Anchor.TOP_LEFT, Anchor.TOP_CENTER, Anchor.TOP_RIGHT)
|
|
78
|
+
BOTTOM_ANCHORS = (Anchor.BOTTOM_LEFT, Anchor.BOTTOM_CENTER, Anchor.BOTTOM_RIGHT)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ApplyMode(Enum):
|
|
82
|
+
INSTANT = 'instant'
|
|
83
|
+
ON_RELEASE = 'on_release'
|
|
84
|
+
CONFIRM = 'confirm'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ExpandMode(Enum):
|
|
88
|
+
"""How the render_func wrapper treats a collapsed (is_tree) view.
|
|
89
|
+
|
|
90
|
+
AUTO - wrapper owns collapse: the view func is skipped while collapsed
|
|
91
|
+
and the box shrinks to its header (default, legacy behavior).
|
|
92
|
+
MANUAL - the view func is ALWAYS called and reads draw_state.expanded to
|
|
93
|
+
choose its own collapsed rendering (e.g. draw_comment showing
|
|
94
|
+
just the first line). The wrapper skips its collapsed-state
|
|
95
|
+
sizing shortcuts so whatever the func draws is measured normally.
|
|
96
|
+
"""
|
|
97
|
+
AUTO = 'auto'
|
|
98
|
+
MANUAL = 'manual'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@no_save_exclude()
|
|
102
|
+
class TabState(DictConversion):
|
|
103
|
+
def __init__(self):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.selected_tabs = []
|
|
106
|
+
self.tab_tints = {}
|
|
107
|
+
self.tab_icons = {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ColorPickerState(DictConversion):
|
|
111
|
+
"""draw_color_picker's persisted state (injected via
|
|
112
|
+
`picker_state: ColorPickerState = None`): which tab is showing —
|
|
113
|
+
"wide" (the P3 + brightness square, the default), "srgb" (the
|
|
114
|
+
classic square) or "extended" (the classic square with the P3
|
|
115
|
+
extension to its right, the sRGB+ tab)."""
|
|
116
|
+
|
|
117
|
+
def __init__(self):
|
|
118
|
+
super().__init__()
|
|
119
|
+
self.tab = "wide"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@no_save("fit_phase")
|
|
125
|
+
class ContextMenuWindowState(DictConversion):
|
|
126
|
+
"""Per-menu persisted state for the context menu WINDOW, draw_context_menu
|
|
127
|
+
(injected via `menu_state: ContextMenuWindowState = None` — the TabState
|
|
128
|
+
pattern). Not the input tab's host cache, new_core_view.ContextMenuState."""
|
|
129
|
+
|
|
130
|
+
def __init__(self):
|
|
131
|
+
super().__init__()
|
|
132
|
+
# True once the menu's first-load auto-fit has sized the window.
|
|
133
|
+
# PERSISTED on purpose: the menu draw_state (so its width/height)
|
|
134
|
+
# survives a restart, and a menu restored open must not be re-fitted -
|
|
135
|
+
# it overwrote the user's size with the default width + a content
|
|
136
|
+
# fit (an underscore draw_state attr never serialized, 08-27).
|
|
137
|
+
self.fit_done = False
|
|
138
|
+
# The fit's two-frame sequence (0: stamp header width, 1: measure
|
|
139
|
+
# height). Session-only - a fit never spans a restart.
|
|
140
|
+
self.fit_phase = 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@exclude("restore_first_line", "restore_total_lines", "restore_text",
|
|
144
|
+
"restore_gutter_digits", "restore_line_offset", "restore_fold_keys",
|
|
145
|
+
"restore_gutter_rows", "restore_diff_collapsed", "restore_diff_rows",
|
|
146
|
+
"restore_preview_rows")
|
|
147
|
+
class TextEditorState(DictConversion):
|
|
148
|
+
"""Per-editor persisted UI state for draw_text (injected via
|
|
149
|
+
`text_editor_state: TextEditorState = None` — the TabState pattern:
|
|
150
|
+
created into draw_state.misc and serialized with it)."""
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
super().__init__()
|
|
154
|
+
self._completion_explicit = False
|
|
155
|
+
self._token_matches = None
|
|
156
|
+
self._completion_buffer = None
|
|
157
|
+
self._completion_anchor = None
|
|
158
|
+
self._signature_dismissed = None
|
|
159
|
+
# {def_name: bool} - whether that function's parameter window is
|
|
160
|
+
# visible. The def widget reads/writes this bool DIRECTLY each
|
|
161
|
+
# render (visibility IS this bool); persisted, so a fresh session
|
|
162
|
+
# re-opens the panels that were open.
|
|
163
|
+
self.params_windows_open = {}
|
|
164
|
+
# {def_name: True} - whether that function's param panel re-runs
|
|
165
|
+
# Run Visualize automatically on every param edit. The panel's
|
|
166
|
+
# Auto Execute checkbox reads/writes this directly (the
|
|
167
|
+
# params_windows_open pattern); persisted with the editor state.
|
|
168
|
+
self.params_auto_execute = {}
|
|
169
|
+
# Width the Ctrl+B usage picker was drag-resized to, or None while
|
|
170
|
+
# it still fits its content; its height always fits the rows
|
|
171
|
+
# (usage_picker.py).
|
|
172
|
+
self.usage_picker_width = None
|
|
173
|
+
# Viewability snapshot for the instant-restore placeholder: what this
|
|
174
|
+
# editor SHOWED last frame - the visible band's first (display)
|
|
175
|
+
# line, its text, and the buffer's total line count. draw_text
|
|
176
|
+
# writes these at the end of every real draw and, when called with
|
|
177
|
+
# input_value=None (buffer still loading, e.g. draw_text_editor's
|
|
178
|
+
# loading branch), rebuilds a same-shape stand-in from them: blank
|
|
179
|
+
# lines up to the band, the band's text, blank lines below - same
|
|
180
|
+
# line count → same content height → the persisted scroll stays
|
|
181
|
+
# unmoved, and the first frame of a session shows the code the last
|
|
182
|
+
# one did. @exclude: pure passive state (all renders stamp
|
|
183
|
+
# them mid-session), so their per-scroll writes must not invalidate.
|
|
184
|
+
self.restore_first_line = 0
|
|
185
|
+
self.restore_total_lines = 0
|
|
186
|
+
self.restore_text = ""
|
|
187
|
+
# Gutter shape for the stand-in: digit count (0 = no gutter - the
|
|
188
|
+
# width decides where the code column starts, so a missing gutter
|
|
189
|
+
# made the text shift left↔right on the swap-in) and the sequential
|
|
190
|
+
# numbering offset (the number on buffer line 0 minus 1).
|
|
191
|
+
self.restore_gutter_digits = 0
|
|
192
|
+
self.restore_line_offset = 0
|
|
193
|
+
# Per-display-row gutter of the snapshot band, exactly as painted -
|
|
194
|
+
# ints are line NUMBERS (fold-remapped, so they skip across
|
|
195
|
+
# collapsed folds), -1/-2 denote a fold-header chevron row
|
|
196
|
+
# (expanded/collapsed), None a numberless row. Without this the
|
|
197
|
+
# stand-in numbered rows sequentially from restore_line_offset -
|
|
198
|
+
# wrong below every collapsed fold, and chevron-less - so the
|
|
199
|
+
# buffer visibly snapped when the real text landed.
|
|
200
|
+
self.restore_gutter_rows = None
|
|
201
|
+
# This editor's collapsed folds as their LINE-INDEPENDENT keys
|
|
202
|
+
# (ds._fold_keys - scope qualnames / header texts, designed to
|
|
203
|
+
# survive folds). ds._fold_keys is an underscore slot and not
|
|
204
|
+
# serialized, so fold state used to RESET to default_collapsed
|
|
205
|
+
# every session. draw_text seeds the fresh draw seed from this
|
|
206
|
+
# instead. None = never captured (let defaults seed); [] = captured
|
|
207
|
+
# with everything expanded (defaults must NOT re-collapse).
|
|
208
|
+
self.restore_fold_keys = None
|
|
209
|
+
# The compare split's hand-toggled DIFF gap state (ds
|
|
210
|
+
# ._diff_fold_collapsed - (header, last hidden line) buffer-line
|
|
211
|
+
# tuples; no line-independent keys exist for gaps; they re-derive
|
|
212
|
+
# from the live diff). Written by the snapshot block whenever the
|
|
213
|
+
# diff layer is active; a fresh draw_state in NEUTRAL mode
|
|
214
|
+
# (expand_diff None - the active switch is its own truth) maps
|
|
215
|
+
# them onto the current pieces by overlap, exactly like an edit's
|
|
216
|
+
# drift. None = never captured.
|
|
217
|
+
self.restore_diff_collapsed = None
|
|
218
|
+
# The band's diff-gap header rows, as painted: {band row offset:
|
|
219
|
+
# hidden line count} - 0 for an expanded gap's header, N for a
|
|
220
|
+
# collapsed one (its "N lines" label). The diff layer sits stand-in
|
|
221
|
+
# frames out (it needs the real buffer), so without this the
|
|
222
|
+
# stand-in painted a collapsed compare split as CONTINUOUS code -
|
|
223
|
+
# grey mid-row chevrons, no separator bands, no counts, no preview
|
|
224
|
+
# fade - and the collapsed look only arrived with the real text,
|
|
225
|
+
# seconds after boot (Lukas 09-01: "loads uncollapsed then 2
|
|
226
|
+
# seconds later makes the switch"). The stand-in paints its bands
|
|
227
|
+
# and labels from these rows instead. None = never captured.
|
|
228
|
+
self.restore_diff_rows = None
|
|
229
|
+
# The row offsets that were preview-faded (the rows around a
|
|
230
|
+
# collapsed diff gap, Toggles.TextEditor.diff_preview_lines_*), so
|
|
231
|
+
# the stand-in fades the same rows. None = never captured.
|
|
232
|
+
self.restore_preview_rows = None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@no_save_exclude("selected", "open_path", "cursor_path", "search_query", "search", "_focus_search",)
|
|
236
|
+
class DropDownState(DictConversion):
|
|
237
|
+
def __init__(self):
|
|
238
|
+
super().__init__()
|
|
239
|
+
self.selected = None
|
|
240
|
+
# The single chain of branch labels currently expanded, e.g. ("color",
|
|
241
|
+
# "rgb"). One path only - guarantees at most one sub-menu open per level.
|
|
242
|
+
self.open_path = ()
|
|
243
|
+
# Full key-path of the keyboard/hover-highlighted row (includes the leaf),
|
|
244
|
+
# e.g. ("color", "rgb", "red"). Drives arrow-key navigation + the
|
|
245
|
+
# highlight; open_path is derived from it (branch -> itself, leaf -> parent).
|
|
246
|
+
self.cursor_path = ()
|
|
247
|
+
# Live search box state (our own, not the framework search). search_query
|
|
248
|
+
# is the raw text the root menu's draw_text edits; search is its lowercased
|
|
249
|
+
# form, propagated to each level for filtering. _focus_search asks the box
|
|
250
|
+
# to grab text focus once (set when the menu opens); _search_box_tile is
|
|
251
|
+
# the box's tile id so key-nav can tell when the box holds focus.
|
|
252
|
+
self.search_query = ""
|
|
253
|
+
self.search = ""
|
|
254
|
+
self._focus_search = 0
|
|
255
|
+
self._search_box_tile = None
|
|
256
|
+
# True once the search box has held text focus this open. Lets us tell a
|
|
257
|
+
# brand-new open (still acquiring focus) from focus genuinely leaving the
|
|
258
|
+
# dropdown; the latter closes us, keeping text focus and open in lock-step.
|
|
259
|
+
self._had_focus = False
|
|
260
|
+
# The committed selection's key-path + text label (the trigger shows the
|
|
261
|
+
# label, e.g. "red", not the raw value). _picked_path is stamped by the row
|
|
262
|
+
# click / Enter so the path survives the close that resets cursor_path.
|
|
263
|
+
self.selected_path = ()
|
|
264
|
+
self.selected_label = ""
|
|
265
|
+
self._picked_path = ()
|
|
266
|
+
# Keyboard-select mode: once an arrow key is pressed, we keep moving the
|
|
267
|
+
# highlight until the mouse moves. _last_mouse detects that movement.
|
|
268
|
+
self._kbd_mode = False
|
|
269
|
+
self._last_mouse = None
|
|
270
|
+
# The popover's (width, height) when the user drag-resized it -
|
|
271
|
+
# persisted, re-applied on every later open; None = size to content
|
|
272
|
+
# (draw_dropdown's fit). _menu_ds is the popover's draw_state and
|
|
273
|
+
# _menu_fit the last size draw_dropdown stamped on it: a size that
|
|
274
|
+
# differs from _menu_fit is the wrapper's resize handle at work.
|
|
275
|
+
self.menu_size = None
|
|
276
|
+
self._menu_ds = None
|
|
277
|
+
self._menu_fit = None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ContextMenuItemsState(DropDownState):
|
|
281
|
+
"""The `context_menu={label: callable}` popover's dropdown state — the
|
|
282
|
+
root_state of the draw_dd_menu it opens — one per view carrying a menu,
|
|
283
|
+
kept in that view's draw_state.misc like an injected state
|
|
284
|
+
(new_core_view.draw_context_menu_items). Adds where the menu opened."""
|
|
285
|
+
|
|
286
|
+
def __init__(self):
|
|
287
|
+
super().__init__()
|
|
288
|
+
# The right-click's position relative to the view's top-left: the
|
|
289
|
+
# popover's window_pos, re-applied every frame so it stays put.
|
|
290
|
+
self.open_at = (0, 0)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@no_save("open_title", "_prev_text_focus", "_hovered_title")
|
|
294
|
+
class MenuBarState(DictConversion):
|
|
295
|
+
"""draw_menu_bar's injected state (view/core_views/menu_bar.py): which
|
|
296
|
+
title's menu is showing and one DropDownState per title, so every menu
|
|
297
|
+
keeps its own open/cursor paths and its drag-resized menu_size — the
|
|
298
|
+
same per-popover state draw_dropdown keeps, one per menu."""
|
|
299
|
+
|
|
300
|
+
def __init__(self):
|
|
301
|
+
super().__init__()
|
|
302
|
+
# The title (top-level key of the bar's dict) whose menu is open;
|
|
303
|
+
# None while the bar is idle. Not saved: an open menu means nothing
|
|
304
|
+
# next session.
|
|
305
|
+
self.open_title = None
|
|
306
|
+
# title -> DropDownState. menu_size saved per menu.
|
|
307
|
+
self.menus = {}
|
|
308
|
+
# The last selection's full key-path, ("File", "Recent", "a.py").
|
|
309
|
+
self.selected_path = ()
|
|
310
|
+
# Who held text focus when a menu opened, handed focus back on close
|
|
311
|
+
# so a click out of the Edit menu leaves the editor typing again.
|
|
312
|
+
self._prev_text_focus = None
|
|
313
|
+
# The title under the pointer at the last run (hover-switching edge-cases).
|
|
314
|
+
self._hovered_title = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@exclude("zoom", "center_u", "center_v", "brightness", "contrast", "hue", "saturation")
|
|
318
|
+
class ZoomState(DictConversion):
|
|
319
|
+
def __init__(self):
|
|
320
|
+
super().__init__()
|
|
321
|
+
self.zoom_level = 1.0
|
|
322
|
+
self.offset_x = 0.0
|
|
323
|
+
self.offset_y = 0.0
|
|
324
|
+
|
|
325
|
+
self.center_u = 0.5
|
|
326
|
+
self.center_v = 0.5
|
|
327
|
+
self.zoom = 1.0
|
|
328
|
+
self.brightness = 0.0
|
|
329
|
+
self.contrast = 1.0
|
|
330
|
+
self.hue = 0.0
|
|
331
|
+
self.saturation = 1.0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class AttrDict:
|
|
335
|
+
__slots__ = ('_data',)
|
|
336
|
+
|
|
337
|
+
def __init__(self, data):
|
|
338
|
+
object.__setattr__(self, '_data', data)
|
|
339
|
+
|
|
340
|
+
def __getattr__(self, name):
|
|
341
|
+
# `_data` itself must raise, not route through here: during deepcopy/
|
|
342
|
+
# pickle reconstruction the slot is briefly unset, and `self._data`
|
|
343
|
+
# would re-enter __getattr__('_data') -> infinite recursion. Dunders
|
|
344
|
+
# raise AttributeError so copy/pickle/unwrap protocol probes (__setstate__,
|
|
345
|
+
# __deepcopy__, ...) read as exceptions instead of a None that some
|
|
346
|
+
# protocols might try to call.
|
|
347
|
+
if name == "_data" or (name.startswith("__") and name.endswith("__")):
|
|
348
|
+
raise AttributeError(name)
|
|
349
|
+
return self._data.get(name, None)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def __setattr__(self, name, value):
|
|
353
|
+
# The `_data` slot is set directly; everything else is a data key.
|
|
354
|
+
# Without this, copy/pickle reconstruction (which setattrs `_data` back
|
|
355
|
+
# onto a fresh, slot-unset instance) would route into `self._data[...]`
|
|
356
|
+
# and hit the unset slot -> AttributeError. Pairs with __getattr__'s
|
|
357
|
+
# `_data` guard to make AttrDict round-trip through copy/pickle.
|
|
358
|
+
if name == "_data":
|
|
359
|
+
object.__setattr__(self, name, value)
|
|
360
|
+
return
|
|
361
|
+
self._data[name] = value
|
|
362
|
+
|
|
363
|
+
def rebind(self, data):
|
|
364
|
+
object.__setattr__(self, '_data', data)
|
|
365
|
+
|
|
366
|
+
@exclude("_items", "_cursor")
|
|
367
|
+
class CursorStack:
|
|
368
|
+
def __init__(self):
|
|
369
|
+
self._items = []
|
|
370
|
+
self._cursor = 0
|
|
371
|
+
|
|
372
|
+
def push(self, item):
|
|
373
|
+
# remove anything after cursor if you've popped, then append
|
|
374
|
+
self._items = self._items[:self._cursor]
|
|
375
|
+
self._items.append(item)
|
|
376
|
+
self._cursor += 1
|
|
377
|
+
|
|
378
|
+
def pop(self):
|
|
379
|
+
if self._cursor == 0:
|
|
380
|
+
raise IndexError("pop from empty stack")
|
|
381
|
+
self._cursor -= 1
|
|
382
|
+
return self._items[self._cursor]
|
|
383
|
+
|
|
384
|
+
def peek(self):
|
|
385
|
+
if self._cursor == 0:
|
|
386
|
+
raise IndexError("peek at empty stack")
|
|
387
|
+
return self._items[self._cursor - 1]
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def history(self):
|
|
391
|
+
return list(self._items)
|
|
392
|
+
|
|
393
|
+
def __repr__(self):
|
|
394
|
+
return f"{self._items} cursor={self._cursor}"
|
|
395
|
+
|
|
396
|
+
class TileMode(Enum):
|
|
397
|
+
MAX = 'max'
|
|
398
|
+
MIN = 'min'
|
|
399
|
+
NONE = 'none'
|
|
400
|
+
NO_MASK = 'no_mask'
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# Search state persists across reloads: search_text, search_active, and
|
|
404
|
+
# text_search_current are saved so the find box reopens with the last query and
|
|
405
|
+
# selected match. text_search_count is derived (recomputed each frame) and stays
|
|
406
|
+
# transient below.
|
|
407
|
+
@no_save("mouse_btn_state", "mouse_up", "mouse_down", "unique",
|
|
408
|
+
"shadow", "size_change", "drag_released", "clicked", "dragged", "clipped_by_rect",
|
|
409
|
+
"dragged", "expanded_height", "clipped", "fully_clipped", "inside_clip",
|
|
410
|
+
"overhead_time", "scroll_visible", "imgui_is_toggled_open", "top", "left",
|
|
411
|
+
"hotkey_receiver", "use_child", "cst", "bg_color", "depth", "return_item",
|
|
412
|
+
"is_active", "clip_rect", "wrapped_top", "current_tint", "wrapped_left", "multi_line", "relative_pos", "footer_width", "footer_height",
|
|
413
|
+
"min_width", "min_height", "max_height", "is_focused", "drag_window_pos_x", "drag_window_pos_y",
|
|
414
|
+
"drag_mode", "is_hovered_last", "bg_shown", "draw_window_pos_x", "z_offset", "melty_window",
|
|
415
|
+
"misc_used", "draw_window_pos_y", "drag_delta", "screen_pos", "hover_rects", "melty_window", "auto_resize",
|
|
416
|
+
"imgui_is_item_activated", "frame_count", "text_search_count",
|
|
417
|
+
# A context menu open at quit came back open at the next boot,
|
|
418
|
+
# drawn under the view that spawned it (09-13, melty_code_editor).
|
|
419
|
+
"context_menu_open")
|
|
420
|
+
@exclude("current_tint", "overhead_time", "premature_break", "drag_mode",
|
|
421
|
+
"clip_rect", "_input_value", "flow_spacing", "expanded_rect", 'max_column', 'text_selection_start', 'text_selection_end',
|
|
422
|
+
'width', "size_change", 'left', 'top', "clipped", "fully_clipped", "melty_window", "text_double_click_time", "text_cursor_blink_time",
|
|
423
|
+
"hovered", "wrapped_top", "params", "scroll_visible", "depth_and_layer", "clipped_by_rect", "multi_line", "text_prev_cursor_pos",
|
|
424
|
+
"text_cursor_pos", "text_h_scroll", "text_selection_start", "text_selection_end", "text_is_focused", "text_cursor_blink_time",
|
|
425
|
+
"premature_break", "wrapped_left", "_did_use_cache", "hover_rects", "window_pos", "content_width", "code_state",
|
|
426
|
+
"content_region", "value_hash", "drag_window", "content_region", "did_render", "footer_height", "footer_width",
|
|
427
|
+
"bounding_hovered", "dlt_count", "clip_rect",
|
|
428
|
+
"scrolled", "is_hovered_last", "frame_count", "z_pos",
|
|
429
|
+
"text_search_current", "text_search_count", "observed_content_height",
|
|
430
|
+
# The context menu's Eval tab: typed per keystroke and must not
|
|
431
|
+
# re-render the inspected view (the eval fires on Run / Enter).
|
|
432
|
+
"eval_code")
|
|
433
|
+
@no_save_exclude('_window_visibility_initialized', 'render_time', "total_z_offset", 'closable', 'has_full_tile', 'invalid_content_height',
|
|
434
|
+
"parent_window", "pressed", "bbox", "", "child_selected", "bg_color",
|
|
435
|
+
'hover_rects', 'nested_window', 'use_cache', "header_top", "header_left", "left_offset",
|
|
436
|
+
"top_offset", 'kwargs', "just_shadow",
|
|
437
|
+
"header_natural_width", "max_header_width", "pin_to_clip", "pin_clip_rect", "pin_clamp",
|
|
438
|
+
"header_left_delta", "header_top_delta", "last_seen", "persistent", "shadow_margin", "bg_depth",
|
|
439
|
+
"anchor_pos", "parent_anchor_pos", "pin_to_clip", "pin_clip_rect", "just_shadow", 'hover_reported', 'explain_convert',
|
|
440
|
+
'channel', 'next', 'previous', 'index_in_parent',
|
|
441
|
+
'_hover_eligible', 'just_shadow')
|
|
442
|
+
@deep_refresh( 'closed', 'search_text', 'search_active')
|
|
443
|
+
class DrawState(DictConversion):
|
|
444
|
+
"""Holds per-widget runtime state (expand/collapse, etc.)."""
|
|
445
|
+
|
|
446
|
+
# _ancestor_scroll cache, keyed on (frame_count, Melty.scroll_version).
|
|
447
|
+
# Class-level defaults so pre-existing/deserialized instances resolve them
|
|
448
|
+
# without __init__; written via object.__setattr__ (never accessed or
|
|
449
|
+
# serialized; to_dict only walks the default instance's fields).
|
|
450
|
+
_anc_scroll_key = None
|
|
451
|
+
_anc_scroll_cache = (0, 0)
|
|
452
|
+
|
|
453
|
+
# Hot reload fallback: live instances predating the persisted rename read
|
|
454
|
+
# this, the wrapper's post-body stamp writes the instance attr.
|
|
455
|
+
observed_content_height = 0
|
|
456
|
+
# Same fallback for the persisted Eval snippet (context menu's Eval-tab).
|
|
457
|
+
eval_code = None
|
|
458
|
+
|
|
459
|
+
def __init__(self):
|
|
460
|
+
super().__init__()
|
|
461
|
+
self._external_change = False
|
|
462
|
+
# The context menu's Eval-tab snippet for THIS view, persisted so the
|
|
463
|
+
# tab reopens with what was last typed. Stored on the inspected view's
|
|
464
|
+
# draw_state (not the tab's) because the eval fires in this view's
|
|
465
|
+
# render wrapper and a scope-up event retargets the tab at an ancestor.
|
|
466
|
+
self.eval_code = None
|
|
467
|
+
self._input_value_cache = UNSET_VALUE
|
|
468
|
+
self._output_value_cache = UNSET_VALUE
|
|
469
|
+
self._file_meta = UNSET_VALUE
|
|
470
|
+
self._loading = False
|
|
471
|
+
self._pending = False
|
|
472
|
+
self._running = False
|
|
473
|
+
self.watch = ""
|
|
474
|
+
self._default_view_func = None
|
|
475
|
+
|
|
476
|
+
# Chain cache, split based on type, UNSET_VALUE as a default
|
|
477
|
+
self._chain_stack = CacheTree()
|
|
478
|
+
self._cursor_start_pos = None
|
|
479
|
+
|
|
480
|
+
self._children = {}
|
|
481
|
+
self._view_children = {}
|
|
482
|
+
self._wrapper = None
|
|
483
|
+
self._parent_ctx = None
|
|
484
|
+
self._bg_depth = 0
|
|
485
|
+
self._current_tint = None
|
|
486
|
+
self._bg_stack = None
|
|
487
|
+
self._parent = self
|
|
488
|
+
self._is_header = False
|
|
489
|
+
self._view_func = None
|
|
490
|
+
self._cursor_pos = (0, 0)
|
|
491
|
+
self._cursor_screen_pos = (0, 0)
|
|
492
|
+
self._source = defaultdict(dict)
|
|
493
|
+
self.next = None
|
|
494
|
+
self.previous = None
|
|
495
|
+
self.index_in_parent = 0
|
|
496
|
+
self.misc = {}
|
|
497
|
+
self._clean_args = {}
|
|
498
|
+
self._func = None
|
|
499
|
+
self.misc_used = set()
|
|
500
|
+
self.closed = False
|
|
501
|
+
self._window_visibility_initialized = False
|
|
502
|
+
self.frame_count = 0
|
|
503
|
+
# corner_radius is no longer a declared field - it migrated to the
|
|
504
|
+
# auto state system (see core_render's auto-state block): views that
|
|
505
|
+
# tune it declare a named `corner_radius` param (e.g. button), the
|
|
506
|
+
# default bg path stamps it from there, and background painters read
|
|
507
|
+
# it via getattr(ds, 'corner_radius', 6).
|
|
508
|
+
self.nested_window = False
|
|
509
|
+
self.use_cache = False
|
|
510
|
+
self.depth = 0
|
|
511
|
+
self.layer = 0
|
|
512
|
+
self.channel = 0
|
|
513
|
+
self.last_seen = None
|
|
514
|
+
self.z_offset = 0
|
|
515
|
+
|
|
516
|
+
self.total_z_offset = 0
|
|
517
|
+
self.shadow_margin = 0
|
|
518
|
+
self.bg_depth = 0
|
|
519
|
+
self.bg_color = (0, 0, 0, 0)
|
|
520
|
+
self.pressed = False
|
|
521
|
+
self.closable = False
|
|
522
|
+
self.has_full_tile = False
|
|
523
|
+
self.behind = False
|
|
524
|
+
self.tile_mode = TileMode.MAX
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
self.context_menu_open = False
|
|
528
|
+
self.context_menu_ds = None
|
|
529
|
+
self.context_menu_offset = 0
|
|
530
|
+
self._offset_ds = None
|
|
531
|
+
# Set on a context menu's target draw_state to reopen it after a deferred
|
|
532
|
+
# per-view screenshot lands (see screenshot.process_take_screenshot_flags).
|
|
533
|
+
self._reopen = False
|
|
534
|
+
# Resolved call site (filename, line) of where this widget was invoked,
|
|
535
|
+
# computed ONCE in the inline render pass of first frame its context menu
|
|
536
|
+
# is opened (see core_render) and cached until app restart. We cache the
|
|
537
|
+
# resolved tuple - NOT the raw frames - so the inspect-arg lens / jump
|
|
538
|
+
# button read a stable value; re-walking the live stack each frame would
|
|
539
|
+
# flip mid-drag (the "skip parents" optimization changes the call stack).
|
|
540
|
+
# Underscore-prefixed → not serialized.
|
|
541
|
+
self._call_site = None
|
|
542
|
+
# The whole UNfiltered call stack (innermost-first list of (filename,
|
|
543
|
+
# lineno, func_name)), captured alongside _call_site. draw_context_menu
|
|
544
|
+
# renders every frame, filtering per-frame: a real call site shows an
|
|
545
|
+
# editable code_file_io, a machinery/ignored frame just a label.
|
|
546
|
+
self._call_stack = []
|
|
547
|
+
# Queue-time call stack for a DEFERRED layer (a view drawn at end of frame).
|
|
548
|
+
# At end-of-frame dispatch the live stack is just the layer-loop machinery,
|
|
549
|
+
# so a view rendered inside a deferred layer has a _call_stack that bottoms
|
|
550
|
+
# out there. The original caller chain that QUEUED the layer is captured
|
|
551
|
+
# preemptively at queue-time (core_render, deferred pass) into this field;
|
|
552
|
+
# draw_context_menu appends it to complete a descendant's stack.
|
|
553
|
+
# _is_deferred_layer marks a queued view; _deferred_stack_requested is the
|
|
554
|
+
# lazy capture gate, set by a descendant's open menu and consumed next frame.
|
|
555
|
+
self._deferred_call_stack = []
|
|
556
|
+
# The same queue-time stack with LOCALS - (path, lineno, func_name,
|
|
557
|
+
# locals) outermost first, the debug tab's format (_call_stack_frames)
|
|
558
|
+
# - so a descendant's stack trace can be spliced onto the chain that
|
|
559
|
+
# queued this layer and rendered as one logical call. None until captured.
|
|
560
|
+
self._deferred_call_stack_frames = None
|
|
561
|
+
self._is_deferred_layer = False
|
|
562
|
+
self._deferred_stack_requested = False
|
|
563
|
+
self._call_site_captured = False
|
|
564
|
+
# Set by the context menu when it walks UP to this draw state via the
|
|
565
|
+
# up-arrow offset; this view never had its own menu open, so the normal
|
|
566
|
+
# context_menu_open capture gate never fired. The flag causes the next
|
|
567
|
+
# inline render will capture the call site anyway (lazy, one-shot).
|
|
568
|
+
self._call_site_requested = False
|
|
569
|
+
# Per-frame caches for the position/clip chain. abs_left/abs_top/abs_clip_rect/
|
|
570
|
+
# pin_rect/clip_anchor_base are pure functions of the draw_state tree but were
|
|
571
|
+
# recomputed many times per frame (the left property is hit by parent walks,
|
|
572
|
+
# pin clamping, clip checks, and BVH lookups), with each computation re-walking
|
|
573
|
+
# the parent chain. With pin_clamp the recursion compounded - ~600K _abs_left
|
|
574
|
+
# calls per ~25 draw_tint_context frames in practice. Cache by frame_count;
|
|
575
|
+
# the recursive internal walks all go through the cached property so an
|
|
576
|
+
# ancestor's value is computed at most once per frame.
|
|
577
|
+
# abs_left/abs_top are keyed by (frame_count, left_offset, top_offset,
|
|
578
|
+
# window_pos) - NOT frame alone. The wrapper sets left_offset/top_offset
|
|
579
|
+
# mid-frame (core_render.py:393, again in the columns branch :1235) to
|
|
580
|
+
# position the widget at the current imgui cursor; a frame-only key
|
|
581
|
+
# cached the first set_offset value and the column re-set never landed,
|
|
582
|
+
# which is what broke column rendering with the earlier cache.
|
|
583
|
+
self._abs_left_key = None
|
|
584
|
+
self._abs_left_cache = 0
|
|
585
|
+
self._abs_top_key = None
|
|
586
|
+
self._abs_top_true_key = None
|
|
587
|
+
|
|
588
|
+
self._abs_top_cache = 0
|
|
589
|
+
self._abs_top_true_cache = 0
|
|
590
|
+
self._abs_content_height_cache = 0
|
|
591
|
+
self._abs_content_height_key = None
|
|
592
|
+
# pin_rect/clip_anchor_base keys widen over frame_count too: the wrapper
|
|
593
|
+
# writes pin_target/pin_clip_rect/pin_clamp/parent_anchor_pos mid-frame
|
|
594
|
+
# (core_render.py:995–1041), AFTER earlier hover/clip code may have already
|
|
595
|
+
# cached a pre-anchor value. Frame-only caching made nested-window anchors
|
|
596
|
+
# take a frame to settle.
|
|
597
|
+
self._pin_rect_key = None
|
|
598
|
+
self._pin_rect_cache = None
|
|
599
|
+
self._clip_anchor_base_key = None
|
|
600
|
+
self._clip_anchor_base_cache = None
|
|
601
|
+
|
|
602
|
+
self.relative_pos = None
|
|
603
|
+
|
|
604
|
+
self._first_draw_state = None
|
|
605
|
+
self.melty_window = False
|
|
606
|
+
self.persistent = True
|
|
607
|
+
self.child_selected = None
|
|
608
|
+
self.just_shadow = False
|
|
609
|
+
self._event_names = set()
|
|
610
|
+
# Body-level on_action registrations from the last real render, replayed
|
|
611
|
+
# by the wrapper for blit-cache hits: (frame_count, [entries]). See
|
|
612
|
+
# on_action / replay_body_actions.
|
|
613
|
+
self._body_actions = None
|
|
614
|
+
# Rect-scoped event PARAMS from the last real render (event_rect):
|
|
615
|
+
# {param_name: [rect relative to (abs_left, abs_top), ...]}. The
|
|
616
|
+
# wrapper consumes and clears it before each body run.
|
|
617
|
+
self._event_rects = None
|
|
618
|
+
|
|
619
|
+
self._queued_windows = []
|
|
620
|
+
self.drag_window_pos_x = None
|
|
621
|
+
self.drag_window_pos_y = None
|
|
622
|
+
self._bounding_hovered = False
|
|
623
|
+
# Imgui state mirror
|
|
624
|
+
self.is_active = False
|
|
625
|
+
self.is_focused = False
|
|
626
|
+
self.scroll_offset = (0, 0)
|
|
627
|
+
# Authoritative max scroll_offset.y, published by core_render's scroll
|
|
628
|
+
# handler each frame. Descendants (text editor drag-auto-scroll) read it
|
|
629
|
+
# so their clamp matches exactly & the two don't fight (bottom flicker).
|
|
630
|
+
self._max_scroll_y = None
|
|
631
|
+
self._imgui_is_active = False
|
|
632
|
+
self._imgui_is_activated = False
|
|
633
|
+
self._imgui_is_focused = False
|
|
634
|
+
self._imgui_is_hovered = False
|
|
635
|
+
self._imgui_is_item_hovered = False
|
|
636
|
+
|
|
637
|
+
self._imgui_block_hovered = False
|
|
638
|
+
self._imgui_is_edited = False
|
|
639
|
+
self._imgui_popover_open = False
|
|
640
|
+
self.imgui_is_item_activated = False
|
|
641
|
+
self.inside_clip = True
|
|
642
|
+
self.fully_clipped = True
|
|
643
|
+
|
|
644
|
+
self._one_full_draw = False
|
|
645
|
+
self.scroll_visible = False
|
|
646
|
+
self._unmanaged_window = False
|
|
647
|
+
self.live = False
|
|
648
|
+
self._shadow_depth = 0
|
|
649
|
+
self.shadow = False
|
|
650
|
+
self.cst = None
|
|
651
|
+
self.window_pos = (0,0)
|
|
652
|
+
self.window_size = None
|
|
653
|
+
self._initial_window_pos = None
|
|
654
|
+
self._initial_window_pos_resize = None
|
|
655
|
+
self._initial_window_size = (300, 300)
|
|
656
|
+
|
|
657
|
+
self.drag_mode = DragMode.NONE
|
|
658
|
+
self.use_child = False
|
|
659
|
+
self.z_pos = 0
|
|
660
|
+
self.depth_and_layer = (0, 0)
|
|
661
|
+
self.content_height = 0
|
|
662
|
+
self.invalid_content_height = True
|
|
663
|
+
# True while this view is holding a placeholder height from the
|
|
664
|
+
# core_render.content_height fast path (never fully rendered yet). Cleared
|
|
665
|
+
# the frame it renders for real.
|
|
666
|
+
self._skipped_render = False
|
|
667
|
+
self.auto_resize = True
|
|
668
|
+
self._tile_id = None
|
|
669
|
+
self.imgui_is_toggled_open = False
|
|
670
|
+
|
|
671
|
+
self._previous_hash = None
|
|
672
|
+
self.tint = (0.11, 0.12, 0.14)
|
|
673
|
+
self.current_tint = None
|
|
674
|
+
|
|
675
|
+
self.unique = None # stable UI ID
|
|
676
|
+
self.expanded = True
|
|
677
|
+
self.name = ""
|
|
678
|
+
self.height = 0
|
|
679
|
+
self.expanded_height = None
|
|
680
|
+
self.width = 0
|
|
681
|
+
self.min_width = 0
|
|
682
|
+
self.min_height = 0
|
|
683
|
+
# The wrapper's max_height kwarg when the caller passes
|
|
684
|
+
# enforce_max_height=True, mirrored like min_height so the resize
|
|
685
|
+
# handlers (corner drag, the frame-edge solve) cap a closable window's
|
|
686
|
+
# height in realtime; None = uncapped (a bare max_height only bounds
|
|
687
|
+
# what the wrapper sizes - the resize handle stays free from it).
|
|
688
|
+
self.max_height = None
|
|
689
|
+
self.drag_window = False
|
|
690
|
+
self._left_rel = None
|
|
691
|
+
self._top_rel = None
|
|
692
|
+
self._min_width = None
|
|
693
|
+
self.top = None
|
|
694
|
+
self.left = None
|
|
695
|
+
self.left_offset = 0
|
|
696
|
+
self.top_offset = 0
|
|
697
|
+
|
|
698
|
+
self.left_offset_true = 0
|
|
699
|
+
self.top_offset_true = 0
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
self._draggable = False
|
|
703
|
+
self.search_text = ""
|
|
704
|
+
self.search_active = False
|
|
705
|
+
self._search_was_active = False
|
|
706
|
+
self._flow_spacing = 0.0
|
|
707
|
+
self.enabled = True
|
|
708
|
+
self._end_header_size = (0, 0)
|
|
709
|
+
self._header_height = 0
|
|
710
|
+
self._max_indent = 0
|
|
711
|
+
self._name_edit = False
|
|
712
|
+
self._screen_pos = (0, 0)
|
|
713
|
+
self._did_use_cache = False
|
|
714
|
+
self.track_mouse = False
|
|
715
|
+
self._scroll_child = None
|
|
716
|
+
self._collection_draw_state = None
|
|
717
|
+
self._mouse_up = False
|
|
718
|
+
self.mouse_down = False
|
|
719
|
+
self.drag_released = False
|
|
720
|
+
self._hovered = False
|
|
721
|
+
self.hotkey_receiver = False
|
|
722
|
+
self._scrolled = False
|
|
723
|
+
self._clicked = False
|
|
724
|
+
self.dragged = False
|
|
725
|
+
self._screen_pos = (0, 0)
|
|
726
|
+
self.drag_delta = (0, 0)
|
|
727
|
+
self._input_value = UNSET_VALUE
|
|
728
|
+
self._raw_input_value = UNSET_VALUE
|
|
729
|
+
self._bypass_cache = False
|
|
730
|
+
|
|
731
|
+
self._input_cache = {"external_state": (UNSET_VALUE, 0, -1), # value, frame, hash
|
|
732
|
+
"internal_state":(UNSET_VALUE, 0)} # value, frame
|
|
733
|
+
|
|
734
|
+
self._collection = None
|
|
735
|
+
self._has_popup = False
|
|
736
|
+
self.is_hovered_last = False
|
|
737
|
+
self.parent_window = None
|
|
738
|
+
self.result = None
|
|
739
|
+
self.params = {}
|
|
740
|
+
self.wrapped_top = 0
|
|
741
|
+
self.wrapped_left = 0
|
|
742
|
+
self.header_height = 0
|
|
743
|
+
self.header_top = 0
|
|
744
|
+
self.header_left = 0
|
|
745
|
+
self.header_width = 0
|
|
746
|
+
self.header_end_width = 0
|
|
747
|
+
# Natural (pre-pad) width of this window's own header.
|
|
748
|
+
self.header_natural_width = 0
|
|
749
|
+
# Multi-window header widths: max_header_width is the stable value
|
|
750
|
+
# read while padding headers this frame. _max_header_width_acc is the
|
|
751
|
+
# running max accumulated as children render, committed at window push.
|
|
752
|
+
self.max_header_width = 0
|
|
753
|
+
self.clip_rect = None
|
|
754
|
+
# Parent window's abs_pos when clip_rect was captured. abs_clip_rect
|
|
755
|
+
# shifts the (absolute, not-based) clip_rect by the window's movement
|
|
756
|
+
# since then, so the clip tracks a window drag without a re-render.
|
|
757
|
+
self._clip_win_anchor = None
|
|
758
|
+
self.dlt_count = Core.melty.save_draw_state_for
|
|
759
|
+
self.premature_break = False
|
|
760
|
+
self.header_left_delta = 0
|
|
761
|
+
self.header_top_delta = 0
|
|
762
|
+
self.multi_line = False
|
|
763
|
+
self.footer_width = 0
|
|
764
|
+
self.footer_height = 0
|
|
765
|
+
|
|
766
|
+
self.content_width = 0
|
|
767
|
+
self.content_height = 0
|
|
768
|
+
|
|
769
|
+
# Profiling
|
|
770
|
+
self.render_time = 0.0
|
|
771
|
+
self.overhead_time = 0.0
|
|
772
|
+
|
|
773
|
+
### Columns
|
|
774
|
+
self.final_max_column = 0
|
|
775
|
+
self._current_max_column = 0
|
|
776
|
+
# Per-divider pixel nudges applied left-to-right on top of the basic
|
|
777
|
+
# content_width/#columns split. offsets[c-1] shifts interior boundary
|
|
778
|
+
# c; values are never clamped (see column_boundary in core_render).
|
|
779
|
+
self.column_offsets = []
|
|
780
|
+
self._column_cursor = defaultdict(lambda: [0, 0])
|
|
781
|
+
self._melty_cursor = (0,0)
|
|
782
|
+
self._melty_content_height = 0
|
|
783
|
+
|
|
784
|
+
self._outside_column_height = 0
|
|
785
|
+
self._inner_cursor = 0 # column -> (x, y)
|
|
786
|
+
self._columns_top = None
|
|
787
|
+
self._max_column_height = 0
|
|
788
|
+
self._max_column_index = 0
|
|
789
|
+
self._columns_bottom = 0
|
|
790
|
+
### End Columns
|
|
791
|
+
self._is_nested = False
|
|
792
|
+
self.anchor_pos = Anchor.TOP_LEFT
|
|
793
|
+
# Which point on the parent the child anchors to. Independent of
|
|
794
|
+
# anchor_pos (the child's own origin). Defaults to TOP_LEFT so parent
|
|
795
|
+
# anchoring is opt-in and legacy top-left layout is preserved.
|
|
796
|
+
self.parent_anchor_pos = Anchor.TOP_LEFT
|
|
797
|
+
# Floating views pin to a clip - resolved live from this Pin enum
|
|
798
|
+
# (PARENT / GRANDPARENT / WINDOW / CLIP) off .parent/parent_window at
|
|
799
|
+
# compute time, so the float tracks the target as it scrolls/moves
|
|
800
|
+
# without snapshotting anything. None = not pinned. Truthy when pinned.
|
|
801
|
+
# See pin_rect / _pin_rect.
|
|
802
|
+
self.pin_to_clip = None
|
|
803
|
+
self._kwargs = {}
|
|
804
|
+
self.kwargs = AttrDict({})
|
|
805
|
+
# Auto-state params (see the auto-state block in core_render.py).
|
|
806
|
+
# auto_params holds only DIVERGED values - what view code wrote via
|
|
807
|
+
# draw_state.<param> = x - keyed by param name. They inject into the
|
|
808
|
+
# kwargs gauntlet above the default layers (defaults and caller-passed
|
|
809
|
+
# kwargs still win). Non-underscore on purpose: it serializes whenever
|
|
810
|
+
# non-empty, so diverged state persists across sessions while
|
|
811
|
+
# at-default params save nothing. _auto_baseline mirrors last frame's
|
|
812
|
+
# RESOLVED value per param (what the caller handed the view); the
|
|
813
|
+
# divergence scan compares the live attr against it to detect writes.
|
|
814
|
+
# The the draw_state.<param> attrs themselves are dynamic (not
|
|
815
|
+
# declared here), so to_dict never serializes them directly.
|
|
816
|
+
self.auto_params = {}
|
|
817
|
+
self._auto_baseline = {}
|
|
818
|
+
self.hover_reported = True
|
|
819
|
+
self._start_z_pos = 3
|
|
820
|
+
self._column_width = 0
|
|
821
|
+
self._front_layer = False
|
|
822
|
+
|
|
823
|
+
# Used to cache abs_clip_rect
|
|
824
|
+
self.clipped_by_rect = None
|
|
825
|
+
|
|
826
|
+
# Set by Melty's end_frame tree rebuild: True while this nested
|
|
827
|
+
# window's spawning view is currently scrolled/clipped out of sight, so
|
|
828
|
+
# the window is skipped (not closed) by the layer dispatch and its
|
|
829
|
+
# subtree filtered out of bvh_query hits. Read via getattr (live
|
|
830
|
+
# instances predating the field won't have it).
|
|
831
|
+
self._hidden_offscreen = False
|
|
832
|
+
|
|
833
|
+
self._show_load = False
|
|
834
|
+
self._show_save = False
|
|
835
|
+
self._read_only = False
|
|
836
|
+
self._internal_pending = None
|
|
837
|
+
self._apply_load = None
|
|
838
|
+
|
|
839
|
+
self._save_pending = None
|
|
840
|
+
self._apply_save = None
|
|
841
|
+
self._pending_convert = False
|
|
842
|
+
self._save_pending_for = 0
|
|
843
|
+
self._save_pending_obj = None
|
|
844
|
+
self._all_pending = {"save_pending":None, "load_pending":None}
|
|
845
|
+
self._load_pending_for = 0
|
|
846
|
+
self._content_rect = (100,30)
|
|
847
|
+
# PERSISTED (non-underscore, @exclude'd): the wrapper's post-body
|
|
848
|
+
# observation. Serialized so a restored view's abs_content_height -
|
|
849
|
+
# and with it needs_scroll and the persisted scroll state - is right
|
|
850
|
+
# on a session's FIRST frame, before the view (or its loading
|
|
851
|
+
# stand-in) has rendered once.
|
|
852
|
+
self.observed_content_height = 0
|
|
853
|
+
self._last_expanded = None
|
|
854
|
+
self.expanded_rect = (0, 0, 0, 0)
|
|
855
|
+
self._collapsed_rect = (0, 0, 0, 0)
|
|
856
|
+
self._load_pending = False
|
|
857
|
+
self._save_pending = None
|
|
858
|
+
|
|
859
|
+
self.explain_convert = None
|
|
860
|
+
|
|
861
|
+
# Text input state
|
|
862
|
+
self.text_cursor_pos = 0
|
|
863
|
+
self.text_selection_start = 0
|
|
864
|
+
self.text_selection_end = 0
|
|
865
|
+
self.text_is_focused = False
|
|
866
|
+
self.text_cursor_blink_time = 0.0
|
|
867
|
+
self.text_double_click_time = 0.0
|
|
868
|
+
self.text_last_click_pos = -1
|
|
869
|
+
self.text_click_count = 0
|
|
870
|
+
self.text_h_scroll = 0.0
|
|
871
|
+
self.text_prev_cursor_pos = 0
|
|
872
|
+
self._name_color = (1,1,1)
|
|
873
|
+
# Drag granularity latched on mouse-down ('char' | 'word' | 'line') plus
|
|
874
|
+
# the anchor pos of the anchor word/line, so a double/triple-click drag
|
|
875
|
+
# extends by whole words/lines and keeps the anchor selected (IntelliJ
|
|
876
|
+
# style) instead of collapsing to a single caret.
|
|
877
|
+
self.text_drag_mode = 'char'
|
|
878
|
+
self.text_drag_anchor_lo = 0
|
|
879
|
+
self.text_drag_anchor_hi = 0
|
|
880
|
+
|
|
881
|
+
# Find-in-text search state. text_search_count/current are populated by
|
|
882
|
+
# draw_text each frame and consumed by the header's find UI (count +
|
|
883
|
+
# nav arrows). The underscore fields are private bookkeeping.
|
|
884
|
+
self.text_search_current = 0
|
|
885
|
+
self.text_search_count = 0
|
|
886
|
+
self._text_search_last_term = None
|
|
887
|
+
self._text_search_scroll_to = False
|
|
888
|
+
|
|
889
|
+
# Cross-view aggregation (search owner side). _search_session is a
|
|
890
|
+
# SearchTerm pushed onto Melty.search_stack each frame; the owner's
|
|
891
|
+
# pre-body search_walk pushes its term here and writes the combined
|
|
892
|
+
# result count into text_search_count for the find UI.
|
|
893
|
+
self._search_session = None
|
|
894
|
+
self._search_last_term = None
|
|
895
|
+
self._search_nav_pending = False
|
|
896
|
+
# Temporary state: set when search is opened (Ctrl+F) so the find box grabs
|
|
897
|
+
# text focus on the activating frame even if the underlying searchable
|
|
898
|
+
# view (the owner) currently holds melty's text focus. Consumed (cleared)
|
|
899
|
+
# by render_search after the box requests focus, so it won't keep
|
|
900
|
+
# yanking focus away from a later deliberate click into the editor.
|
|
901
|
+
self._search_focus_pending = False
|
|
902
|
+
# Set by the owner's search_walk to the local index of the global-current
|
|
903
|
+
# match when it lands in THIS view (else None). The view reads it while
|
|
904
|
+
# drawing to highlight/scroll to that match - so the count and the
|
|
905
|
+
# selection come from one source and can't disagree.
|
|
906
|
+
self._search_active_local = None
|
|
907
|
+
# draw_collection's copy of the key holding the global-current match.
|
|
908
|
+
self._search_current_key = None
|
|
909
|
+
# Closure set each render: (term, session) -> claims this view's own
|
|
910
|
+
# matches into the session without imgui. Driven by meltygui.search_walk.
|
|
911
|
+
self._search_matcher = None
|
|
912
|
+
|
|
913
|
+
self._print_last_invalid = False
|
|
914
|
+
self._last_invalidate = None
|
|
915
|
+
self._hover_eligible = 0
|
|
916
|
+
|
|
917
|
+
# BVH spatial index
|
|
918
|
+
self._bvh_id = None
|
|
919
|
+
self._bvh_bbox = None # cached bbox for delete operations
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
# File watch state (replaces _WatchState for convert_in/convert_out)
|
|
923
|
+
self._address = None
|
|
924
|
+
self._file_mtime = 0.0
|
|
925
|
+
self._file_size = 0
|
|
926
|
+
self._original_load_data = None
|
|
927
|
+
self._original_input_ref = None
|
|
928
|
+
|
|
929
|
+
self._stack_trace = None
|
|
930
|
+
self._nested_index = 0
|
|
931
|
+
|
|
932
|
+
self.selected = False
|
|
933
|
+
self._cache = None
|
|
934
|
+
self._return_value = None
|
|
935
|
+
|
|
936
|
+
def invalidate_up(self, max_depth=4, frame_delta=0, note=None, force=False):
|
|
937
|
+
Core.melty.cache.invalidate_up(self._tile_id, frame_delta=frame_delta,
|
|
938
|
+
max_depth=max_depth, note=note, force=force)
|
|
939
|
+
|
|
940
|
+
def invalidate(self, frame_delta=0, note=None):
|
|
941
|
+
Core.melty.cache.invalidate(self._tile_id, frame_delta=frame_delta, note=note)
|
|
942
|
+
|
|
943
|
+
def invalidate_by_obj(self, obj=None, frame_delta=0, note=None):
|
|
944
|
+
Core.melty.cache.invalidate_by_obj(obj=obj, frame_delta=frame_delta, note=note)
|
|
945
|
+
|
|
946
|
+
def invalidate_up_by_obj(self, obj=None, max_depth=3, frame_delta=0, note=None):
|
|
947
|
+
Core.melty.cache.invalidate_up_by_obj(obj, max_depth=max_depth, frame_delta=frame_delta, note=note)
|
|
948
|
+
|
|
949
|
+
@property
|
|
950
|
+
def cursor_screen_pos(self):
|
|
951
|
+
cursor_x = self.abs_left + self._melty_cursor[0]
|
|
952
|
+
cursor_y = self.abs_top + self._melty_cursor[1]
|
|
953
|
+
return cursor_x, cursor_y
|
|
954
|
+
|
|
955
|
+
@property
|
|
956
|
+
def clip_size(self):
|
|
957
|
+
# if self.clip_rect is None:
|
|
958
|
+
# return (self.width or self.min_width, self.height or self.min_height)
|
|
959
|
+
left, top, right, bottom = self.abs_clip_rect
|
|
960
|
+
return (right - left, bottom - top)
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def bbox(self):
|
|
964
|
+
l, t, w, h = self.abs_left, self.abs_top, self.width, self.height
|
|
965
|
+
if l is None or t is None or not w or not h:
|
|
966
|
+
return None
|
|
967
|
+
return (l, t, l + w, t + h - 2)
|
|
968
|
+
|
|
969
|
+
def is_file_stale(self):
|
|
970
|
+
if self._address is None:
|
|
971
|
+
return False
|
|
972
|
+
try:
|
|
973
|
+
s = self._address.path.stat()
|
|
974
|
+
return s.st_mtime != self._file_mtime or s.st_size != self._file_size
|
|
975
|
+
except OSError:
|
|
976
|
+
return True
|
|
977
|
+
|
|
978
|
+
def mark_file_current(self):
|
|
979
|
+
if self._address is None:
|
|
980
|
+
return
|
|
981
|
+
try:
|
|
982
|
+
s = self._address.path.stat()
|
|
983
|
+
self._file_mtime = s.st_mtime
|
|
984
|
+
self._file_size = s.st_size
|
|
985
|
+
except OSError:
|
|
986
|
+
pass
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
# ─── locate_* : the set-anywhere accessors ────────────────────────────
|
|
990
|
+
# `locate_<param>` access any param of the view, read and write:
|
|
991
|
+
#
|
|
992
|
+
# tilt = draw_state.locate_tilt # framework-resolved value
|
|
993
|
+
# draw_state.locate_x_dim = 0 # -> the DRIVING source
|
|
994
|
+
#
|
|
995
|
+
# ASYMMETRY on purpose: reading is ONLY the resolved value (_kwargs, with
|
|
996
|
+
# the in-flight set cache and the ds fallback anywhere_value applies) -
|
|
997
|
+
# cheap enough per frame. Only the WRITE walks the source registry, picks
|
|
998
|
+
# the driving source, and drives its deferred save/hotswap.
|
|
999
|
+
#
|
|
1000
|
+
# Declared HERE, in the class, so they work on every draw_state from the
|
|
1001
|
+
# first frame - independent of whether anything's imported the view
|
|
1002
|
+
# module yet. The implementation stays in view/core_views/anywhere.py and
|
|
1003
|
+
# is imported lazily per access (this module is model-layer; a top-level
|
|
1004
|
+
# import of the view module would cycle) - a sys.modules hit after the
|
|
1005
|
+
# first call.
|
|
1006
|
+
#
|
|
1007
|
+
# NAME ROUTING on read, the two directions are wired differently:
|
|
1008
|
+
# READ __getattr__ only runs on a MISS (no instance dict entry, no
|
|
1009
|
+
# class property), so every normal attribute read pays nothing.
|
|
1010
|
+
# WRITE there is no miss-only hook for setattr, and a DrawState
|
|
1011
|
+
# __setattr__ would add a Python frame for EVERY attribute write
|
|
1012
|
+
# (~100 per render_func call, per the wrapper's own bookkeeping).
|
|
1013
|
+
# So write routing rides the __setattr__ that already exists: the
|
|
1014
|
+
# @live wrapper calls `_locate_set` when a name starts with
|
|
1015
|
+
# LOCATE_PREFIX (invalidating_decoration ensures the prefix is a single
|
|
1016
|
+
# char compare for every other name).
|
|
1017
|
+
#
|
|
1018
|
+
# A locate write does NOT invalidate this draw_state - it's exactly the
|
|
1019
|
+
# `set_anywhere(...)` call it replaces, and the value only gets there
|
|
1020
|
+
# through the source save + hotswap, which invalidates on its own.
|
|
1021
|
+
LOCATE_PREFIX = "locate_"
|
|
1022
|
+
# Class-level default lets draw_states alive from before a hotswap (whose
|
|
1023
|
+
# __init__ never saw the field) read None instead of raising.
|
|
1024
|
+
_body_actions = None
|
|
1025
|
+
_event_rects = None
|
|
1026
|
+
|
|
1027
|
+
def __getattr__(self, name):
|
|
1028
|
+
# Reached only when normal lookup failed. `locate_params` and any
|
|
1029
|
+
# future explicit property resolve BEFORE this and never arrive here.
|
|
1030
|
+
if name.startswith("locate_"):
|
|
1031
|
+
from meltygui.core.rendering.parameter_core import anywhere_value
|
|
1032
|
+
return anywhere_value(name[7:], self)
|
|
1033
|
+
raise AttributeError(name)
|
|
1034
|
+
|
|
1035
|
+
def _locate_set(self, name, value):
|
|
1036
|
+
"""The write half, called by @live's __setattr__ for `locate_*` names.
|
|
1037
|
+
Writes skip the SET_ANYWHERE_PARAMS whitelist (allow_any) — the point
|
|
1038
|
+
of the generic accessor is that any param of the view is settable —
|
|
1039
|
+
and fall back to storing the value ON THIS DRAW_STATE (ds_fallback)
|
|
1040
|
+
when no source in code sets the param, rather than rewriting the
|
|
1041
|
+
view function's signature default."""
|
|
1042
|
+
if isinstance(getattr(type(self), name, None), property):
|
|
1043
|
+
raise AttributeError(f"{name} is read-only")
|
|
1044
|
+
from meltygui.core.rendering.parameter_core import set_anywhere
|
|
1045
|
+
set_anywhere(name[7:], value, self, allow_any=True, ds_fallback=True)
|
|
1046
|
+
|
|
1047
|
+
@property
|
|
1048
|
+
def locate_params(self):
|
|
1049
|
+
"""Dict-shaped live view over this view's input parameters: reads
|
|
1050
|
+
resolve from _kwargs, item-writes go through set_anywhere.
|
|
1051
|
+
|
|
1052
|
+
ds.locate_params["x_dim"] = 0
|
|
1053
|
+
for param, value in ds.locate_params: ...
|
|
1054
|
+
|
|
1055
|
+
The whole-signature companion to `locate_<param>` above — same reads
|
|
1056
|
+
and writes, enumerable, and a real dict subclass so it renders and
|
|
1057
|
+
routes like any other dict. Read-only as an attribute; mutate it by
|
|
1058
|
+
item, which is where the anywhere write happens.
|
|
1059
|
+
|
|
1060
|
+
ONE proxy per draw_state, re-snapshotted on each access: views key
|
|
1061
|
+
caches and dirty checks off the identity of the value they were
|
|
1062
|
+
handed, so a fresh object per frame would look like a new value every
|
|
1063
|
+
frame. Stored via object.__setattr__ — like _anc_scroll_key above, it
|
|
1064
|
+
never appears on the default instance, so it isn't serialized."""
|
|
1065
|
+
from meltygui.core.rendering.parameter_core import ParamProxy
|
|
1066
|
+
proxy = self.__dict__.get('_locate_proxy')
|
|
1067
|
+
if proxy is None:
|
|
1068
|
+
proxy = ParamProxy(self)
|
|
1069
|
+
object.__setattr__(self, '_locate_proxy', proxy)
|
|
1070
|
+
return proxy
|
|
1071
|
+
return proxy.refresh()
|
|
1072
|
+
|
|
1073
|
+
@property
|
|
1074
|
+
def locate_all_params(self):
|
|
1075
|
+
"""`locate_params` plus the HEADER's params, GROUPED: two nested
|
|
1076
|
+
dicts — {'params': <view params>, 'header': <header-only params>} —
|
|
1077
|
+
each a live ParamProxy (reads resolve, item-writes go through
|
|
1078
|
+
set_anywhere). A separate cached instance, same identity rules as
|
|
1079
|
+
locate_params above."""
|
|
1080
|
+
from meltygui.core.rendering.parameter_core import GroupedParamProxy
|
|
1081
|
+
proxy = self.__dict__.get('_locate_all_proxy')
|
|
1082
|
+
if proxy is None or not isinstance(proxy, GroupedParamProxy):
|
|
1083
|
+
proxy = GroupedParamProxy(self)
|
|
1084
|
+
object.__setattr__(self, '_locate_all_proxy', proxy)
|
|
1085
|
+
return proxy
|
|
1086
|
+
return proxy.refresh()
|
|
1087
|
+
|
|
1088
|
+
def pos_changed(self):
|
|
1089
|
+
"""Reconcile this view's BVH box with its current geometry/visibility.
|
|
1090
|
+
|
|
1091
|
+
Called at the end of every render. bvh_sync is the single source of
|
|
1092
|
+
truth — it no-ops unless `bbox` (abs_left/abs_top/width/height) or
|
|
1093
|
+
visibility (inside_clip/closed/abs_closed) actually changed, and owns
|
|
1094
|
+
`_bvh_id`/`_bvh_bbox` so they stay in lockstep with the rtree."""
|
|
1095
|
+
Core.melty.bvh_sync(self)
|
|
1096
|
+
|
|
1097
|
+
# @property
|
|
1098
|
+
# def inner_cursor(self):
|
|
1099
|
+
# if self._columns_top is None:
|
|
1100
|
+
# self._columns_top = imgui.get_cursor_pos()[1]
|
|
1101
|
+
#
|
|
1102
|
+
# return self._columns_top - self.abs_top
|
|
1103
|
+
|
|
1104
|
+
#
|
|
1105
|
+
# def __getattr__(self, name):
|
|
1106
|
+
# if name.startswith("kw_"):
|
|
1107
|
+
# key = name[2:] # strip "arg_" prefix
|
|
1108
|
+
# try:
|
|
1109
|
+
# return self._kwargs[key]
|
|
1110
|
+
# except KeyError:
|
|
1111
|
+
# print(f"Warning: Attempted to access missing argument '{key}' in DrawState.")
|
|
1112
|
+
# raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
1113
|
+
#
|
|
1114
|
+
# def __setattr__(self, name, value):
|
|
1115
|
+
# if name.startswith("kw_"):
|
|
1116
|
+
# self._args[name[3:]] = value
|
|
1117
|
+
# else:
|
|
1118
|
+
# super().__setattr__(name, value)
|
|
1119
|
+
|
|
1120
|
+
@property
|
|
1121
|
+
def abs_clipped_height(self):
|
|
1122
|
+
clip_rect = self.abs_clip_rect
|
|
1123
|
+
|
|
1124
|
+
clipped_bottom = min(clip_rect[3], self.abs_top + self.height)
|
|
1125
|
+
clipped_top = max(clip_rect[1], self.abs_top)
|
|
1126
|
+
clipped_height = clipped_bottom - clipped_top
|
|
1127
|
+
return max(0, clipped_height)
|
|
1128
|
+
|
|
1129
|
+
@property
|
|
1130
|
+
def abs_content_height(self):
|
|
1131
|
+
if self.frame_count < 2:
|
|
1132
|
+
# Serve the PERSISTED measurement while this session hasn't
|
|
1133
|
+
# re-measured yet: a restored view then scrolls to its persisted
|
|
1134
|
+
# scroll_offset on the very first frame, before its content (or
|
|
1135
|
+
# a loading stand-in) renders. Returns 0 only for never-measured
|
|
1136
|
+
# views, matching the old behavior for genuinely new ones.
|
|
1137
|
+
return int(self.observed_content_height)
|
|
1138
|
+
|
|
1139
|
+
content_height = 0
|
|
1140
|
+
f = Core.melty.frame_count
|
|
1141
|
+
max_bottom = 0
|
|
1142
|
+
min_top = float('inf')
|
|
1143
|
+
key = (f, self.height, self._parent.abs_clip_rect, self.observed_content_height)
|
|
1144
|
+
is_scroll_view = not self._kwargs.get("disable_scroll", True)
|
|
1145
|
+
if self._abs_content_height_key == key:
|
|
1146
|
+
return self._abs_content_height_cache
|
|
1147
|
+
for child in self._view_children.values():
|
|
1148
|
+
if child._kwargs.get("column", 0) != 0:
|
|
1149
|
+
continue
|
|
1150
|
+
|
|
1151
|
+
if child.closable:
|
|
1152
|
+
continue
|
|
1153
|
+
if is_scroll_view:
|
|
1154
|
+
clipped_bottom = child.abs_top + child.height
|
|
1155
|
+
clipped_top = child.abs_top
|
|
1156
|
+
else:
|
|
1157
|
+
clip_rect = self._parent.abs_clip_rect
|
|
1158
|
+
clipped_bottom = min(clip_rect[3], child.abs_top + child.height)
|
|
1159
|
+
clipped_top = max(clip_rect[1], child.abs_top)
|
|
1160
|
+
|
|
1161
|
+
clipped_height = clipped_bottom - clipped_top
|
|
1162
|
+
content_height += max(0, clipped_height)
|
|
1163
|
+
|
|
1164
|
+
min_top = min(min_top, child.abs_top)
|
|
1165
|
+
max_bottom = max(max_bottom, child.abs_top + child.height)
|
|
1166
|
+
|
|
1167
|
+
content_height = max_bottom - min_top if max_bottom > min_top else 0
|
|
1168
|
+
|
|
1169
|
+
# Special case where view wants to scroll but has no children for which to infer its height
|
|
1170
|
+
if "determines_height" in self._kwargs:
|
|
1171
|
+
content_height = self._content_rect[1]
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
self._abs_content_height_cache = content_height
|
|
1175
|
+
|
|
1176
|
+
# if is_scroll_view:
|
|
1177
|
+
# content_height = max(content_height, self.height)
|
|
1178
|
+
|
|
1179
|
+
return int(self.observed_content_height)
|
|
1180
|
+
|
|
1181
|
+
@property
|
|
1182
|
+
def abs_layer(self):
|
|
1183
|
+
|
|
1184
|
+
if self.parent_window is not None and self.closable:
|
|
1185
|
+
# Nested closable windows live in the dedicated nested layer band
|
|
1186
|
+
# when their parent's root is the front window; otherwise they stay
|
|
1187
|
+
# parent-relative in the parent band. See Melty.nested_window_layer.
|
|
1188
|
+
return Core.melty.nested_window_layer(
|
|
1189
|
+
self.parent_window.abs_layer, self._kwargs.get("layer_offset", 4),
|
|
1190
|
+
ds=self)
|
|
1191
|
+
elif self.parent_window is not None:
|
|
1192
|
+
return self.parent_window.abs_layer
|
|
1193
|
+
else:
|
|
1194
|
+
return self.layer
|
|
1195
|
+
|
|
1196
|
+
@property
|
|
1197
|
+
def abs_closed(self):
|
|
1198
|
+
|
|
1199
|
+
if self.closed and self.closable:
|
|
1200
|
+
return True
|
|
1201
|
+
|
|
1202
|
+
if not self.expanded:
|
|
1203
|
+
return True
|
|
1204
|
+
|
|
1205
|
+
if self.parent_window is not None:
|
|
1206
|
+
return self.parent_window.abs_closed
|
|
1207
|
+
|
|
1208
|
+
return False
|
|
1209
|
+
|
|
1210
|
+
@property
|
|
1211
|
+
def root_window(self):
|
|
1212
|
+
if self.parent_window is None:
|
|
1213
|
+
return self
|
|
1214
|
+
else:
|
|
1215
|
+
return self.parent_window.root_window
|
|
1216
|
+
|
|
1217
|
+
@property
|
|
1218
|
+
def anchor_offset(self):
|
|
1219
|
+
anchor_margin = 2
|
|
1220
|
+
|
|
1221
|
+
if self.anchor_pos in LEFT_ANCHORS:
|
|
1222
|
+
offset_x = 0
|
|
1223
|
+
elif self.anchor_pos in RIGHT_ANCHORS:
|
|
1224
|
+
offset_x = -self.width
|
|
1225
|
+
else: # horizontally centered
|
|
1226
|
+
offset_x = int(-self.width / 2)
|
|
1227
|
+
|
|
1228
|
+
if self.anchor_pos in TOP_ANCHORS:
|
|
1229
|
+
offset_y = 0
|
|
1230
|
+
elif self.anchor_pos in BOTTOM_ANCHORS:
|
|
1231
|
+
offset_y = -self.height + -anchor_margin
|
|
1232
|
+
else: # vertically centered
|
|
1233
|
+
offset_y = int(-self.height / 2)
|
|
1234
|
+
|
|
1235
|
+
return (offset_x, offset_y)
|
|
1236
|
+
|
|
1237
|
+
@property
|
|
1238
|
+
def parent_anchor_offset(self):
|
|
1239
|
+
"""Offset from the parent window's top-left to the anchor point on the
|
|
1240
|
+
parent selected by ``anchor_pos``.
|
|
1241
|
+
|
|
1242
|
+
This is the parent-side counterpart to ``anchor_offset`` (which selects
|
|
1243
|
+
the child's own corner). The two are independent: ``parent_anchor_pos``
|
|
1244
|
+
picks the point on the parent, ``anchor_pos`` picks the child's corner
|
|
1245
|
+
that lands on it. Returns (0, 0) for a TOP_LEFT parent anchor (the
|
|
1246
|
+
default), preserving the legacy top-left-relative layout.
|
|
1247
|
+
"""
|
|
1248
|
+
parent = self.parent_window
|
|
1249
|
+
if parent is None or parent is self:
|
|
1250
|
+
return (0, 0)
|
|
1251
|
+
|
|
1252
|
+
if self.parent_anchor_pos in LEFT_ANCHORS:
|
|
1253
|
+
offset_x = 0
|
|
1254
|
+
elif self.parent_anchor_pos in RIGHT_ANCHORS:
|
|
1255
|
+
offset_x = parent.width
|
|
1256
|
+
else: # horizontally centered
|
|
1257
|
+
offset_x = int(parent.width / 2)
|
|
1258
|
+
|
|
1259
|
+
if self.parent_anchor_pos in TOP_ANCHORS:
|
|
1260
|
+
offset_y = 0
|
|
1261
|
+
elif self.parent_anchor_pos in BOTTOM_ANCHORS:
|
|
1262
|
+
offset_y = parent.height
|
|
1263
|
+
else: # vertically centered
|
|
1264
|
+
offset_y = int(parent.height / 2)
|
|
1265
|
+
|
|
1266
|
+
return (offset_x, offset_y)
|
|
1267
|
+
|
|
1268
|
+
@property
|
|
1269
|
+
def _pin_target(self):
|
|
1270
|
+
"""The view this pins to, resolved live from the Pin mode off
|
|
1271
|
+
_parent/parent_window at call time, so the float tracks it as it
|
|
1272
|
+
scrolls/moves. None when not pinned or the relative is missing.
|
|
1273
|
+
|
|
1274
|
+
Pinning only ever targets an ancestor (parent / grandparent / window),
|
|
1275
|
+
and ancestors never depend on a descendant's position — so reading the
|
|
1276
|
+
target's abs_clip_rect from here can't cycle, and needs no cache."""
|
|
1277
|
+
mode = self.pin_to_clip
|
|
1278
|
+
if mode is Pin.WINDOW:
|
|
1279
|
+
t = self.parent_window
|
|
1280
|
+
elif mode is Pin.GRANDPARENT:
|
|
1281
|
+
p = self._parent
|
|
1282
|
+
t = p._parent if (p is not None and p is not self) else None
|
|
1283
|
+
elif mode is Pin.PARENT or mode is Pin.CLIP:
|
|
1284
|
+
t = self._parent
|
|
1285
|
+
else:
|
|
1286
|
+
return None
|
|
1287
|
+
return t if (t is not None and t is not self) else None
|
|
1288
|
+
|
|
1289
|
+
@property
|
|
1290
|
+
def pin_rect(self):
|
|
1291
|
+
"""Reference rect ``(left, top, right, bottom)`` the pin anchors to, in
|
|
1292
|
+
absolute coords — the live clip rect of the resolved relative
|
|
1293
|
+
(``_pin_target``). None when there's nothing to pin to.
|
|
1294
|
+
|
|
1295
|
+
For ``Pin.CLIP`` the rect is intersected with the parent window's box so
|
|
1296
|
+
each corner clamps to the visible edge (e.g. bottom = min(parent_bottom,
|
|
1297
|
+
clip_bottom)) instead of running past it.
|
|
1298
|
+
|
|
1299
|
+
The target's box is read from its LIVE abs (``_abs_left``/``_abs_top``),
|
|
1300
|
+
not its per-frame-cached ``abs_left``/``abs_top``. A pinned float needs
|
|
1301
|
+
the target's current position on frames the target itself isn't
|
|
1302
|
+
re-rendering — e.g. while its window is dragged: the window moves (its
|
|
1303
|
+
own abs cache invalidates on window_pos), but the target's cache key
|
|
1304
|
+
doesn't change, so cached ``target.abs_left`` would return last frame's
|
|
1305
|
+
anchor. Computing live re-reads the moved window through the parent
|
|
1306
|
+
chain, so the anchor tracks the drag instead of lagging a frame."""
|
|
1307
|
+
target = self._pin_target
|
|
1308
|
+
if target is None:
|
|
1309
|
+
return None
|
|
1310
|
+
# Anchor to the target's RAW box (live abs), so the float tracks the
|
|
1311
|
+
# target's actual position. PARENT/WINDOW/GRANDPARENT do NOT clamp here:
|
|
1312
|
+
# intersecting with the target's clip rect makes the anchor corner snap
|
|
1313
|
+
# to the clip edge whenever the target is clipped, so the window tracks
|
|
1314
|
+
# the *clip* instead of the target (the "only moves when the clamp moves
|
|
1315
|
+
# it" bug). Clamping is Pin.CLIP's job - see below.
|
|
1316
|
+
tl, tt = target._abs_left(), target._abs_top()
|
|
1317
|
+
rect = (tl, tt, tl + target.width, tt + target.height)
|
|
1318
|
+
if self.pin_to_clip is Pin.CLIP:
|
|
1319
|
+
# Pin.CLIP clamps the float to the parent window's visible box so
|
|
1320
|
+
# corners track the visible edge. Use the window's LIVE box
|
|
1321
|
+
# (win._abs_left()/_abs_top()), not its cached abs_left or the
|
|
1322
|
+
# target's captured clip_rect: both are fixed in screen space and so
|
|
1323
|
+
# are stale when the window itself is dragged, which left Pin.CLIP
|
|
1324
|
+
# floats clamped to the old position. The live window box is correct
|
|
1325
|
+
# in both cases - it's constant while content scrolls and moves with
|
|
1326
|
+
# a window window.
|
|
1327
|
+
win = self.parent_window
|
|
1328
|
+
if win is None and len(Core.melty.melty_windows) > 0:
|
|
1329
|
+
win = Core.melty.melty_windows[-1]
|
|
1330
|
+
if win is not None and win is not self:
|
|
1331
|
+
wl, wt = win._abs_left(), win._abs_top()
|
|
1332
|
+
c = (wl, wt, wl + win.width, wt + win.height)
|
|
1333
|
+
rect = (max(rect[0], c[0]), max(rect[1], c[1]),
|
|
1334
|
+
min(rect[2], c[2]), min(rect[3], c[3]))
|
|
1335
|
+
return rect
|
|
1336
|
+
|
|
1337
|
+
@property
|
|
1338
|
+
def clip_anchor_base(self):
|
|
1339
|
+
"""Anchor reference point on the pinned rect (see ``pin_rect``).
|
|
1340
|
+
|
|
1341
|
+
Picks the corner of the pin rect selected by ``parent_anchor_pos`` — the
|
|
1342
|
+
point *on the target* the window attaches to. ``anchor_offset`` (driven
|
|
1343
|
+
by ``anchor_pos``) then shifts the window so its own corner lands there,
|
|
1344
|
+
so the two are independent: e.g. parent_anchor=BOTTOM_RIGHT /
|
|
1345
|
+
anchor=TOP_RIGHT hangs the window off the target's bottom-right corner.
|
|
1346
|
+
With both at the TOP_LEFT default the window's top-left sits on the
|
|
1347
|
+
target's top-left. Computed live (pin_rect already is), so it tracks the
|
|
1348
|
+
target regardless of how it scrolls.
|
|
1349
|
+
"""
|
|
1350
|
+
clip = self.pin_rect
|
|
1351
|
+
if clip is None:
|
|
1352
|
+
return None
|
|
1353
|
+
clip_left, clip_top, clip_right, clip_bottom = clip
|
|
1354
|
+
|
|
1355
|
+
if self.parent_anchor_pos in LEFT_ANCHORS:
|
|
1356
|
+
base_x = clip_left
|
|
1357
|
+
elif self.parent_anchor_pos in RIGHT_ANCHORS:
|
|
1358
|
+
base_x = clip_right
|
|
1359
|
+
else: # horizontally centered
|
|
1360
|
+
base_x = (clip_left + clip_right) / 2
|
|
1361
|
+
|
|
1362
|
+
if self.parent_anchor_pos in TOP_ANCHORS:
|
|
1363
|
+
base_y = clip_top
|
|
1364
|
+
elif self.parent_anchor_pos in BOTTOM_ANCHORS:
|
|
1365
|
+
base_y = clip_bottom
|
|
1366
|
+
else: # vertically centered
|
|
1367
|
+
base_y = (clip_top + clip_bottom) / 2
|
|
1368
|
+
|
|
1369
|
+
return (base_x, base_y)
|
|
1370
|
+
|
|
1371
|
+
def _ancestor_scroll(self):
|
|
1372
|
+
"""Sum the scroll_offsets of intermediate ancestors between self and
|
|
1373
|
+
``parent_window`` (exclusive at both ends) by walking the ``_parent``
|
|
1374
|
+
chain. Used so abs_left/abs_top respond to ancestor scroll deltas
|
|
1375
|
+
without waiting for the descendant to re-render — left_offset is
|
|
1376
|
+
stored as the *unscrolled* content position and this delta subtracted
|
|
1377
|
+
on demand. Returns (sx, sy).
|
|
1378
|
+
|
|
1379
|
+
Memoized per (frame_count, Melty.scroll_version): the walk used to run
|
|
1380
|
+
on EVERY abs_left/abs_top access just to build their cache keys (~57
|
|
1381
|
+
walks per render_func call). Any real scroll_offset change anywhere
|
|
1382
|
+
bumps scroll_version (see new_setattr in invalidation_decoration), so
|
|
1383
|
+
mid-frame scroll deltas still invalidate exactly like the uncached
|
|
1384
|
+
walk did."""
|
|
1385
|
+
meltygui = Core.melty
|
|
1386
|
+
key = (meltygui.frame_count, getattr(meltygui, 'scroll_version', 0))
|
|
1387
|
+
if self._anc_scroll_key == key:
|
|
1388
|
+
return self._anc_scroll_cache
|
|
1389
|
+
sx = sy = 0
|
|
1390
|
+
node = self._parent
|
|
1391
|
+
stop = self.parent_window
|
|
1392
|
+
# Cycle guard mirroring the abs_left guard: bail if we revisit self
|
|
1393
|
+
# or walk longer than the deepest realistic chain.
|
|
1394
|
+
for _ in range(64):
|
|
1395
|
+
if node is None or node is self or node is stop:
|
|
1396
|
+
break
|
|
1397
|
+
so = node.scroll_offset
|
|
1398
|
+
if so is not None:
|
|
1399
|
+
ny = so[1]
|
|
1400
|
+
# Enforce the scroll bound here, at the source, so writers (a
|
|
1401
|
+
# text editor's drag-auto-scroll, pans) don't each have to clamp.
|
|
1402
|
+
# node._max_scroll_y is the authoritative max core_render
|
|
1403
|
+
# published this frame. Clamp any contribution AND write it back
|
|
1404
|
+
# so the stored offset can't run past the content ends - that
|
|
1405
|
+
# runaway is what made auto-scroll overshoot EOF and lag on the
|
|
1406
|
+
# way back. None = the node hasn't published a max yet.
|
|
1407
|
+
mx = node._max_scroll_y
|
|
1408
|
+
if mx is not None:
|
|
1409
|
+
if ny < 0:
|
|
1410
|
+
ny = 0.0
|
|
1411
|
+
elif ny > mx:
|
|
1412
|
+
ny = mx
|
|
1413
|
+
if ny != so[1]:
|
|
1414
|
+
node.scroll_offset = (so[0], ny)
|
|
1415
|
+
|
|
1416
|
+
sx += so[0]
|
|
1417
|
+
sy += ny
|
|
1418
|
+
nxt = node._parent
|
|
1419
|
+
if nxt is node:
|
|
1420
|
+
break
|
|
1421
|
+
node = nxt
|
|
1422
|
+
# Raw writes so the memo must not re-enter @live setattr tracking.
|
|
1423
|
+
# Cache before key, so a matching key always sees a written cache.
|
|
1424
|
+
# (The clamp writeback above may have bumped scroll_version, making
|
|
1425
|
+
# this key already anyway - the next access just recomputes.)
|
|
1426
|
+
object.__setattr__(self, '_anc_scroll_cache', (sx, sy))
|
|
1427
|
+
object.__setattr__(self, '_anc_scroll_key', key)
|
|
1428
|
+
return sx, sy
|
|
1429
|
+
|
|
1430
|
+
def _pinned_base_y(self, base_y, anchor_y):
|
|
1431
|
+
"""Bound a pinned nested window's vertical ANCHOR to its parent window:
|
|
1432
|
+
the float's TOP can ride at most 200px above the window's top, and its
|
|
1433
|
+
TOP can't drop below the window's bottom. (Display top/height when there is no
|
|
1434
|
+
parent window.)
|
|
1435
|
+
|
|
1436
|
+
Pinned floats (context menus, live-value windows — pin_to_clip set)
|
|
1437
|
+
follow their spawner: pin_rect reads the target's live abs, so a tall
|
|
1438
|
+
spawner (e.g. a big scrolling draw_text) that scrolls drags the float
|
|
1439
|
+
clean out of the window with it — the spawning VIEW routinely sits far
|
|
1440
|
+
above/below the window's own box. Bounding against the WINDOW (not the
|
|
1441
|
+
display, not the view) keeps the float within a window-height of travel
|
|
1442
|
+
past each edge, so it's always adjacent to the window it belongs to.
|
|
1443
|
+
|
|
1444
|
+
Clamps the ANCHOR (base_y), NOT the final abs top: window_pos_y (the
|
|
1445
|
+
user's drag) is deliberately left out of the bound, so a click-drag
|
|
1446
|
+
still moves the window freely — clamping the sum ate vertical drag and
|
|
1447
|
+
left a dead zone on the way back. Same shape as the swoosh, which
|
|
1448
|
+
clamps its parent anchor to the window edge."""
|
|
1449
|
+
if not self.closable:
|
|
1450
|
+
return base_y
|
|
1451
|
+
win = self.parent_window
|
|
1452
|
+
if win is not None and win is not self:
|
|
1453
|
+
win_top, win_h = win._abs_top(), win.height or 0
|
|
1454
|
+
else:
|
|
1455
|
+
disp = Core.melty.display_size
|
|
1456
|
+
win_top, win_h = 0, (disp[1] if disp is not None else 0)
|
|
1457
|
+
floor_y = win_top - 200 - anchor_y
|
|
1458
|
+
ceil_y = win_top + win_h - anchor_y
|
|
1459
|
+
if floor_y <= base_y <= ceil_y:
|
|
1460
|
+
return base_y
|
|
1461
|
+
try:
|
|
1462
|
+
# Exempt the floating DnD window: glue_window_to_cursor assumes abs
|
|
1463
|
+
# is linear in window_pos, and a clamp makes its per-frame
|
|
1464
|
+
# correction accumulate without bound (see _cap_to_display).
|
|
1465
|
+
from meltygui.core.input.drag_drop_core import DragDrop
|
|
1466
|
+
if DragDrop.is_dragged_item(self):
|
|
1467
|
+
return base_y
|
|
1468
|
+
except Exception:
|
|
1469
|
+
pass
|
|
1470
|
+
return floor_y if base_y < floor_y else ceil_y
|
|
1471
|
+
|
|
1472
|
+
def _cap_to_display(self, pos, axis):
|
|
1473
|
+
"""Cap a nested window's computed abs position so at least a sliver of
|
|
1474
|
+
its box stays on the display. Nested windows follow their spawning
|
|
1475
|
+
view's scroll (the ancestor-scroll subtraction in _abs_left/_abs_top),
|
|
1476
|
+
which otherwise carries them right off screen. Only the COMPUTED
|
|
1477
|
+
position is capped — left_offset / window_pos stay untouched — so
|
|
1478
|
+
scrolling back returns the window to its natural spot. A sliver cap
|
|
1479
|
+
(not full containment) so a deliberate drag can still tuck a window
|
|
1480
|
+
mostly off screen without ever losing its grab handle. The floating
|
|
1481
|
+
DnD window is exempt: glue_window_to_cursor assumes abs is linear in
|
|
1482
|
+
window_pos, and a clamp would make its per-frame correction
|
|
1483
|
+
accumulate without bound."""
|
|
1484
|
+
if (not self.closable or self.parent_window is None
|
|
1485
|
+
or self.parent_window is self):
|
|
1486
|
+
return pos
|
|
1487
|
+
size = self.width if axis == 0 else self.height
|
|
1488
|
+
disp = Core.melty.display_size
|
|
1489
|
+
if size is None or disp is None:
|
|
1490
|
+
return pos
|
|
1491
|
+
keep = min(48, size)
|
|
1492
|
+
if self._kwargs.get("keep_in_view", False):
|
|
1493
|
+
# Popup menus stay wholly visible. Ordinary movable windows retain
|
|
1494
|
+
# their sliver allowance so you can deliberately tuck them away.
|
|
1495
|
+
lo, hi = 0, max(0, disp[axis] - size)
|
|
1496
|
+
else:
|
|
1497
|
+
lo, hi = keep - size, disp[axis] - keep
|
|
1498
|
+
# whether the cap is holding this axis: the OS-edge physics
|
|
1499
|
+
# (os_frame._root_extent) pull a scrolled-out nested window out of
|
|
1500
|
+
# its parent's collision extent
|
|
1501
|
+
capped = not (lo <= pos <= hi)
|
|
1502
|
+
if axis == 0:
|
|
1503
|
+
self._capped_x = capped
|
|
1504
|
+
else:
|
|
1505
|
+
self._capped_y = capped
|
|
1506
|
+
if not capped:
|
|
1507
|
+
return pos
|
|
1508
|
+
try:
|
|
1509
|
+
from meltygui.core.input.drag_drop_core import DragDrop
|
|
1510
|
+
if DragDrop.is_dragged_item(self):
|
|
1511
|
+
return pos
|
|
1512
|
+
except Exception:
|
|
1513
|
+
pass
|
|
1514
|
+
return max(min(pos, hi), lo)
|
|
1515
|
+
|
|
1516
|
+
def _abs_left(self):
|
|
1517
|
+
# Parent-relative placement reads the dependency-checked absolute
|
|
1518
|
+
# origin. The cache marks evaluation before recursing, so a malformed
|
|
1519
|
+
# parent cycle returns the last position instead of recursing forever.
|
|
1520
|
+
parent_left = 0
|
|
1521
|
+
if self.parent_window is not None and self.parent_window is not self:
|
|
1522
|
+
parent_left = self.parent_window.abs_left
|
|
1523
|
+
|
|
1524
|
+
window_pos_x = self.window_pos[0] if self.window_pos is not None else 0
|
|
1525
|
+
anchor = self.anchor_offset
|
|
1526
|
+
|
|
1527
|
+
base = self.clip_anchor_base if self.pin_to_clip else None
|
|
1528
|
+
if base is not None:
|
|
1529
|
+
# Pin to the clip rect corner rather than the scrolled position of
|
|
1530
|
+
# the declaring view (which left_offset tracks).
|
|
1531
|
+
this_left = window_pos_x + base[0] + anchor[0]
|
|
1532
|
+
if (self.pin_to_clip is Pin.CLIP and self.width is not None
|
|
1533
|
+
and self.parent_window is not None and self.parent_window is not self):
|
|
1534
|
+
# Box-level clamp: the rect clamp only repositions the anchor
|
|
1535
|
+
# point, so if anchor_offset shifts the box its far edge can
|
|
1536
|
+
# still hang off the window. Pull the box back so it stays
|
|
1537
|
+
# inside the window horizontally (favoring the left edge if the
|
|
1538
|
+
# box is wider than the window).
|
|
1539
|
+
win = self.parent_window
|
|
1540
|
+
win_left = win.abs_left
|
|
1541
|
+
this_left = min(this_left, win_left + win.width - self.width)
|
|
1542
|
+
this_left = max(this_left, win_left)
|
|
1543
|
+
else:
|
|
1544
|
+
# left_offset is stored as the *unscrolled* position in parent_window's
|
|
1545
|
+
# content (capture sites add the ancestor scroll). Subtracting the
|
|
1546
|
+
# live ancestor scroll here makes abs_left immediately reflect any
|
|
1547
|
+
# mid-frame scroll change on an ancestor - what previously had to
|
|
1548
|
+
# wait for the descendant to re-render with a new cursor pos.
|
|
1549
|
+
sx, _ = self._ancestor_scroll()
|
|
1550
|
+
this_left = (window_pos_x + parent_left + self.left_offset - sx
|
|
1551
|
+
+ self.parent_anchor_offset[0] + anchor[0])
|
|
1552
|
+
this_left = self._cap_to_display(this_left, 0)
|
|
1553
|
+
return int(this_left)
|
|
1554
|
+
|
|
1555
|
+
def _abs_top_true(self):
|
|
1556
|
+
parent_top = 0
|
|
1557
|
+
if self._parent is not None and self._parent is not self:
|
|
1558
|
+
parent_top = self._parent._abs_top_true()
|
|
1559
|
+
parent_scroll = self._parent.scroll_offset[1] if self._parent.scroll_offset is not None else 0
|
|
1560
|
+
parent_top -= parent_scroll
|
|
1561
|
+
|
|
1562
|
+
anchor = self.anchor_offset
|
|
1563
|
+
window_pos_y = self.window_pos[1] if self.window_pos is not None else 0
|
|
1564
|
+
|
|
1565
|
+
base = self.clip_anchor_base if self.pin_to_clip else None
|
|
1566
|
+
if base is not None:
|
|
1567
|
+
# Pin to the clip rect corner rather than the scrolled position of
|
|
1568
|
+
# the declaring view (which top_offset tracks).
|
|
1569
|
+
this_top = window_pos_y + self._pinned_base_y(base[1], anchor[1]) + anchor[1]
|
|
1570
|
+
if (self.pin_to_clip is Pin.CLIP and self.height is not None
|
|
1571
|
+
and self.parent_window is not None and self.parent_window is not self):
|
|
1572
|
+
# Box-level clamp: see _abs_left. Keeps the bottom edge from
|
|
1573
|
+
# hanging below the window (favoring the top edge if the box
|
|
1574
|
+
# is taller than the window).
|
|
1575
|
+
win = self.parent_window
|
|
1576
|
+
win_top = win.abs_top
|
|
1577
|
+
this_top = min(this_top, win_top + win.height - self.height)
|
|
1578
|
+
this_top = max(this_top, win_top)
|
|
1579
|
+
else:
|
|
1580
|
+
# See _abs_left for why this subtracts the live ancestor scroll.
|
|
1581
|
+
this_top = (window_pos_y + parent_top + self.top_offset_true
|
|
1582
|
+
+ self.parent_anchor_offset[1] + anchor[1])
|
|
1583
|
+
return int(this_top)
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _abs_top(self):
|
|
1587
|
+
parent_top = 0
|
|
1588
|
+
if self.parent_window is not None and self.parent_window is not self:
|
|
1589
|
+
parent_top = self.parent_window.abs_top
|
|
1590
|
+
|
|
1591
|
+
anchor = self.anchor_offset
|
|
1592
|
+
window_pos_y = self.window_pos[1] if self.window_pos is not None else 0
|
|
1593
|
+
|
|
1594
|
+
base = self.clip_anchor_base if self.pin_to_clip else None
|
|
1595
|
+
if base is not None:
|
|
1596
|
+
# Pin to the clip rect corner rather than the scrolled position of
|
|
1597
|
+
# the declaring view (which top_offset tracks).
|
|
1598
|
+
this_top = window_pos_y + self._pinned_base_y(base[1], anchor[1]) + anchor[1]
|
|
1599
|
+
if (self.pin_to_clip is Pin.CLIP and self.height is not None
|
|
1600
|
+
and self.parent_window is not None and self.parent_window is not self):
|
|
1601
|
+
# Box-level clamp: see _abs_left. Keeps the bottom edge from
|
|
1602
|
+
# hanging below the window (favoring the top edge if the box
|
|
1603
|
+
# is taller than the window).
|
|
1604
|
+
win = self.parent_window
|
|
1605
|
+
win_top = win.abs_top
|
|
1606
|
+
this_top = min(this_top, win_top + win.height - self.height)
|
|
1607
|
+
this_top = max(this_top, win_top)
|
|
1608
|
+
else:
|
|
1609
|
+
# See _abs_left for why this subtracts the live ancestor scroll.
|
|
1610
|
+
_, sy = self._ancestor_scroll()
|
|
1611
|
+
this_top = (window_pos_y + parent_top + self.top_offset - sy
|
|
1612
|
+
+ self.parent_anchor_offset[1] + anchor[1])
|
|
1613
|
+
this_top = self._cap_to_display(this_top, 1)
|
|
1614
|
+
return int(this_top)
|
|
1615
|
+
|
|
1616
|
+
@property
|
|
1617
|
+
def abs_clip_rect(self):
|
|
1618
|
+
abs_left = self.abs_left
|
|
1619
|
+
abs_top = self.abs_top
|
|
1620
|
+
box_right = abs_left + self.width
|
|
1621
|
+
box_bottom = abs_top + self.height
|
|
1622
|
+
clip = self.clip_rect
|
|
1623
|
+
if clip is None:
|
|
1624
|
+
return (abs_left, abs_top, box_right, box_bottom)
|
|
1625
|
+
|
|
1626
|
+
# clip_rect is stored in absolute screen coords, so it's correct while
|
|
1627
|
+
# content scrolls (the clip region is fixed in screen space) but stale
|
|
1628
|
+
# when the parent window moves. Shift it by however far the parent window
|
|
1629
|
+
# has moved since capture - zero during scroll, the mouse delta during a
|
|
1630
|
+
# window drag - so the clamp tracks the window without a re-render. The
|
|
1631
|
+
# clip region moves rigidly with the window, so a uniform shift is exact.
|
|
1632
|
+
pw = self.parent_window
|
|
1633
|
+
anchor = self._clip_win_anchor
|
|
1634
|
+
if pw is not None and pw is not self and anchor is not None:
|
|
1635
|
+
dx = pw._abs_left() - anchor[0]
|
|
1636
|
+
dy = pw._abs_top() - anchor[1]
|
|
1637
|
+
clip = (clip[0] + dx, clip[1] + dy, clip[2] + dx, clip[3] + dy)
|
|
1638
|
+
|
|
1639
|
+
# Intersect the LIVE box with the (shifted) clip rect. Both move with the
|
|
1640
|
+
# window now, so the clamped edges stay pinned to the unclamped corner.
|
|
1641
|
+
return (int(max(abs_left, clip[0])),
|
|
1642
|
+
int(max(abs_top, clip[1])),
|
|
1643
|
+
int(min(box_right, clip[2])),
|
|
1644
|
+
int(min(box_bottom, clip[3])))
|
|
1645
|
+
|
|
1646
|
+
@property
|
|
1647
|
+
def abs_clip_rect_local(self):
|
|
1648
|
+
abs_left = self.abs_left
|
|
1649
|
+
abs_top = self.abs_top
|
|
1650
|
+
clipped_by = self.clipped_by_rect
|
|
1651
|
+
if clipped_by is None:
|
|
1652
|
+
clip = (abs_left, abs_top, abs_left + self.width, abs_top + self.height)
|
|
1653
|
+
else:
|
|
1654
|
+
clip = (abs_left + clipped_by[0],
|
|
1655
|
+
abs_top + clipped_by[1],
|
|
1656
|
+
abs_left + self.width - clipped_by[2],
|
|
1657
|
+
abs_top + self.height - clipped_by[3])
|
|
1658
|
+
box_right = abs_left + self.width
|
|
1659
|
+
box_bottom = abs_top + self.height
|
|
1660
|
+
# clip_rect is captured in absolute screen coords, so it's correct while
|
|
1661
|
+
# content scrolls (the clip region is fixed in screen space) but stale
|
|
1662
|
+
# when the whole window moves. Shift it by however far the parent window
|
|
1663
|
+
# has moved since capture - zero during scroll, the drag delta during a
|
|
1664
|
+
# window drag - so the clamp tracks the window without a re-render. The
|
|
1665
|
+
# clip region moves rigidly with the window, so a uniform shift is exact.
|
|
1666
|
+
pw = self.parent_window
|
|
1667
|
+
anchor = self._clip_win_anchor
|
|
1668
|
+
if pw is not None and pw is not self and anchor is not None:
|
|
1669
|
+
dx = pw._abs_left() - anchor[0]
|
|
1670
|
+
dy = pw._abs_top() - anchor[1]
|
|
1671
|
+
clip = (clip[0] + dx, clip[1] + dy, clip[2] + dx, clip[3] + dy)
|
|
1672
|
+
|
|
1673
|
+
# Intersect the LIVE box with the (shifted) clip rect. Both move with the
|
|
1674
|
+
# window now, so the clamped edges stay locked to the unclamped corner.
|
|
1675
|
+
return (int(max(abs_left, clip[0])),
|
|
1676
|
+
int(max(abs_top, clip[1])),
|
|
1677
|
+
int(min(box_right, clip[2])),
|
|
1678
|
+
int(min(box_bottom, clip[3])))
|
|
1679
|
+
|
|
1680
|
+
def children_in_clip(self, clip=None, max_depth=1, include_windows=True):
|
|
1681
|
+
"""Descendants whose vertical span overlaps `clip` (default: this view's
|
|
1682
|
+
abs_clip_rect), top-to-bottom.
|
|
1683
|
+
|
|
1684
|
+
`max_depth` bounds the recursion: 1 (default) returns only direct
|
|
1685
|
+
children; higher values also descend into them, including any descendant
|
|
1686
|
+
that overlaps `clip`, down at most `max_depth` levels. Pass
|
|
1687
|
+
`max_depth=None` for the whole subtree. The same `clip` is carried down
|
|
1688
|
+
— positions are absolute (screen space), so a grandchild is kept iff it
|
|
1689
|
+
overlaps the original viewport, not its immediate parent's box.
|
|
1690
|
+
|
|
1691
|
+
`include_windows=False` prunes nested windows (closable) and their
|
|
1692
|
+
whole subtree — same contract as get_child_keys' include_windows: a
|
|
1693
|
+
window owns its own composition and doesn't move with this view, so
|
|
1694
|
+
callers reacting to this view's layout (e.g. the scroll-in sweep)
|
|
1695
|
+
shouldn't touch it.
|
|
1696
|
+
|
|
1697
|
+
Results are pre-order (each child immediately followed by its own
|
|
1698
|
+
in-clip subtree) and deduped by identity across the whole walk."""
|
|
1699
|
+
if clip is None:
|
|
1700
|
+
clip = self.abs_clip_rect
|
|
1701
|
+
if clip is None:
|
|
1702
|
+
return []
|
|
1703
|
+
|
|
1704
|
+
direct = self._direct_children_in_clip(clip)
|
|
1705
|
+
if not include_windows:
|
|
1706
|
+
direct = [c for c in direct if not c.closable]
|
|
1707
|
+
if max_depth is not None and max_depth <= 1:
|
|
1708
|
+
return direct
|
|
1709
|
+
|
|
1710
|
+
next_depth = None if max_depth is None else max_depth - 1
|
|
1711
|
+
seen = set()
|
|
1712
|
+
result = []
|
|
1713
|
+
for c in direct:
|
|
1714
|
+
if id(c) not in seen:
|
|
1715
|
+
seen.add(id(c))
|
|
1716
|
+
result.append(c)
|
|
1717
|
+
for gc in c.children_in_clip(clip, next_depth, include_windows):
|
|
1718
|
+
if id(gc) not in seen:
|
|
1719
|
+
seen.add(id(gc))
|
|
1720
|
+
result.append(gc)
|
|
1721
|
+
return result
|
|
1722
|
+
|
|
1723
|
+
def _direct_children_in_clip(self, clip):
|
|
1724
|
+
"""Direct children overlapping `clip`, ordered top-to-bottom by live
|
|
1725
|
+
abs_top.
|
|
1726
|
+
|
|
1727
|
+
Children self-register into _view_children (core_render, where _parent
|
|
1728
|
+
is set) keyed by id. abs_left/abs_top are read live, so this reflects
|
|
1729
|
+
the current scroll immediately — unlike a BVH clip query, whose boxes
|
|
1730
|
+
only catch up when each child re-renders, so it would miss exactly the
|
|
1731
|
+
children that just scrolled in.
|
|
1732
|
+
|
|
1733
|
+
A plain 1-D overlap filter, NOT a binary search: rows can have different
|
|
1734
|
+
heights, so ordering by abs_top does not order by bottom edge
|
|
1735
|
+
(abs_top + height), and a binary search keyed on either edge skips a
|
|
1736
|
+
band of visible rows. We filter (so dict iteration order is irrelevant),
|
|
1737
|
+
then sort the survivors top-to-bottom. A child can appear under more
|
|
1738
|
+
than one key, and one that has since re-rendered elsewhere leaves a
|
|
1739
|
+
stale entry behind — so we dedupe by identity and drop any whose _parent
|
|
1740
|
+
is no longer this view."""
|
|
1741
|
+
children = self._view_children
|
|
1742
|
+
if not children:
|
|
1743
|
+
return []
|
|
1744
|
+
clip_top, clip_bottom = clip[1], clip[3]
|
|
1745
|
+
|
|
1746
|
+
seen = set()
|
|
1747
|
+
result = []
|
|
1748
|
+
for c in children.values():
|
|
1749
|
+
if c is None or c is self or c._parent is not self:
|
|
1750
|
+
continue
|
|
1751
|
+
if id(c) in seen:
|
|
1752
|
+
continue
|
|
1753
|
+
seen.add(id(c))
|
|
1754
|
+
top = c.abs_top
|
|
1755
|
+
if top < clip_bottom and top + (c.height or 0) > clip_top:
|
|
1756
|
+
result.append(c)
|
|
1757
|
+
result.sort(key=lambda c: c.abs_top)
|
|
1758
|
+
return result
|
|
1759
|
+
|
|
1760
|
+
def descendants(self, max_depth=None):
|
|
1761
|
+
"""Every descendant draw_state via _view_children, pre-order and deduped
|
|
1762
|
+
by identity, bounded by `max_depth` (None = whole subtree).
|
|
1763
|
+
|
|
1764
|
+
The un-clipped sibling of children_in_clip: no viewport filter, so it
|
|
1765
|
+
returns rows that have scrolled off-screen too — as long as they
|
|
1766
|
+
rendered at least once and are still parented here. Used to walk the
|
|
1767
|
+
whole tree without drawing, e.g. to recount search matches by invoking
|
|
1768
|
+
each view's _search_matcher (meltygui.search_walk), instead of force-
|
|
1769
|
+
rendering off-screen rows just so they re-register their counts."""
|
|
1770
|
+
seen = set()
|
|
1771
|
+
result = []
|
|
1772
|
+
|
|
1773
|
+
def _walk(node, depth):
|
|
1774
|
+
if depth is not None and depth <= 0:
|
|
1775
|
+
return
|
|
1776
|
+
children = node._view_children
|
|
1777
|
+
if not children:
|
|
1778
|
+
return
|
|
1779
|
+
for c in list(children.values()):
|
|
1780
|
+
if c is None or c is node or c._parent is not node or id(c) in seen:
|
|
1781
|
+
continue
|
|
1782
|
+
seen.add(id(c))
|
|
1783
|
+
result.append(c)
|
|
1784
|
+
_walk(c, None if depth is None else depth - 1)
|
|
1785
|
+
|
|
1786
|
+
_walk(self, max_depth)
|
|
1787
|
+
return result
|
|
1788
|
+
|
|
1789
|
+
@property
|
|
1790
|
+
def size_change(self):
|
|
1791
|
+
if self._tile_id is None or self.width is None or self.height is None:
|
|
1792
|
+
return True
|
|
1793
|
+
#
|
|
1794
|
+
# if (not imgui.is_mouse_down(0)
|
|
1795
|
+
# and not imgui.is_mouse_down(1)
|
|
1796
|
+
# and not imgui.is_mouse_down(2)):
|
|
1797
|
+
# return True
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
cache = Core.melty.cache
|
|
1802
|
+
if cache is None:
|
|
1803
|
+
return False
|
|
1804
|
+
t = cache._tiles.get(self._tile_id)
|
|
1805
|
+
if t is None:
|
|
1806
|
+
return False
|
|
1807
|
+
return t.size != (int(self.width), int(self.height))
|
|
1808
|
+
|
|
1809
|
+
@property
|
|
1810
|
+
def tile_fully_filled(self) -> bool:
|
|
1811
|
+
"""Debug helper: True when this view's blit tile has had every pixel
|
|
1812
|
+
written from the main framebuffer at least once. Used by the filled-
|
|
1813
|
+
tile overlay (Toggles.show_filled_tiles)."""
|
|
1814
|
+
if self._tile_id is None:
|
|
1815
|
+
return False
|
|
1816
|
+
cache = Core.melty.cache
|
|
1817
|
+
if cache is None:
|
|
1818
|
+
return False
|
|
1819
|
+
return cache._tile_fully_filled(cache._tiles.get(self._tile_id))
|
|
1820
|
+
|
|
1821
|
+
def _cached_absolute_position(self, axis):
|
|
1822
|
+
# Pins already resolve their target live. For ordinary placement,
|
|
1823
|
+
# validate the parent origin and selected corner before accepting a
|
|
1824
|
+
# cached value: solve() may have read us before a parent hand resize
|
|
1825
|
+
# or native rebase later in this same frame.
|
|
1826
|
+
#
|
|
1827
|
+
# This is read thousands of times a frame (every on_action, clip rect
|
|
1828
|
+
# and hover test), so a cache hit must stay cheap at any nesting depth:
|
|
1829
|
+
# the key and cache writes are raw, as in _ancestor_scroll, because the
|
|
1830
|
+
# memo must not re-enter @live setattr tracking, and only this axis of
|
|
1831
|
+
# parent_anchor_offset is worked out.
|
|
1832
|
+
if self.pin_to_clip:
|
|
1833
|
+
return self._abs_left() if axis == 0 else self._abs_top()
|
|
1834
|
+
if axis == 0:
|
|
1835
|
+
key_name, previous, cached = '_abs_left_key', self._abs_left_key, self._abs_left_cache
|
|
1836
|
+
else:
|
|
1837
|
+
key_name, previous, cached = '_abs_top_key', self._abs_top_key, self._abs_top_cache
|
|
1838
|
+
if previous is False:
|
|
1839
|
+
return cached # malformed parent cycles retain the last position
|
|
1840
|
+
parent = self.parent_window
|
|
1841
|
+
if parent is None or parent is self:
|
|
1842
|
+
# A root reads no other draw_state, so it needs no cycle mark.
|
|
1843
|
+
parent = None
|
|
1844
|
+
parent_origin = parent_extent = 0
|
|
1845
|
+
else:
|
|
1846
|
+
# Mark evaluation before reading the parent: a parent cycle gets
|
|
1847
|
+
# the last position back instead of recursing forever.
|
|
1848
|
+
object.__setattr__(self, key_name, False)
|
|
1849
|
+
key = None
|
|
1850
|
+
try:
|
|
1851
|
+
if parent is not None:
|
|
1852
|
+
parent_anchor_pos = self.parent_anchor_pos
|
|
1853
|
+
if axis == 0:
|
|
1854
|
+
parent_origin = parent.abs_left
|
|
1855
|
+
parent_extent = 0 if parent_anchor_pos in LEFT_ANCHORS else parent.width
|
|
1856
|
+
else:
|
|
1857
|
+
parent_origin = parent.abs_top
|
|
1858
|
+
parent_extent = 0 if parent_anchor_pos in TOP_ANCHORS else parent.height
|
|
1859
|
+
scroll = self._ancestor_scroll()[axis]
|
|
1860
|
+
# parent_extent with parent_anchor_pos decides parent_anchor_offset.
|
|
1861
|
+
key = (Core.melty.frame_count,
|
|
1862
|
+
self.left_offset if axis == 0 else self.top_offset,
|
|
1863
|
+
self.window_pos, self.anchor_pos, self.parent_anchor_pos,
|
|
1864
|
+
self.width if axis == 0 else self.height, scroll,
|
|
1865
|
+
parent_origin, parent_extent)
|
|
1866
|
+
if previous == key:
|
|
1867
|
+
key = previous
|
|
1868
|
+
return cached
|
|
1869
|
+
value = self._abs_left() if axis == 0 else self._abs_top()
|
|
1870
|
+
object.__setattr__(self, '_abs_left_cache' if axis == 0 else '_abs_top_cache', value)
|
|
1871
|
+
return value
|
|
1872
|
+
finally:
|
|
1873
|
+
if parent is not None or key is not previous:
|
|
1874
|
+
object.__setattr__(self, key_name, key)
|
|
1875
|
+
|
|
1876
|
+
@property
|
|
1877
|
+
def abs_left(self):
|
|
1878
|
+
return self._cached_absolute_position(0)
|
|
1879
|
+
|
|
1880
|
+
@property
|
|
1881
|
+
def abs_top(self):
|
|
1882
|
+
return self._cached_absolute_position(1)
|
|
1883
|
+
|
|
1884
|
+
@property
|
|
1885
|
+
def abs_top_true(self):
|
|
1886
|
+
# if self.pin_to_clip:
|
|
1887
|
+
# return self._abs_top()
|
|
1888
|
+
f = Core.melty.frame_count
|
|
1889
|
+
key = (f, self.top_offset_true, self.window_pos,
|
|
1890
|
+
self.anchor_pos, self.parent_anchor_pos, self.height)
|
|
1891
|
+
if self._abs_top_true_key == key:
|
|
1892
|
+
return self._abs_top_true_cache
|
|
1893
|
+
self._abs_top_true_key = key
|
|
1894
|
+
val = self._abs_top_true()
|
|
1895
|
+
self._abs_top_true_cache = val
|
|
1896
|
+
return val
|
|
1897
|
+
|
|
1898
|
+
def mark_column(self, column):
|
|
1899
|
+
self.max_column = max(self.max_column, column)
|
|
1900
|
+
|
|
1901
|
+
def shadow_depth_at(self, depth, active_layer):
|
|
1902
|
+
divisor = max(0.5, depth - 20.0)
|
|
1903
|
+
depth_and_layer = active_layer * Core.melty.max_depth + (depth * (20.0 / (divisor)))
|
|
1904
|
+
depth_and_layer *= Core.melty.layer_inc
|
|
1905
|
+
return depth_and_layer
|
|
1906
|
+
|
|
1907
|
+
@property
|
|
1908
|
+
def shadow_depth(self):
|
|
1909
|
+
depth, active_layer = self.depth_and_layer
|
|
1910
|
+
return shadow_depth_at(depth, active_layer)
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
@property
|
|
1914
|
+
def seen(self):
|
|
1915
|
+
debounce = 1
|
|
1916
|
+
return self.last_seen is not None and Core.melty.frame_count - self.last_seen < debounce
|
|
1917
|
+
|
|
1918
|
+
@property
|
|
1919
|
+
def window_index(self):
|
|
1920
|
+
nested_offset = self._nested_index if self.parent_window is not None else 0
|
|
1921
|
+
return self.abs_layer + nested_offset
|
|
1922
|
+
|
|
1923
|
+
# if self.closable:
|
|
1924
|
+
# layer_index = min(Melty.max_layer - 1, self.abs_layer + self._nested_index - 2)
|
|
1925
|
+
# elif self.parent_window is not None:
|
|
1926
|
+
# layer_index = min(Melty.max_layer - 1, self.abs_layer + self.parent_window._nested_index)
|
|
1927
|
+
# else:
|
|
1928
|
+
# layer_index = min(Melty.max_layer - 1, self.abs_layer)
|
|
1929
|
+
|
|
1930
|
+
# return layer_index
|
|
1931
|
+
|
|
1932
|
+
def init_cst_state(self, node, module_id: str):
|
|
1933
|
+
import libcst as cst # lazy: libcst takes ~80 ms to import
|
|
1934
|
+
self.cst = None
|
|
1935
|
+
self.cst.path_key = Core.melty.current_path()
|
|
1936
|
+
self.cst.module_id = module_id
|
|
1937
|
+
self.cst.root_gen = Core.melty.current_gen(module_id)
|
|
1938
|
+
# super light anchor for re-attachment later (customize as you like)
|
|
1939
|
+
if isinstance(node, cst.Name):
|
|
1940
|
+
self.cst.anchor = ("Name", node.value)
|
|
1941
|
+
elif isinstance(node, cst.Attribute):
|
|
1942
|
+
self.cst.anchor = ("Attr", node.attr.value)
|
|
1943
|
+
else:
|
|
1944
|
+
self.cst.anchor = (type(node).__name__,)
|
|
1945
|
+
|
|
1946
|
+
def get_resize_handle(self):
|
|
1947
|
+
if self.left is None or self.width is None or self.top is None or self.height is None:
|
|
1948
|
+
return (0, 0, 0, 0)
|
|
1949
|
+
left = self.left
|
|
1950
|
+
top = self.top
|
|
1951
|
+
right = left + self.width
|
|
1952
|
+
bottom = top + self.height + 2
|
|
1953
|
+
|
|
1954
|
+
margin = 20
|
|
1955
|
+
return (right - margin, bottom - margin, right, bottom)
|
|
1956
|
+
|
|
1957
|
+
def get_drag_mode(self):
|
|
1958
|
+
if self.auto_resize:
|
|
1959
|
+
return DragMode.WINDOW
|
|
1960
|
+
|
|
1961
|
+
mx, my = imgui.get_mouse_pos()
|
|
1962
|
+
rect_br = self.get_resize_handle()
|
|
1963
|
+
inside_br = (rect_br[0] <= mx <= rect_br[2] and rect_br[1] <= my <= rect_br[3])
|
|
1964
|
+
|
|
1965
|
+
# Bottom right
|
|
1966
|
+
if inside_br:
|
|
1967
|
+
return DragMode.RESIZE_BR
|
|
1968
|
+
|
|
1969
|
+
return DragMode.WINDOW
|
|
1970
|
+
|
|
1971
|
+
def is_glfw_mouse_hovering_rect(self, x1, y1, x2, y2):
|
|
1972
|
+
|
|
1973
|
+
global_mouse = glfw.get_cursor_pos(Core.melty.glfw_window)
|
|
1974
|
+
ox, oy = getattr(Core.melty, "frame_origin", None) or (0, 0) # shadow margin (+ shift) → content coords
|
|
1975
|
+
mx, my = global_mouse[0] - ox, global_mouse[1] - oy
|
|
1976
|
+
# basic collision check
|
|
1977
|
+
if x1 <= mx <= x2 and y1 <= my <= y2:
|
|
1978
|
+
return True
|
|
1979
|
+
return False
|
|
1980
|
+
|
|
1981
|
+
def get_rect(self):
|
|
1982
|
+
width = self.width or 0
|
|
1983
|
+
height = self.height or 0
|
|
1984
|
+
|
|
1985
|
+
top = self.top or 0
|
|
1986
|
+
left = self.left or 0
|
|
1987
|
+
|
|
1988
|
+
right = left + width
|
|
1989
|
+
bottom = top + height
|
|
1990
|
+
|
|
1991
|
+
is_outside = left >= right or top >= bottom
|
|
1992
|
+
if is_outside:
|
|
1993
|
+
return (0, 0, 0, 0)
|
|
1994
|
+
|
|
1995
|
+
return (left, top, right - 1, bottom - 1)
|
|
1996
|
+
|
|
1997
|
+
def draw_rect(self, rounding=0, tint=None, rect=None):
|
|
1998
|
+
if Core.melty.channels_split:
|
|
1999
|
+
draw_list = imgui.get_window_draw_list()
|
|
2000
|
+
draw_list.channels_set_current(Core.melty.get_channel() + 1)
|
|
2001
|
+
|
|
2002
|
+
if tint is None:
|
|
2003
|
+
tint = getattr(self._input_value, 'tint', None)
|
|
2004
|
+
|
|
2005
|
+
draw_list = imgui.get_overlay_draw_list()
|
|
2006
|
+
draw_list.add_rect(self.left, self.top, self.left + self.width, self.top + self.height,
|
|
2007
|
+
pack_color(*tint[:3], 1.0) if tint is not None else
|
|
2008
|
+
pack_color(1, 1, 1, 1),
|
|
2009
|
+
rounding=rounding, thickness=1)
|
|
2010
|
+
|
|
2011
|
+
if Core.melty.channels_split:
|
|
2012
|
+
draw_list = imgui.get_window_draw_list()
|
|
2013
|
+
draw_list.channels_set_current(Core.melty.get_channel())
|
|
2014
|
+
|
|
2015
|
+
def is_inside_clip(self, child_draw_state=None):
|
|
2016
|
+
clip_rect = self.abs_clip_rect
|
|
2017
|
+
|
|
2018
|
+
if child_draw_state is None:
|
|
2019
|
+
child_draw_state = self
|
|
2020
|
+
|
|
2021
|
+
if clip_rect is None:
|
|
2022
|
+
return True, False, False
|
|
2023
|
+
|
|
2024
|
+
clip_left, clip_top, clip_right, clip_bottom = clip_rect
|
|
2025
|
+
|
|
2026
|
+
if child_draw_state is not None:
|
|
2027
|
+
left = child_draw_state.abs_left
|
|
2028
|
+
top = child_draw_state.abs_top
|
|
2029
|
+
width = child_draw_state.width
|
|
2030
|
+
height = child_draw_state.height
|
|
2031
|
+
|
|
2032
|
+
if top is None or left is None:
|
|
2033
|
+
return True, False, False
|
|
2034
|
+
|
|
2035
|
+
if width is None or height is None:
|
|
2036
|
+
return True, False, False
|
|
2037
|
+
|
|
2038
|
+
if (top + height < clip_top or top > clip_bottom):
|
|
2039
|
+
if top > clip_bottom:
|
|
2040
|
+
return False, True, False
|
|
2041
|
+
else:
|
|
2042
|
+
return False, False, False
|
|
2043
|
+
return True, False, False
|
|
2044
|
+
|
|
2045
|
+
@property
|
|
2046
|
+
def abs_clamped_rect(self):
|
|
2047
|
+
clip_rect = self.abs_clip_rect
|
|
2048
|
+
if clip_rect is None:
|
|
2049
|
+
return (self.abs_left, self.abs_top, self.abs_left + self.width, self.abs_top + self.height)
|
|
2050
|
+
|
|
2051
|
+
clip_left, clip_top, clip_right, clip_bottom = clip_rect
|
|
2052
|
+
|
|
2053
|
+
left = max(self.abs_left, clip_left)
|
|
2054
|
+
top = max(self.abs_top, clip_top)
|
|
2055
|
+
right = min(self.abs_left + self.width, clip_right)
|
|
2056
|
+
bottom = min(self.abs_top + self.height, clip_bottom)
|
|
2057
|
+
|
|
2058
|
+
return (left, top, right, bottom)
|
|
2059
|
+
|
|
2060
|
+
def hover_eligible(self, rect=None, ignore_reports=True, clip_rect=None):
|
|
2061
|
+
# `clip_rect`: a caller that already read abs_clamped_rect this call
|
|
2062
|
+
# passes it in - on_action's registration computes it for the cursor
|
|
2063
|
+
# rect too, ~240 registrations a frame on the code editor window.
|
|
2064
|
+
if self.just_shadow:
|
|
2065
|
+
return False
|
|
2066
|
+
|
|
2067
|
+
if self.closed or not Core.melty.imgui_main_window_hovered:
|
|
2068
|
+
return False
|
|
2069
|
+
|
|
2070
|
+
if not self.inside_clip:
|
|
2071
|
+
return False
|
|
2072
|
+
|
|
2073
|
+
if rect is None:
|
|
2074
|
+
# Lean on the BVH: begin_frame already point-tested each view against
|
|
2075
|
+
# the cursor (Melty.bvh_hover_ids), O(1) membership replaces
|
|
2076
|
+
# imgui.is_mouse_hovering_rect on this view's own bbox.
|
|
2077
|
+
if id(self) not in Core.melty.bvh_hover_ids:
|
|
2078
|
+
return False
|
|
2079
|
+
# The BVH stores raw bboxes, not clipped ones, so still check the
|
|
2080
|
+
# cursor inside the active clip (scrolled-away views aren't hovered).
|
|
2081
|
+
if clip_rect is None:
|
|
2082
|
+
clip_rect = self.abs_clamped_rect
|
|
2083
|
+
if clip_rect is not None:
|
|
2084
|
+
mx, my = imgui.get_mouse_pos()
|
|
2085
|
+
if not (clip_rect[0] <= mx <= clip_rect[2] and clip_rect[1] <= my <= clip_rect[3]):
|
|
2086
|
+
return False
|
|
2087
|
+
else:
|
|
2088
|
+
# Custom sub-region: clip it and point-test the cursor directly.
|
|
2089
|
+
# Clamp to abs_clamped_rect (bbox ∩ abs clip), not just the bbox -
|
|
2090
|
+
# a view scrolled under its parent's header still has the header
|
|
2091
|
+
# band inside its bbox, and clipping only to the bbox let it report
|
|
2092
|
+
# hover there and steal the header's drag (window_move) event.
|
|
2093
|
+
if clip_rect is None:
|
|
2094
|
+
clip_rect = self.abs_clamped_rect
|
|
2095
|
+
if clip_rect is not None:
|
|
2096
|
+
rect = (max(rect[0], clip_rect[0]), max(rect[1], clip_rect[1]),
|
|
2097
|
+
min(rect[2], clip_rect[2]), min(rect[3], clip_rect[3]))
|
|
2098
|
+
mx, my = imgui.get_mouse_pos()
|
|
2099
|
+
if not (rect[0] <= mx <= rect[2] and rect[1] <= my <= rect[3]):
|
|
2100
|
+
return False
|
|
2101
|
+
|
|
2102
|
+
if not (self.hover_reported is None or self.hover_reported or ignore_reports):
|
|
2103
|
+
return False
|
|
2104
|
+
return True
|
|
2105
|
+
|
|
2106
|
+
def _action_base_priority(self, z_pos=None):
|
|
2107
|
+
"""on_action's default priority: the wrapper's z ordering inverted
|
|
2108
|
+
(0 = topmost). `z_pos=None` reads Melty's live layer/depth — what a
|
|
2109
|
+
call from inside this view's body sees."""
|
|
2110
|
+
max_layer_depth = Core.melty.max_depth * Core.melty.max_depth + Core.melty.max_depth
|
|
2111
|
+
if z_pos is None:
|
|
2112
|
+
z_pos = Core.melty.paint_rank * Core.melty.max_depth + Core.melty.depth
|
|
2113
|
+
return max_layer_depth - z_pos
|
|
2114
|
+
|
|
2115
|
+
def _register_action(self, view_id, event_names, registered_priority, rect, cursor,
|
|
2116
|
+
debug_priority=0, debug_delta=0, cursor_gate=None):
|
|
2117
|
+
"""The registration half of on_action: hover-test `rect` (None = this
|
|
2118
|
+
view's bbox) and subscribe. Shared by the live call and the cache-hit
|
|
2119
|
+
replay so both obey the same z-order / blocker rules."""
|
|
2120
|
+
if rect is not None:
|
|
2121
|
+
# Cheapest test first: ~330 registrations a frame on the code
|
|
2122
|
+
# editor window (frame edges, scrollbars, buttons, dials), and the
|
|
2123
|
+
# pointer is inside a handful of them. hover_eligible clips the
|
|
2124
|
+
# rect before calling, and the clip can only shrink it, so a
|
|
2125
|
+
# pointer outside the input rect is outside the clipped one too -
|
|
2126
|
+
# skip the priority / clamped-rect work on those.
|
|
2127
|
+
mx, my = imgui.get_mouse_pos()
|
|
2128
|
+
if not (rect[0] <= mx <= rect[2] and rect[1] <= my <= rect[3]):
|
|
2129
|
+
return
|
|
2130
|
+
if not self.hover_eligible(rect):
|
|
2131
|
+
return
|
|
2132
|
+
cursor_rect = None
|
|
2133
|
+
if cursor is not None:
|
|
2134
|
+
# The input handler re-tests this rect against the LATEST pointer
|
|
2135
|
+
# position when it changes the shape (gl_gui/mouse_cursor.py), so
|
|
2136
|
+
# a slow frame can't hold the I-beam after the pointer has left.
|
|
2137
|
+
cursor_rect = self.abs_clamped_rect
|
|
2138
|
+
if rect is not None and cursor_rect is not None:
|
|
2139
|
+
cursor_rect = (max(rect[0], cursor_rect[0]), max(rect[1], cursor_rect[1]),
|
|
2140
|
+
min(rect[2], cursor_rect[2]), min(rect[3], cursor_rect[3]))
|
|
2141
|
+
elif rect is not None:
|
|
2142
|
+
cursor_rect = tuple(rect)
|
|
2143
|
+
Core.melty.event_handler.register_hovered(view_id, event_names,
|
|
2144
|
+
priority=registered_priority,
|
|
2145
|
+
tile_id=self._tile_id, cursor=cursor,
|
|
2146
|
+
cursor_rect=cursor_rect, cursor_gate=cursor_gate)
|
|
2147
|
+
|
|
2148
|
+
overlay = imgui.get_overlay_draw_list()
|
|
2149
|
+
overlay.channels_set_current(Core.melty.max_layer - 1)
|
|
2150
|
+
if Toggles.InputHandlerToggles.show_debug:
|
|
2151
|
+
for name in event_names:
|
|
2152
|
+
self._event_names.add(name)
|
|
2153
|
+
|
|
2154
|
+
ds = self
|
|
2155
|
+
color = (1,1,1)
|
|
2156
|
+
text = f"zpos:{self.layer} priority:{debug_priority} priority_delta:{debug_delta} | {str(self._event_names)}"
|
|
2157
|
+
invalidation_rect = (ds.abs_left, ds.abs_top,
|
|
2158
|
+
ds.abs_left + (ds.width or 0),
|
|
2159
|
+
ds.abs_top + (ds.height or 0))
|
|
2160
|
+
text_size = imgui.calc_text_size(text)
|
|
2161
|
+
overlay.add_rect_filled(invalidation_rect[0] + ds.width - text_size.x, invalidation_rect[1],
|
|
2162
|
+
invalidation_rect[0] + ds.width,
|
|
2163
|
+
invalidation_rect[1] + text_size.y,
|
|
2164
|
+
pack_color(*color[:3],
|
|
2165
|
+
1))
|
|
2166
|
+
|
|
2167
|
+
overlay.add_text(invalidation_rect[0] + ds.width - text_size.x, invalidation_rect[1],
|
|
2168
|
+
pack_color(*(0, 0, 0),
|
|
2169
|
+
1),
|
|
2170
|
+
text)
|
|
2171
|
+
|
|
2172
|
+
overlay.add_rect(invalidation_rect[0], invalidation_rect[1], invalidation_rect[2],
|
|
2173
|
+
invalidation_rect[3],
|
|
2174
|
+
pack_color(*color[:3],
|
|
2175
|
+
1),
|
|
2176
|
+
thickness=1.0)
|
|
2177
|
+
|
|
2178
|
+
def replay_body_actions(self):
|
|
2179
|
+
"""Blit-cache hit: the body did not run, so re-issue every on_action
|
|
2180
|
+
it made on its last real render (see on_action). Rects re-anchor to
|
|
2181
|
+
the LIVE abs position (the cached property lags a blit-served drag)
|
|
2182
|
+
and priorities to the current z_pos, then go through the normal
|
|
2183
|
+
hover / blocker gauntlet — a covered or un-hovered rect registers
|
|
2184
|
+
nothing, exactly as the live call would."""
|
|
2185
|
+
record = self._body_actions
|
|
2186
|
+
if not record or not record[1]:
|
|
2187
|
+
return
|
|
2188
|
+
if self.just_shadow or not Core.melty.inside_clip(draw_state=self):
|
|
2189
|
+
return
|
|
2190
|
+
base = self._action_base_priority(z_pos=self.z_pos)
|
|
2191
|
+
left, top = self._abs_left(), self._abs_top()
|
|
2192
|
+
for view_suffix, event_names, offset, relative_rect, cursor, cursor_gate in record[1]:
|
|
2193
|
+
view_id = self._tile_id if view_suffix is None else str(self._tile_id) + "_" + str(view_suffix)
|
|
2194
|
+
rect = None
|
|
2195
|
+
if relative_rect is not None:
|
|
2196
|
+
rect = (left + relative_rect[0], top + relative_rect[1],
|
|
2197
|
+
left + relative_rect[2], top + relative_rect[3])
|
|
2198
|
+
self._register_action(view_id, list(event_names), base + offset, rect, cursor,
|
|
2199
|
+
debug_priority=base + offset, cursor_gate=cursor_gate)
|
|
2200
|
+
|
|
2201
|
+
@property
|
|
2202
|
+
def priority(self):
|
|
2203
|
+
max_layer_depth = (Core.melty.max_depth *
|
|
2204
|
+
Core.melty.max_layer + Core.melty.max_depth)
|
|
2205
|
+
layer_and_depth = (Core.melty.paint_rank *
|
|
2206
|
+
Core.melty.max_depth + Core.melty.depth)
|
|
2207
|
+
return max_layer_depth - layer_and_depth
|
|
2208
|
+
|
|
2209
|
+
def get_content_rect(self):
|
|
2210
|
+
# An inline header shares the value row; it consumes horizontal space.
|
|
2211
|
+
header_height = 0 if self._kwargs.get('header_same_line', False) else self.header_height
|
|
2212
|
+
return (self.abs_left, self.abs_top + header_height,
|
|
2213
|
+
self.abs_left + self.width, self.abs_top + self.height - self.footer_height)
|
|
2214
|
+
|
|
2215
|
+
def get_header_rect(self):
|
|
2216
|
+
# header_width is the measured width of the drawn header, so the drag
|
|
2217
|
+
# handle covers only the label band; 0 (no header drawn / not yet
|
|
2218
|
+
# measured) falls back to the full view width.
|
|
2219
|
+
right = self.abs_left + (self.header_width if self.header_width else self.width)
|
|
2220
|
+
return (self.abs_left, self.abs_top, right, self.abs_top + self.header_height)
|
|
2221
|
+
|
|
2222
|
+
# Transient per-draw_state UI state that should travel with a value undo, so
|
|
2223
|
+
# undoing a text edit also restores the caret/selection/scroll to where they
|
|
2224
|
+
# were before the edit. Snapshotting is generic (just attribute names): a
|
|
2225
|
+
# widget that wants more state restored on undo adds its field names, with no
|
|
2226
|
+
# changes to core_render or the renderer itself.
|
|
2227
|
+
UNDO_STATE_FIELDS = (
|
|
2228
|
+
"text_cursor_pos", "text_prev_cursor_pos",
|
|
2229
|
+
"text_selection_start", "text_selection_end", "text_h_scroll",
|
|
2230
|
+
)
|
|
2231
|
+
|
|
2232
|
+
def capture_undo_state(self):
|
|
2233
|
+
"""Snapshot the undo-tracked transient fields as a name->value dict."""
|
|
2234
|
+
return {f: getattr(self, f) for f in self.UNDO_STATE_FIELDS}
|
|
2235
|
+
|
|
2236
|
+
def apply_undo_state(self, snapshot):
|
|
2237
|
+
"""Restore a snapshot produced by capture_undo_state()."""
|
|
2238
|
+
if not snapshot:
|
|
2239
|
+
return
|
|
2240
|
+
for f, v in snapshot.items():
|
|
2241
|
+
setattr(self, f, v)
|
|
2242
|
+
|
|
2243
|
+
def get_action(self, event_name, view_id=None):
|
|
2244
|
+
"""Read an already-delivered event without registering a hit region.
|
|
2245
|
+
|
|
2246
|
+
Geometry owners consume input, resolve their rectangle, then subscribe
|
|
2247
|
+
with on_action using that final rectangle for the next input dispatch.
|
|
2248
|
+
"""
|
|
2249
|
+
identity = self._tile_id if view_id is None else f"{self._tile_id}_{view_id}"
|
|
2250
|
+
return Core.melty.events.get(identity, {}).get(event_name)
|
|
2251
|
+
|
|
2252
|
+
def on_action(self, event_names, view_id=None, priority=None, priority_delta=0, rect=None, cursor=None,
|
|
2253
|
+
cursor_gate=None):
|
|
2254
|
+
"""Subscribe this view to `event_names` (or, with an empty list, just
|
|
2255
|
+
tag `rect` with a pointer `cursor` shape) for this frame.
|
|
2256
|
+
`cursor_gate="left_mouse_dragged"` shows the shape only where this
|
|
2257
|
+
subscription is the one that event would be captured by
|
|
2258
|
+
(InputHandler.register_hovered) — a drag handle's shape.
|
|
2259
|
+
|
|
2260
|
+
Called from a view BODY it is recorded on the draw_state as well
|
|
2261
|
+
(`_body_actions`), because the wrapper skips the body on a blit-cache
|
|
2262
|
+
hit and InputHandler forgets every subscription each frame — without
|
|
2263
|
+
the record a cached tile could not latch a drag or keep its I-beam
|
|
2264
|
+
(`replay_body_actions` re-issues the record on a hit). Recording only
|
|
2265
|
+
happens for the frame the wrapper opened the record in, so pre-gate
|
|
2266
|
+
calls on cached frames never pile up."""
|
|
2267
|
+
if self.parent_window is None and not self.closable:
|
|
2268
|
+
priority_delta -= 1
|
|
2269
|
+
|
|
2270
|
+
if not Core.melty.inside_clip(draw_state=self):
|
|
2271
|
+
return None
|
|
2272
|
+
if self.just_shadow:
|
|
2273
|
+
return None
|
|
2274
|
+
view_suffix = view_id
|
|
2275
|
+
if view_id is None:
|
|
2276
|
+
view_id = self._tile_id
|
|
2277
|
+
else:
|
|
2278
|
+
view_id = str(self._tile_id) + "_" + str(view_id)
|
|
2279
|
+
|
|
2280
|
+
single_event = False
|
|
2281
|
+
if isinstance(event_names, str):
|
|
2282
|
+
event_names = [event_names]
|
|
2283
|
+
single_event = True
|
|
2284
|
+
|
|
2285
|
+
if priority is None:
|
|
2286
|
+
priority = self._action_base_priority()
|
|
2287
|
+
registered_priority = priority - priority_delta
|
|
2288
|
+
|
|
2289
|
+
record = self._body_actions
|
|
2290
|
+
if record is not None and record[0] == Core.melty.frame_count:
|
|
2291
|
+
# Priority is kept as an offset from this view's z_pos and the rect
|
|
2292
|
+
# relative to its abs position: both are re-derived on replay, so a
|
|
2293
|
+
# view that moved (blit-served drag) or changed layer replays right.
|
|
2294
|
+
if rect is None:
|
|
2295
|
+
relative_rect = None
|
|
2296
|
+
else:
|
|
2297
|
+
left, top = self.abs_left, self.abs_top
|
|
2298
|
+
relative_rect = (rect[0] - left, rect[1] - top, rect[2] - left, rect[3] - top)
|
|
2299
|
+
record[1].append((view_suffix, tuple(event_names),
|
|
2300
|
+
registered_priority - self._action_base_priority(z_pos=self.z_pos),
|
|
2301
|
+
relative_rect, cursor, cursor_gate))
|
|
2302
|
+
|
|
2303
|
+
self._register_action(view_id, event_names, registered_priority, rect, cursor,
|
|
2304
|
+
debug_priority=priority, debug_delta=priority_delta,
|
|
2305
|
+
cursor_gate=cursor_gate)
|
|
2306
|
+
|
|
2307
|
+
if single_event:
|
|
2308
|
+
return self.get_action(event_names[0], view_id=view_suffix)
|
|
2309
|
+
|
|
2310
|
+
return_events = {}
|
|
2311
|
+
if view_id in Core.melty.events:
|
|
2312
|
+
for event_name in Core.melty.events[view_id]:
|
|
2313
|
+
return_events[event_name] = Core.melty.events[view_id][event_name]
|
|
2314
|
+
return return_events
|
|
2315
|
+
|
|
2316
|
+
def event_rect(self, event_names, rect):
|
|
2317
|
+
"""Narrow a declared event PARAM to `rect` (screen coords) — the hit
|
|
2318
|
+
area the wrapper registers it over, instead of the whole content
|
|
2319
|
+
rect. Call it from the view body every run, next to the geometry
|
|
2320
|
+
that defines the rect; several calls for the same name union their
|
|
2321
|
+
rects. Outside the rects the param is simply not subscribed, so a
|
|
2322
|
+
press/drag there falls through to whatever sits behind this view in
|
|
2323
|
+
z-order — the enclosing window's move handle, a parent's scroll —
|
|
2324
|
+
with no forwarding code. draw_text scopes `left_mouse_drag` to its
|
|
2325
|
+
text rows this way so the gutter and the space below the last line
|
|
2326
|
+
drag the window. Rects are stored relative to the view origin (a
|
|
2327
|
+
blit-served window drag moves the view without re-running the body)
|
|
2328
|
+
and re-anchored at registration; the wrapper reads the record of the
|
|
2329
|
+
LAST body run when it registers the params for this one, then
|
|
2330
|
+
clears it, so a run that stops declaring lifts the scope. Not the
|
|
2331
|
+
tool for an EXTRA sub-rect the body wants events from — that is
|
|
2332
|
+
`on_action(event, view_id=…, rect=…)`."""
|
|
2333
|
+
if isinstance(event_names, str):
|
|
2334
|
+
event_names = (event_names,)
|
|
2335
|
+
left, top = self.abs_left, self.abs_top
|
|
2336
|
+
if left is None or top is None:
|
|
2337
|
+
return
|
|
2338
|
+
relative_rect = (rect[0] - left, rect[1] - top, rect[2] - left, rect[3] - top)
|
|
2339
|
+
record = self._event_rects
|
|
2340
|
+
if record is None:
|
|
2341
|
+
record = self._event_rects = {}
|
|
2342
|
+
for name in event_names:
|
|
2343
|
+
record.setdefault(name, []).append(relative_rect)
|
|
2344
|
+
|
|
2345
|
+
def is_bounding_hovered(self):
|
|
2346
|
+
if (self._imgui_is_active or self._imgui_is_edited or self._imgui_is_item_hovered or self._imgui_popover_open):
|
|
2347
|
+
return True
|
|
2348
|
+
|
|
2349
|
+
if self.top is None or self.left is None or self.width is None or self.height is None:
|
|
2350
|
+
return False
|
|
2351
|
+
|
|
2352
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
2353
|
+
if not Core.melty.inside_clip(rect=(mouse_x, mouse_y, 1, 1)):
|
|
2354
|
+
return False
|
|
2355
|
+
|
|
2356
|
+
if not (Core.melty.imgui_main_window_hovered or Core.melty.imgui_popup_open):
|
|
2357
|
+
return False
|
|
2358
|
+
|
|
2359
|
+
# Cursor-over-this-view comes from the BVH hit test, not is_mouse_hovering_rect.
|
|
2360
|
+
return id(self) in Core.melty.bvh_hover_ids
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
class KeyMod(Enum):
|
|
2364
|
+
CTRL = 'ctrl'
|
|
2365
|
+
ALT = 'alt'
|
|
2366
|
+
SHIFT = 'shift'
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
class Hotkey:
|
|
2370
|
+
def __init__(self, key=None, name="", mod=None, scoped=True):
|
|
2371
|
+
self.name = name
|
|
2372
|
+
self.key = key
|
|
2373
|
+
self.mod = mod
|
|
2374
|
+
self.scoped = scoped
|
|
2375
|
+
|
|
2376
|
+
# hashing and equality based on key and mod only
|
|
2377
|
+
def __hash__(self):
|
|
2378
|
+
return hash((self.key, self.mod))
|
|
2379
|
+
|
|
2380
|
+
def __eq__(self, other):
|
|
2381
|
+
if not isinstance(other, Hotkey):
|
|
2382
|
+
return False
|
|
2383
|
+
return self.key == other.key and self.mod == other.mod
|
|
2384
|
+
|
|
2385
|
+
def mod_active(self):
|
|
2386
|
+
if self.mod == KeyMod.CTRL:
|
|
2387
|
+
return imgui.get_io().key_ctrl
|
|
2388
|
+
elif self.mod == KeyMod.ALT:
|
|
2389
|
+
return imgui.get_io().key_alt
|
|
2390
|
+
elif self.mod == KeyMod.SHIFT:
|
|
2391
|
+
return imgui.get_io().key_shift
|
|
2392
|
+
|
|
2393
|
+
if self.mod is None:
|
|
2394
|
+
return True
|