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,2677 @@
|
|
|
1
|
+
"""Code view functions and supporting definitions."""
|
|
2
|
+
from inspect import Parameter
|
|
3
|
+
from meltygui.code.fileref import Address
|
|
4
|
+
from meltygui.code.libcst_conversion import Comment
|
|
5
|
+
from meltygui.code.libcst_conversion import SymbolUsage
|
|
6
|
+
from meltygui.code.libcst_conversion import UsageRef
|
|
7
|
+
from meltygui.core.styling.fonts import Font
|
|
8
|
+
from meltygui.hdr_color import pack_color
|
|
9
|
+
from meltygui.core.melty import Melty
|
|
10
|
+
from meltygui.model.code_model import UsagePickerModel
|
|
11
|
+
from meltygui.core.rendering.modes import Modes
|
|
12
|
+
from meltygui.core.core_render import render_func
|
|
13
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
14
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
15
|
+
from meltygui.state.code_state import SourcePreviewState
|
|
16
|
+
from meltygui.state.new_core_model import Anchor
|
|
17
|
+
from meltygui.state.new_core_model import DrawState
|
|
18
|
+
from meltygui.state.new_core_model import ExpandMode
|
|
19
|
+
from meltygui.state.new_core_model import Pin
|
|
20
|
+
from meltygui.state.new_core_model import TabState
|
|
21
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
22
|
+
from meltygui.view.header_view import draw_footer
|
|
23
|
+
from meltygui.view.header_view import draw_header
|
|
24
|
+
from meltygui_imgui.core import _DrawList
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import bisect
|
|
27
|
+
import collections
|
|
28
|
+
import inspect
|
|
29
|
+
import meltygui_imgui as imgui
|
|
30
|
+
import sys
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
import types
|
|
34
|
+
import weakref
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@render_func(use_cache=True, selectable=False)
|
|
38
|
+
def run_button(input_value: any, with_kwargs=None, draw_state=None, clicked=False):
|
|
39
|
+
from meltygui.core.conversion.path_finder import Pending
|
|
40
|
+
|
|
41
|
+
is_render_func = hasattr(input_value, "__render_func__")
|
|
42
|
+
if not is_render_func:
|
|
43
|
+
imgui.text_colored(f"Value of type {type(input_value).__name__} needs @render_func",
|
|
44
|
+
1.0, 0.5, 0.0)
|
|
45
|
+
return False, None
|
|
46
|
+
if with_kwargs is None:
|
|
47
|
+
with_kwargs = {}
|
|
48
|
+
|
|
49
|
+
if hasattr(input_value, "__header_defaults__"):
|
|
50
|
+
run_in_background = input_value.__header_defaults__.get("background", False)
|
|
51
|
+
else:
|
|
52
|
+
run_in_background = True
|
|
53
|
+
|
|
54
|
+
running = draw_state._running is input_value if run_in_background else False
|
|
55
|
+
|
|
56
|
+
fa_run_arrow = ""
|
|
57
|
+
from meltygui.view.control_view import button
|
|
58
|
+
if clicked or running or button(f"{fa_run_arrow} {input_value.__name__}##{draw_state.unique}",
|
|
59
|
+
height=30, draw=True, value=0.4, saturation=1.5,
|
|
60
|
+
name=f"{input_value.__name__}{draw_state.unique}_run")[0]:
|
|
61
|
+
with_kwargs['changed'] = True
|
|
62
|
+
changed, value = input_value(**with_kwargs)
|
|
63
|
+
if isinstance(value, Pending):
|
|
64
|
+
draw_state._running = input_value
|
|
65
|
+
return False, None
|
|
66
|
+
|
|
67
|
+
draw_state._running = False
|
|
68
|
+
return True, (changed, value)
|
|
69
|
+
|
|
70
|
+
return False, None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@render_func(use_cache=True, show_bg=False, selectable=False, disable_scroll=True,
|
|
74
|
+
shadow=False, indent_size=0, with_footer=None, fill_height=False)
|
|
75
|
+
def draw_with_view_funcs(input_value, view_funcs, route, routed, route_to_kwargs,
|
|
76
|
+
tab_state: TabState, unique, column_widths=None,
|
|
77
|
+
draw=False, draw_state=None, **kwargs):
|
|
78
|
+
"""Tab strip + columns half of the old draw_modes.
|
|
79
|
+
|
|
80
|
+
Shows a tab per entry in `view_funcs` and renders the selected ones side by
|
|
81
|
+
side. The shared `routed` payload (built by convert_in_and_out) decides each
|
|
82
|
+
column's input: a view with a `route` entry is fed routed[route[view]] (e.g.
|
|
83
|
+
draw_collection <- the parsed dict); a view with no entry edits the raw text
|
|
84
|
+
(draw_text <- input_value). Every routed value also rides in as a kwarg, so a
|
|
85
|
+
view picks up the extras it wants (draw_text <- jump_to / error / code_tree)
|
|
86
|
+
WITHOUT convert_in_and_out hand-threading them — that's the routing.
|
|
87
|
+
|
|
88
|
+
Two edit channels flow back to convert_in_and_out:
|
|
89
|
+
* a RAW text edit is returned directly as (changed, value);
|
|
90
|
+
* a CONVERTED edit (a routed/structured view) is stashed on the shared
|
|
91
|
+
`routed` dict under 'converted_edit', because it must go back through
|
|
92
|
+
chain_out before it becomes text. It's written AFTER the loop so the
|
|
93
|
+
columns in this same frame never see it as an input kwarg."""
|
|
94
|
+
from meltygui.code.new_converters import UNSET
|
|
95
|
+
|
|
96
|
+
# Drop entries that didn't survive (de)serialization, then default to the
|
|
97
|
+
# first two views (text | structured) like the old draw_modes did.
|
|
98
|
+
tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t is not None]
|
|
99
|
+
if not tab_state.selected_tabs:
|
|
100
|
+
tab_state.selected_tabs = view_funcs[:2] or view_funcs[:1]
|
|
101
|
+
|
|
102
|
+
imgui.dummy(0, 5)
|
|
103
|
+
names = [getattr(vf, '__name__', str(vf)) for vf in view_funcs]
|
|
104
|
+
tab_changed, new_tabs = RenderFuncs.draw_tab_bar(input_value=tab_state.selected_tabs,
|
|
105
|
+
tab_height=30, show_bg=False, bg_offset=1,
|
|
106
|
+
name=f"tab_bar{unique}", names=names,
|
|
107
|
+
collection=view_funcs, as_toggles=False)
|
|
108
|
+
if tab_changed:
|
|
109
|
+
tab_state.selected_tabs = new_tabs
|
|
110
|
+
|
|
111
|
+
imgui.dummy(0, 2)
|
|
112
|
+
raw_changed, raw_value = False, input_value
|
|
113
|
+
converted_edit = UNSET
|
|
114
|
+
for idx, view_func in enumerate(tab_state.selected_tabs):
|
|
115
|
+
if column_widths is not None and len(column_widths) > idx:
|
|
116
|
+
column_width = column_widths[idx]
|
|
117
|
+
else:
|
|
118
|
+
column_width = None
|
|
119
|
+
|
|
120
|
+
# A view with a route entry consumes a routed (converted) value; one
|
|
121
|
+
# without edits the raw text. Falls back to the last-good routed value
|
|
122
|
+
# (already merged into `routed`), or UNSET if it never parsed.
|
|
123
|
+
uses_converted = route is not None and view_func in route
|
|
124
|
+
if uses_converted:
|
|
125
|
+
view_input = routed.get(route[view_func], UNSET)
|
|
126
|
+
else:
|
|
127
|
+
view_input = input_value
|
|
128
|
+
|
|
129
|
+
# route_to_kwargs_this = route_to_kwargs.get(view_func, {})
|
|
130
|
+
# for k in route_to_kwargs_this:
|
|
131
|
+
# arg_name = route_to_kwargs_this[k]
|
|
132
|
+
# if arg_name in routed:
|
|
133
|
+
# routed[k] = routed[arg_name]
|
|
134
|
+
|
|
135
|
+
# routed = {**routed, **{k: routed.get(k) for k in route_to_kwargs_this}}
|
|
136
|
+
|
|
137
|
+
# `draw=` (the external trigger flag) bypasses the view's cache for one
|
|
138
|
+
# redraw WITHOUT setting any sticky edit flags - never pass it as
|
|
139
|
+
# `changed=`, or a forced redraw comes back reported as an edit and, with
|
|
140
|
+
# auto_save, spins an endless reload -> redraw -> save.
|
|
141
|
+
if len(tab_state.selected_tabs) == 1:
|
|
142
|
+
column = None
|
|
143
|
+
m_changed, m_out = view_func(input_value=view_input, excluded=["__cst__", "__origin__"],
|
|
144
|
+
show_system=False, draw=draw, max_width=draw_state.content_width - 10,
|
|
145
|
+
disable_scroll=False, show_header=False,
|
|
146
|
+
column=idx, column_width=column_width,
|
|
147
|
+
show_add_delete=False, name=f"{view_func.__name__}##{unique}",
|
|
148
|
+
selectable=False, **routed)
|
|
149
|
+
if not m_changed:
|
|
150
|
+
continue
|
|
151
|
+
if draw_state is not None:
|
|
152
|
+
draw_state.invalidate_up(max_depth=3)
|
|
153
|
+
if uses_converted:
|
|
154
|
+
# Keep only the latest converted edit so a continuous drag collapses
|
|
155
|
+
# into one chain_out call when it settles.
|
|
156
|
+
converted_edit = m_out
|
|
157
|
+
else:
|
|
158
|
+
raw_changed, raw_value = True, m_out
|
|
159
|
+
|
|
160
|
+
if converted_edit is not UNSET:
|
|
161
|
+
routed['converted_edit'] = converted_edit
|
|
162
|
+
return raw_changed, raw_value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def draw_text_from_code_cache(input_value=None, root_input=None, error=None,
|
|
166
|
+
run_jedi=False, **kwargs):
|
|
167
|
+
"""FILE_TREE's editor view: plain draw_text fed from the shared code-host
|
|
168
|
+
cache instead of an inline chain. code_hosts_for(path) owns the parse —
|
|
169
|
+
str_host watches the file, dict_host re-parses on change — and we pull its
|
|
170
|
+
held cst_dict each frame, so usage links and syntax-error highlighting
|
|
171
|
+
arrive as code_dict / code_tree exactly as in the NEW_CODE routes. Pull-
|
|
172
|
+
based by design: an edit here auto-saves to disk, the cache's str_host
|
|
173
|
+
reloads from the file, and the editor picks up the fresh parse a beat
|
|
174
|
+
later — the leaf and the cache only ever talk through the file."""
|
|
175
|
+
from meltygui.code.new_converters import ModesState
|
|
176
|
+
from meltygui.code.new_converters import _ensure_symbol_index
|
|
177
|
+
from meltygui.code.new_converters import _error_markers
|
|
178
|
+
from meltygui.code.new_converters import _host_label
|
|
179
|
+
from meltygui.code.new_converters import _host_relint_and_fixes
|
|
180
|
+
from meltygui.code.new_converters import code_hosts_for
|
|
181
|
+
from meltygui.core.diagnostics.perf_trace import once as _ponce
|
|
182
|
+
from meltygui.core.diagnostics.perf_trace import trace as _ptrace
|
|
183
|
+
from meltygui.core.diagnostics.perf_trace import trace_rl as _ptrace_rl
|
|
184
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
185
|
+
|
|
186
|
+
code_dict, cache_error, dict_host = None, None, None
|
|
187
|
+
# Scope-up auto-select (contextual func tab): a file-ABSOLUTE line whose
|
|
188
|
+
# statement should be selected in this editor - consumed here (popped so it
|
|
189
|
+
# never leaks into draw_text as a stray kwarg) and applied one-shot below.
|
|
190
|
+
# select_seq is the arrow-press generation: it keys the one-shot, so every
|
|
191
|
+
# click re-selects even when the same line repeats in a persisted ds.
|
|
192
|
+
select_line = kwargs.pop("select_line", None)
|
|
193
|
+
select_seq = kwargs.pop("select_seq", 0)
|
|
194
|
+
_t_editor0 = time.monotonic()
|
|
195
|
+
if root_input is not None:
|
|
196
|
+
_str_host, dict_host = code_hosts_for(root_input)
|
|
197
|
+
code_dict = dict_host._held()
|
|
198
|
+
# First-parse arrival transition (once per host): the gap between
|
|
199
|
+
# "waiting" and "visible" is what the user sees as load time.
|
|
200
|
+
if isinstance(code_dict, dict):
|
|
201
|
+
if _ponce(("parse-visible", _host_label(dict_host))):
|
|
202
|
+
_ptrace("editor: first parse visible", host=_host_label(dict_host))
|
|
203
|
+
elif _ponce(("parse-wait", _host_label(dict_host))):
|
|
204
|
+
_ptrace("editor: waiting for first parse", host=_host_label(dict_host))
|
|
205
|
+
# Background auto-index: keep the held parse's symbol usages current
|
|
206
|
+
# without the manual Index click (no-op when already indexed).
|
|
207
|
+
|
|
208
|
+
_ensure_symbol_index(dict_host, _str_host, code_dict, kwargs.get("jump_to"))
|
|
209
|
+
# The chain's parse error lives on the wrapper's injected ModesState.
|
|
210
|
+
# Normalize it to the ParseError-dict shape _code_tree_errors reads
|
|
211
|
+
# ({'__error__','__line__'}) and pass it as code_tree: the raw exception
|
|
212
|
+
# could be a cst.ParserSyntaxError, whose line lives in `raw_line` - a
|
|
213
|
+
# name neither _code_tree_errors nor _exception_handler knows. code_dict
|
|
214
|
+
# keeps the last GOOD parse alongside, so the editor highlights the
|
|
215
|
+
# offending line without losing its structure.
|
|
216
|
+
wds = getattr(dict_host, "_wrapper_draw_state", None)
|
|
217
|
+
import_fixes = _host_relint_and_fixes(dict_host, _str_host, wds)
|
|
218
|
+
for v in (getattr(wds, "misc", None) or {}).values():
|
|
219
|
+
if isinstance(v, ModesState):
|
|
220
|
+
err = v.last_error
|
|
221
|
+
lint = getattr(v, "last_lint", None) or None
|
|
222
|
+
if err is not None or lint:
|
|
223
|
+
# ONE dict per underlying (exception, lint) pair, not one
|
|
224
|
+
# per frame: draw_text's parse-error staleness check
|
|
225
|
+
# compares code_tree by IDENTITY to detect "a fresh parse
|
|
226
|
+
# landed", so a dict rebuilt every frame would re-hide a
|
|
227
|
+
# stale highlight one frame after an edit, pinned to the
|
|
228
|
+
# old line. The memo keeps identity stable until the
|
|
229
|
+
# background reparse actually replaces last_error /
|
|
230
|
+
# last_lint (both swapped per completed parse, never
|
|
231
|
+
# mutated in place).
|
|
232
|
+
memo = getattr(dict_host, "_err_view_memo", None)
|
|
233
|
+
if memo is not None and memo[0] is err and memo[1] is lint:
|
|
234
|
+
cache_error = memo[2]
|
|
235
|
+
else:
|
|
236
|
+
# The parse/compile error, then the lint findings -
|
|
237
|
+
# all in __errors__, with the first mirrored into the
|
|
238
|
+
# single __error__/__line__ keys the editors use.
|
|
239
|
+
markers = _error_markers(err, lint)
|
|
240
|
+
cache_error = {"__error__": markers[0][1], "__line__": markers[0][0],
|
|
241
|
+
"__errors__": markers}
|
|
242
|
+
dict_host._err_view_memo = (err, lint, cache_error)
|
|
243
|
+
# The Index button's pulse rides to the cache's chain_in (one-shot:
|
|
244
|
+
# cleared again on the next un-pulsed frame). cst_module_to_dict only
|
|
245
|
+
# runs jedi when it ALSO has the resolved address (jump_to) - the leaf's
|
|
246
|
+
# code_file_io hands us the whole-file Address, so forward it alongside.
|
|
247
|
+
# _pending_external makes the host re-run chain_in (its start gate is
|
|
248
|
+
# external_change); invalidating alone would just replay the blit.
|
|
249
|
+
if run_jedi:
|
|
250
|
+
dict_host.child_kwargs["run_jedi"] = True
|
|
251
|
+
if kwargs.get("jump_to") is not None:
|
|
252
|
+
dict_host.child_kwargs["jump_to"] = kwargs["jump_to"]
|
|
253
|
+
dict_host._pending_external = True
|
|
254
|
+
for hds in (wds, getattr(dict_host, "_draw_state", None)):
|
|
255
|
+
if hds is not None:
|
|
256
|
+
hds.invalidate()
|
|
257
|
+
request_render()
|
|
258
|
+
elif (dict_host.child_kwargs.get("run_jedi")
|
|
259
|
+
and not dict_host._pending_external):
|
|
260
|
+
# One-shot clear - but only after the host actually consumed the
|
|
261
|
+
# pulse (_pending_external drops when its chain_in is handed the
|
|
262
|
+
# kwargs). Popping earlier loses a click whenever this editor
|
|
263
|
+
# re-renders between the pulse frame and the host's next draw.
|
|
264
|
+
dict_host.child_kwargs.pop("run_jedi", None)
|
|
265
|
+
|
|
266
|
+
held_values = list(_str_host.values())
|
|
267
|
+
from_host = len(held_values) > 0
|
|
268
|
+
# Short-circuit the first-parse wait: the code_dict pair materializes
|
|
269
|
+
# through the full cst→dict chain, and until it lands this editor
|
|
270
|
+
# rendered NOTHING - that gap IS the perceived load time of a code
|
|
271
|
+
# buffer ("editor: waiting for first parse" above). The codec-loaded
|
|
272
|
+
# buffer (input_value) is available the frame load_file lands, so draw
|
|
273
|
+
# it immediately: the colors come from the tokenizer and def-hints
|
|
274
|
+
# from the text mode roster, both text-based; code_dict extras
|
|
275
|
+
# (usage links) join when the parse arrives and dict_host's
|
|
276
|
+
# notify_on_change repaints this editor. Edits during this brief window
|
|
277
|
+
# are discarded - the host isn't there to receive them, the same
|
|
278
|
+
# contract as a read-only code-diff tab.
|
|
279
|
+
buffer_text = (held_values[0] if from_host
|
|
280
|
+
else input_value if isinstance(input_value, str) else None)
|
|
281
|
+
if buffer_text is not None:
|
|
282
|
+
_t_dt0 = time.monotonic()
|
|
283
|
+
changed, value, ds = RenderFuncs.draw_text(buffer_text, code_dict=code_dict,
|
|
284
|
+
code_tree=cache_error, error=error,
|
|
285
|
+
import_fixes=import_fixes,
|
|
286
|
+
return_extras=True,
|
|
287
|
+
**{"gutter_indent": True, **kwargs, "is_tree": False})
|
|
288
|
+
_t_dt1 = time.monotonic()
|
|
289
|
+
# Every frame's editor draws: mark as a LIVE user so the idle sweep
|
|
290
|
+
# keeps the host registered (and repaint it when a background parse
|
|
291
|
+
# lands). Not gated on `changed` - an open-but-unedited editor still
|
|
292
|
+
# owns the host, and the sweep would otherwise immediately-register it.
|
|
293
|
+
dict_host.notify_on_change(ds)
|
|
294
|
+
# Apply a pending start-up auto-select: map the file-absolute line
|
|
295
|
+
# into this span buffer via jump_to.start (the 0-based file line of
|
|
296
|
+
# buffer line #0) and select that line's code - same shape as the
|
|
297
|
+
# mouse-click line-select and draw_code_editor's jump_to_line
|
|
298
|
+
# consumption (caret placement + focus grant; the editor's own
|
|
299
|
+
# cursor-follow scroll brings it into view on the next body frame).
|
|
300
|
+
# One-shot per (arrow press, line) - stamped on ds.misc so re-renders
|
|
301
|
+
# must not keep clamping the selection while the user edits the
|
|
302
|
+
# editor, but a new press (select_seq bump) re-applies it.
|
|
303
|
+
if (select_line is not None and ds is not None
|
|
304
|
+
and isinstance(buffer_text, str)
|
|
305
|
+
and ds.misc.get("_applied_select_line") != (select_seq, select_line)):
|
|
306
|
+
ds.misc["_applied_select_line"] = (select_seq, select_line)
|
|
307
|
+
_start0 = getattr(kwargs.get("jump_to"), "start", 0) or 0
|
|
308
|
+
_lines = buffer_text.split("\n")
|
|
309
|
+
_li = max(0, min(int(select_line) - 1 - _start0, len(_lines) - 1))
|
|
310
|
+
_line_start = sum(len(l) + 1 for l in _lines[:_li])
|
|
311
|
+
_indent = len(_lines[_li]) - len(_lines[_li].lstrip())
|
|
312
|
+
_sel_len = len(_lines[_li]) - _indent
|
|
313
|
+
# Fold projection: these are FULL-buffer coords; the editor
|
|
314
|
+
# lays out fold-spliced display text. Expands any collapsed
|
|
315
|
+
# fold at the line, then shifts the selection start - the
|
|
316
|
+
# end rides the same line, so it shifts by the same delta.
|
|
317
|
+
from meltygui.editor.text_editor import fold_project_jump
|
|
318
|
+
_sel_s, _ = fold_project_jump(
|
|
319
|
+
ds, buffer_text, _line_start + _indent, _li)
|
|
320
|
+
ds.text_selection_start = _sel_s
|
|
321
|
+
ds.text_selection_end = _sel_s + _sel_len
|
|
322
|
+
ds.text_cursor_pos = ds.text_selection_end
|
|
323
|
+
Melty.text_focused_ds = ds
|
|
324
|
+
Melty._text_focus_grant_frame = Melty.frame_count
|
|
325
|
+
ds.invalidate()
|
|
326
|
+
request_render()
|
|
327
|
+
if changed and from_host:
|
|
328
|
+
_key0 = list(_str_host.keys())[0]
|
|
329
|
+
_held0 = _str_host[_key0]
|
|
330
|
+
if _held0 == value:
|
|
331
|
+
# Echo breaker: a changed=True with byte-identical text must
|
|
332
|
+
# not dirty the str host - that write is what feeds the
|
|
333
|
+
# startup parse storm (dirty → chain_out → queue_save →
|
|
334
|
+
# pending_gen bump → full reparse + usage recompute, per
|
|
335
|
+
# frame, for a no-op). Python == short-circuits by length
|
|
336
|
+
# and first differing char, so real edits pay ~nothing;
|
|
337
|
+
# the full-length compare only matters in the spurious
|
|
338
|
+
# case, where it replaces a multi-frame pipeline.
|
|
339
|
+
_ptrace("editor changed with IDENTICAL text — host write suppressed",
|
|
340
|
+
host=_host_label(dict_host))
|
|
341
|
+
else:
|
|
342
|
+
_t_w0 = time.monotonic()
|
|
343
|
+
_str_host[_key0] = value
|
|
344
|
+
_t_w1 = time.monotonic()
|
|
345
|
+
# TEMP perf: split the editor frame's tail - the draw_text
|
|
346
|
+
# CALL (body + render_func wrapper epilogue; body time is
|
|
347
|
+
# the top "draw_text took" line) vs the host WRITE
|
|
348
|
+
# (install_bubbling + notify_on_changed + invalidations).
|
|
349
|
+
if (_t_w1 - _t_dt0) * 1000.0 >= 30.0:
|
|
350
|
+
_ptrace("editor tail split",
|
|
351
|
+
call_ms=round((_t_dt1 - _t_dt0) * 1000.0, 1),
|
|
352
|
+
write_ms=round((_t_w1 - _t_w0) * 1000.0, 1))
|
|
353
|
+
# # Re-render this editor when a background parse lands: its cached
|
|
354
|
+
# # subtree is outside the host's own draw loop, so without registering it
|
|
355
|
+
# # the fresh cst_dict sits invisible until an unrelated invalidation.
|
|
356
|
+
# if dict_host is not None and ds is not None:
|
|
357
|
+
# dict_host.notify_on_change(ds)
|
|
358
|
+
_dt_editor = (time.monotonic() - _t_editor0) * 1000.0
|
|
359
|
+
if _dt_editor >= 20.0 and dict_host is not None:
|
|
360
|
+
# Render-thread stall inside the editor's loop (draw_text + pulls).
|
|
361
|
+
_ptrace_rl(("editor-slow", id(dict_host)),
|
|
362
|
+
f"editor frame took {_dt_editor:.0f}ms",
|
|
363
|
+
host=_host_label(dict_host))
|
|
364
|
+
return False, None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@render_func(use_cache=True, show_bg=False, selectable=False, disable_scroll=True,
|
|
368
|
+
shadow=False, indent_size=0, with_footer=None, bg_offset=0)
|
|
369
|
+
def draw_code_tabs_from_cache(input_value=None, root_input=None, tab_state: TabState = None,
|
|
370
|
+
unique=None, draw_state: DrawState = None, column_widths=None,
|
|
371
|
+
column_edges=None, draw=False, error=None,
|
|
372
|
+
run_jedi=False, **kwargs):
|
|
373
|
+
"""NEW_CODE's text|structured tabs on the code-host-cache route — no inline
|
|
374
|
+
chain_in/chain_out (the legacy convert_in_and_out path this replaces).
|
|
375
|
+
|
|
376
|
+
• draw_text — delegates to draw_text_from_code_cache; a text edit returns
|
|
377
|
+
to the enclosing code_file_io, which saves the span (its normal path).
|
|
378
|
+
• draw_collection — renders the shared dict_host's HELD GeneralParse
|
|
379
|
+
(code_hosts_for). An edit mutates the bubbling-wrapped parse in place,
|
|
380
|
+
marking the host dirty; the host chain_outs to source and saves through
|
|
381
|
+
the cache's own str_host, so nothing returns upward from this column.
|
|
382
|
+
An edit ALSO drives the live source immediately (live_apply_edits:
|
|
383
|
+
class vars, function param defaults + constant locals, module
|
|
384
|
+
globals) — the same responsive preview the chain route's
|
|
385
|
+
general_parse_to_address gives, ahead of any recompile.
|
|
386
|
+
|
|
387
|
+
The two code_file_io instances (this window's and the cache's str_host)
|
|
388
|
+
only ever talk through the FILE: a dict edit saves via the cache and this
|
|
389
|
+
window's auto_load_edits picks it up; a text edit saves here and the
|
|
390
|
+
cache's file watch re-parses."""
|
|
391
|
+
from meltygui.code.chain_converters import live_apply_edits
|
|
392
|
+
from meltygui.code.new_converters import _ensure_symbol_index
|
|
393
|
+
from meltygui.code.new_converters import _host_code_tree_error
|
|
394
|
+
from meltygui.code.new_converters import _host_label
|
|
395
|
+
from meltygui.code.new_converters import _host_relint_and_fixes
|
|
396
|
+
from meltygui.code.new_converters import code_hosts_for
|
|
397
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
398
|
+
from meltygui.core.diagnostics.perf_trace import once as _ponce
|
|
399
|
+
from meltygui.core.diagnostics.perf_trace import trace as _ptrace
|
|
400
|
+
|
|
401
|
+
_t_tabs0 = time.monotonic()
|
|
402
|
+
view_funcs = [RenderFuncs.draw_collection_as_tabs, RenderFuncs.draw_text]
|
|
403
|
+
# Drop entries that didn't survive (de)serialization, then default to two
|
|
404
|
+
# tabs (structured | text), matching draw_with_view_funcs.
|
|
405
|
+
tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t is not None]
|
|
406
|
+
if not tab_state.selected_tabs:
|
|
407
|
+
tab_state.selected_tabs = view_funcs[:2]
|
|
408
|
+
|
|
409
|
+
imgui.dummy(0, 5)
|
|
410
|
+
names = [getattr(vf, '__name__', str(vf)) for vf in view_funcs]
|
|
411
|
+
tab_changed, new_tabs = RenderFuncs.draw_tab_bar(input_value=tab_state.selected_tabs,
|
|
412
|
+
tab_height=30, show_bg=False, bg_offset=0,
|
|
413
|
+
name=f"tab_bar{unique}", names=names,
|
|
414
|
+
collection=view_funcs, as_toggles=False)
|
|
415
|
+
if tab_changed:
|
|
416
|
+
tab_state.selected_tabs = new_tabs
|
|
417
|
+
|
|
418
|
+
imgui.dummy(0, 2)
|
|
419
|
+
dict_host = None
|
|
420
|
+
if root_input is not None:
|
|
421
|
+
_str_host, dict_host = code_hosts_for(root_input)
|
|
422
|
+
# Repaint this subtree when the background parse lands - it reads the
|
|
423
|
+
# host's value from outside the host's own draw loop (same registration
|
|
424
|
+
# draw_text_from_code_cache makes for its error/code_dict pull).
|
|
425
|
+
dict_host.notify_on_change(draw_state)
|
|
426
|
+
# Auto-index here too (idempotent with the draw_text delegate's call):
|
|
427
|
+
# a structured-only window never runs draw_text_from_code_cache, and
|
|
428
|
+
# its usage links should stay live all the same.
|
|
429
|
+
_ensure_symbol_index(dict_host, _str_host, dict_host._held(),
|
|
430
|
+
kwargs.get("jump_to"))
|
|
431
|
+
|
|
432
|
+
# New columnLayout (shared edge system): the panes line up with other
|
|
433
|
+
# objects drawn on the root window - the divider between the
|
|
434
|
+
# structured and text panes is a draggable line in the same collision
|
|
435
|
+
# region as every other window edge. Lazy import (new_core_view sits
|
|
436
|
+
# between this module and columns.py). left_edge/right_edge: when this
|
|
437
|
+
# view renders inside another row's cell, the host passes the cell's edge
|
|
438
|
+
# dicts (through code_file_io's child_kwargs) and they become this row's
|
|
439
|
+
# far edges by reference - same adoption draw_columns gives nested
|
|
440
|
+
# Columns. Absent (the usual standalone window), ColumnLayout falls back
|
|
441
|
+
# to the window frame edges.
|
|
442
|
+
from meltygui.core.layout.column_core import ColumnLayout
|
|
443
|
+
from meltygui.core.layout.column_core import MIN_ROW_HEIGHT
|
|
444
|
+
|
|
445
|
+
cols = ColumnLayout(draw_state, len(tab_state.selected_tabs),
|
|
446
|
+
column_edges=column_edges, column_widths=column_widths,
|
|
447
|
+
left_edge=kwargs.get("left_edge"),
|
|
448
|
+
right_edge=kwargs.get("right_edge"))
|
|
449
|
+
# Pin each pane to the visible viewport (the legacy column_max_height
|
|
450
|
+
# path this replaces) - long sources scroll inside their pane.
|
|
451
|
+
avail_h = None
|
|
452
|
+
size_kwargs = {}
|
|
453
|
+
clip = cols.clip if cols.clip is not None else draw_state.abs_clip_rect
|
|
454
|
+
if clip is not None:
|
|
455
|
+
# Exactly match the frame band: the pane then ends one pixel
|
|
456
|
+
# above the band's bottom edge, so the black reads even all around.
|
|
457
|
+
avail_h = max(MIN_ROW_HEIGHT, clip[3] - cols.top)
|
|
458
|
+
# The pane content is inset by the fixed padding on every side.
|
|
459
|
+
size_kwargs = {"height": avail_h - 2 * cols.padding}
|
|
460
|
+
|
|
461
|
+
# Grab the parse + its normalized error off the MANAGED dict_host once, before
|
|
462
|
+
# the tab loop. Both tabs read them: the structured tab renders `gp` directly,
|
|
463
|
+
# the text tab injects code_dict/code_tree/error into its draw_text leaf via
|
|
464
|
+
# child_kwargs. Computing here also drops the old order dependency (the text tab
|
|
465
|
+
# read `gp` before the structured branch defined it).
|
|
466
|
+
gp = dict_host._held() if dict_host is not None else None
|
|
467
|
+
if isinstance(gp, dict):
|
|
468
|
+
if _ponce(("parse-visible", _host_label(dict_host))):
|
|
469
|
+
_ptrace("tabs: first parse visible", host=_host_label(dict_host))
|
|
470
|
+
elif dict_host is not None and _ponce(("parse-wait", _host_label(dict_host))):
|
|
471
|
+
_ptrace("tabs: waiting for first parse", host=_host_label(dict_host))
|
|
472
|
+
# Relint machinery + the import-suggestions pull run here too - this
|
|
473
|
+
# route's text pane wires draw_text directly (below), NOT through
|
|
474
|
+
# draw_text_from_code_cache, so without this the Alt-Enter quick-fix
|
|
475
|
+
# data never reached it (the live_view_forward NEW_CODE path).
|
|
476
|
+
import_fixes = _host_relint_and_fixes(
|
|
477
|
+
dict_host, _str_host, getattr(dict_host, "_wrapper_draw_state", None))
|
|
478
|
+
cache_error = _host_code_tree_error(dict_host)
|
|
479
|
+
|
|
480
|
+
raw_changed, raw_value = False, input_value
|
|
481
|
+
_pane_ms = {}
|
|
482
|
+
for idx, view_func in enumerate(tab_state.selected_tabs):
|
|
483
|
+
_t_pane0 = time.monotonic()
|
|
484
|
+
with cols.cell(idx, height=avail_h) as col_width:
|
|
485
|
+
if getattr(view_func, "__name__", "") == "draw_text":
|
|
486
|
+
# The host's parse + errors flow to the draw_text leaf through
|
|
487
|
+
# draw_collection's child_kwargs: code_dict → token views / symbol
|
|
488
|
+
# usages, code_tree → the syntax/lint error highlight, error → the
|
|
489
|
+
# recompile/runtime highlight (the same trio draw_text_from_code_cache
|
|
490
|
+
# hands draw_text, now via the parent _str_host).
|
|
491
|
+
m_changed, m_out = RenderFuncs.draw_collection(
|
|
492
|
+
input_value=_str_host,
|
|
493
|
+
child_kwargs={"error": error, "view_func": RenderFuncs.draw_text, "is_tree": False,
|
|
494
|
+
"code_dict": gp, "code_tree": cache_error, "child_kwargs": {"is_tree": False},
|
|
495
|
+
"import_fixes": import_fixes,
|
|
496
|
+
"run_jedi": run_jedi, "jump_to": kwargs.get("jump_to")},
|
|
497
|
+
show_header=False, show_name=False,
|
|
498
|
+
width=col_width, **size_kwargs,
|
|
499
|
+
name=f"draw_text##{unique}")
|
|
500
|
+
if m_changed:
|
|
501
|
+
notify("text changed", tag="save bug", tint=(1, 1, 0.5))
|
|
502
|
+
raw_changed, raw_value = True, m_out
|
|
503
|
+
# draw_state.invalidate_up(max_depth=2)
|
|
504
|
+
else:
|
|
505
|
+
if not isinstance(gp, dict):
|
|
506
|
+
# Placeholder frame: keep every ancestor's persisted
|
|
507
|
+
# content_height (see Melty.pending_placeholder_frame) -
|
|
508
|
+
# this one-line stand-in doesn't become the measure.
|
|
509
|
+
Melty.pending_placeholder_frame = Melty.frame_count
|
|
510
|
+
imgui.text_colored("Parsing…" if dict_host is not None
|
|
511
|
+
else "No parse for this source", 0.6, 0.6, 0.6, 1.0)
|
|
512
|
+
continue
|
|
513
|
+
# No draw= forwarding: code_file_io passes draw=True the frame
|
|
514
|
+
# after EVERY keystroke (its reconvert trigger), and the one-shot
|
|
515
|
+
# cache bypass rebuilt this whole structured pane per key
|
|
516
|
+
# (~13-23ms for Toggles' ~110 rows). The pane's content (gp)
|
|
517
|
+
# only changes when a chain_in parse lands, and that landing
|
|
518
|
+
# already force-invalidates this subtree (dict_host's
|
|
519
|
+
# _notify_callers) - so the pane refreshes once per debounced
|
|
520
|
+
# parse instead of per keystroke.
|
|
521
|
+
# DEBUG (dict-pane keystroke cost): snapshot the pane's tile
|
|
522
|
+
# state BEFORE the call - was it dirty (someone invalidated
|
|
523
|
+
# it) or clean (cache gate re-ran the pane anyway)?
|
|
524
|
+
_dbg_key = getattr(draw_state, "_dbg_dict_key", None)
|
|
525
|
+
_dbg_t = (Melty.cache._tiles.get(_dbg_key)
|
|
526
|
+
if _dbg_key and Melty.cache is not None else None)
|
|
527
|
+
_pane_ms["dict_pre"] = (
|
|
528
|
+
f"dirty={_dbg_t.dirty}/inv=f{_dbg_t.last_invalidated_frame}"
|
|
529
|
+
f"/clean=f{_dbg_t.last_clean_frame}/now=f{Melty.frame_count}"
|
|
530
|
+
if _dbg_t is not None else "tile=?")
|
|
531
|
+
m_changed, m_out = RenderFuncs.draw_collection(
|
|
532
|
+
gp, excluded=["__cst__", "__origin__"],
|
|
533
|
+
child_kwargs={"show_bg": False, "shadow": False, "folder_type":(dict), "use_cache": True, "z_offset": 0, "view_func":RenderFuncs.draw_collection_as_tabs},
|
|
534
|
+
show_system=False,
|
|
535
|
+
disable_scroll=False, show_header=False, show_add_delete=False,
|
|
536
|
+
width=col_width, **size_kwargs, show_parent_add_delete=False,
|
|
537
|
+
name=f"draw_collection##{unique}", selectable=False)
|
|
538
|
+
if _dbg_key is None and Melty.cache is not None:
|
|
539
|
+
# One-shot: resolve the pane's tile key by name and arm the
|
|
540
|
+
# tile's invalidation stack-print on its draw_state.
|
|
541
|
+
_nm = f"draw_collection##{unique}"
|
|
542
|
+
for _k, _ds2 in Melty.cache.key_to_draw_state.items():
|
|
543
|
+
if _ds2 is not None and getattr(_ds2, "name", None) == _nm:
|
|
544
|
+
draw_state._dbg_dict_key = _k
|
|
545
|
+
# Arm the tile-bump tracer (see draw_offscreen's
|
|
546
|
+
# _bump_note): every code path that dirties this
|
|
547
|
+
# pane's tile names itself in the perf log.
|
|
548
|
+
_ds2._bump_trace_armed = True
|
|
549
|
+
break
|
|
550
|
+
if m_changed:
|
|
551
|
+
notify("dict changed", tag="save bug", tint=(1, 1, 0.5))
|
|
552
|
+
# A rebuilt top-level dict (reorder / add / delete) replaces the
|
|
553
|
+
# host value; an in-place value edit already bubbled the host
|
|
554
|
+
# dirty. Either way the host chain_outs + updates on its own draw.
|
|
555
|
+
if m_out is not gp and isinstance(m_out, dict):
|
|
556
|
+
dict_host[dict_host.value_key] = m_out
|
|
557
|
+
gp = m_out
|
|
558
|
+
live_apply_edits(root_input, gp)
|
|
559
|
+
draw_state.invalidate_up(max_depth=2)
|
|
560
|
+
_pane_ms[getattr(view_func, "__name__", str(idx))] = \
|
|
561
|
+
(time.monotonic() - _t_pane0) * 1000.0
|
|
562
|
+
|
|
563
|
+
cols.finish()
|
|
564
|
+
_dt_tabs = (time.monotonic() - _t_tabs0) * 1000.0
|
|
565
|
+
if dict_host is not None and (
|
|
566
|
+
_dt_tabs >= 10.0
|
|
567
|
+
or (isinstance(_pane_ms.get("draw_collection"), float)
|
|
568
|
+
and _pane_ms["draw_collection"] >= 3.0)):
|
|
569
|
+
# Render time time inside the tabs subtree this frame - per-pane
|
|
570
|
+
# split so a slow frame shows the pane (text editor vs structured
|
|
571
|
+
# collection) instead of one opaque total. UNratelimited while the
|
|
572
|
+
# dict pane takes time, so a typing burst shows every occurrence
|
|
573
|
+
# (bounded by keystroke rate; dict_pre shows the pane tile's dirty
|
|
574
|
+
# state going in).
|
|
575
|
+
_panes = " ".join(f"{k}={v:.0f}ms" if isinstance(v, float) else f"{k}={v}"
|
|
576
|
+
for k, v in _pane_ms.items())
|
|
577
|
+
_ptrace(f"tabs frame took {_dt_tabs:.0f}ms [{_panes}]",
|
|
578
|
+
host=_host_label(dict_host))
|
|
579
|
+
return False, None
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def draw_jump_to(input_value: Address, unique, width=30, error_msg=None,
|
|
583
|
+
draw_state=None):
|
|
584
|
+
file_name = input_value.path.name if input_value.path is not None else "Unknown file"
|
|
585
|
+
line_number = input_value.start + 1 if input_value.start is not None else None
|
|
586
|
+
# Unicode escape (not a literal string) for the Font Awesome folder icon - a
|
|
587
|
+
# pasted PUA char gets stripped to empty on save, which is why it vanished.
|
|
588
|
+
folder_icon = "" # FA folder
|
|
589
|
+
|
|
590
|
+
# Label with the enclosing function name + line number. Prefer the function
|
|
591
|
+
# already attached to the address (.source); otherwise retrieve it from the
|
|
592
|
+
# line via the cached _enclosing_function helper.
|
|
593
|
+
fn = input_value.source if isinstance(input_value.source, types.FunctionType) else None
|
|
594
|
+
if fn is None and line_number is not None and input_value.path is not None:
|
|
595
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
596
|
+
fn = _enclosing_function(str(input_value.path), line_number)
|
|
597
|
+
|
|
598
|
+
label = f"{file_name}:{line_number}" if line_number is not None else file_name
|
|
599
|
+
if fn is not None:
|
|
600
|
+
label = f"{fn.__name__} ({label})"
|
|
601
|
+
|
|
602
|
+
# File-header bar: a rounded filled rect spanning the content width, drawn
|
|
603
|
+
# behind the label + jump button. Packed ABGR colors per the codebase idiom.
|
|
604
|
+
# When there's an error, the bar grows a second row to hold the message, and
|
|
605
|
+
# both the fill and the outline tint red so the header reads as "this file has
|
|
606
|
+
# a problem".
|
|
607
|
+
draw_list = imgui.get_window_draw_list()
|
|
608
|
+
x0, y0 = imgui.get_cursor_screen_pos()
|
|
609
|
+
pad_x, pad_y = 8, 3
|
|
610
|
+
row_h = imgui.get_frame_height() + pad_y * 2
|
|
611
|
+
msg = str(error_msg).split('\n', 1)[0] if error_msg else None
|
|
612
|
+
msg_row_h = (imgui.get_text_line_height() + 4) if msg else 0
|
|
613
|
+
x1, y1 = x0 + width, y0 + row_h + msg_row_h
|
|
614
|
+
# Round only the top two corners so the bar reads as a box sitting flush
|
|
615
|
+
# on top of the body below it.
|
|
616
|
+
rounding = 4.0
|
|
617
|
+
top_corners = imgui.DRAW_ROUND_CORNERS_TOP
|
|
618
|
+
if msg:
|
|
619
|
+
fill_col = pack_color(70 / 255, 30 / 255, 40 / 255, 235 / 255) # dark red-tinted fill
|
|
620
|
+
line_col = pack_color(150 / 255, 60 / 255, 70 / 255, 1.0) # red outline
|
|
621
|
+
else:
|
|
622
|
+
fill_col = pack_color(44 / 255, 52 / 255, 62 / 255, 230 / 255)
|
|
623
|
+
line_col = pack_color(66 / 255, 78 / 255, 90 / 255, 1.0)
|
|
624
|
+
draw_list.add_rect_filled(x0, y0, x1, y1, fill_col, rounding, top_corners)
|
|
625
|
+
draw_list.add_rect(x0, y0, x1, y1, line_col, rounding, top_corners)
|
|
626
|
+
|
|
627
|
+
# Row 1: label (vertically centered against the button frame) + icon jump
|
|
628
|
+
# button. flat_button (draw-list + on_action through the EDITOR's
|
|
629
|
+
# draw_state - this bar is drawn inside draw_text's body), not a
|
|
630
|
+
# @render_func button: the old widget re-rendered its full wrapper every
|
|
631
|
+
# editor frame. The measured rect is stashed in the editor's state so its
|
|
632
|
+
# selection pass can null the PRESS inside it (the old button's own
|
|
633
|
+
# draw_state used to claim that press; without the null, clicking Open
|
|
634
|
+
# would also place the caret in the document under the floating bar).
|
|
635
|
+
from meltygui.view.header_view import flat_button
|
|
636
|
+
from meltygui.core.melty import Melty
|
|
637
|
+
imgui.set_cursor_screen_pos((x0 + pad_x, y0 + pad_y))
|
|
638
|
+
_open_label = f"{folder_icon} Open"
|
|
639
|
+
_bw = imgui.calc_text_size(_open_label).x + Melty.px(15)
|
|
640
|
+
_bh = Melty.px(18.0)
|
|
641
|
+
_bx, _by = imgui.get_cursor_screen_pos()
|
|
642
|
+
if draw_state is not None:
|
|
643
|
+
draw_state._jump_btn_rect = (_bx, _by, _bx + _bw, _by + _bh)
|
|
644
|
+
if flat_button(f"{_open_label}##jump_to{unique}", draw_state,
|
|
645
|
+
view_id=f"jump_open{unique}", width=_bw, height=_bh):
|
|
646
|
+
from meltygui.core.runtime.extensions import open_source as open_in_editor
|
|
647
|
+
open_in_editor(str(input_value.path), line_number=line_number,
|
|
648
|
+
token=fn.__name__ if fn is not None else None)
|
|
649
|
+
|
|
650
|
+
imgui.same_line()
|
|
651
|
+
imgui.align_text_to_frame_padding()
|
|
652
|
+
imgui.text(label)
|
|
653
|
+
|
|
654
|
+
# Row 2: the full error message, in red, spanning the bar. Truncated to the
|
|
655
|
+
# bar width so a long message can't overflow.
|
|
656
|
+
if msg:
|
|
657
|
+
avail = max(0, width - 2 * pad_x)
|
|
658
|
+
if imgui.calc_text_size(msg).x > avail:
|
|
659
|
+
ch_w = max(1.0, imgui.calc_text_size("x").x)
|
|
660
|
+
keep = max(3, int(avail / ch_w) - 1)
|
|
661
|
+
msg = msg[:keep] + "…"
|
|
662
|
+
imgui.set_cursor_screen_pos((x0 + pad_x, y0 + row_h - 2))
|
|
663
|
+
imgui.text_colored(msg, 1.0, 0.5, 0.46, 1.0)
|
|
664
|
+
|
|
665
|
+
# Reserve the bar's full height so following content doesn't overlap it.
|
|
666
|
+
imgui.set_cursor_screen_pos((x0, y1 + 2))
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def draw_code_line_fast(draw_list, x, y, text, char_w, line_h, max_width=None,
|
|
670
|
+
block_tint=None, spans=(), line_open=None, alpha=1.0,
|
|
671
|
+
block_rounding=4.0, emphasis=None, dim_alpha=None):
|
|
672
|
+
"""Paint `text` (one line, no newline) at (x, y) with the editor's syntax
|
|
673
|
+
colours. `char_w` / `line_h` come from `push_code_font` (the font must
|
|
674
|
+
be pushed). `max_width` truncates by whole cells. `block_tint` (rgb)
|
|
675
|
+
paints the definition block wash under the line — the enclosing tinted
|
|
676
|
+
class/def — and `spans` = [(col_start, col_end, rgb, scale)] the
|
|
677
|
+
occurrence washes, LINE-relative columns (see `CodeLineTints`).
|
|
678
|
+
`line_open` is the lexer state at the line's start (None = code,
|
|
679
|
+
'comment', or the (quote, kind) pair of a string the line begins
|
|
680
|
+
inside). `alpha` fades everything (a context row). `emphasis` =
|
|
681
|
+
[(col_start, col_end)] keeps those glyphs at full brightness and fades
|
|
682
|
+
every other glyph by `dim_alpha` (default the diff-collapse preview
|
|
683
|
+
fade, `Toggles.TextEditor.diff_preview_alpha`) — an EMPTY list dims the
|
|
684
|
+
whole line, None dims nothing. Washes never dim. Returns the painted
|
|
685
|
+
width in pixels."""
|
|
686
|
+
from meltygui.editor.code_line_fast import _split_emphasis
|
|
687
|
+
from meltygui.editor.code_line_fast import _u32
|
|
688
|
+
from meltygui.editor.code_line_fast import _wash_factors
|
|
689
|
+
|
|
690
|
+
from meltygui.editor.text_editor import COLORS
|
|
691
|
+
from meltygui.editor.text_editor import _tokenize_from
|
|
692
|
+
from meltygui.editor.text_editor import _bg_adjust
|
|
693
|
+
from meltygui.editor.text_editor import _mix_packed
|
|
694
|
+
from meltygui.editor.text_editor import _fade_packed
|
|
695
|
+
if max_width is not None:
|
|
696
|
+
cells = max(0, int(max_width // char_w))
|
|
697
|
+
if len(text) > cells:
|
|
698
|
+
text = text[:cells]
|
|
699
|
+
width = len(text) * char_w
|
|
700
|
+
block_f, sym_f, text_f = _wash_factors()
|
|
701
|
+
|
|
702
|
+
# ── block wash: the tinted definition body this line belongs to ──
|
|
703
|
+
if block_tint is not None:
|
|
704
|
+
rgb = _bg_adjust(tuple(block_tint[:3]), block_f)
|
|
705
|
+
a = max(0.0, min(1.0, Toggles.TextEditor.def_block_alpha)) * alpha
|
|
706
|
+
draw_list.add_rect_filled(x, y, x + max(len(text) + 1, 2) * char_w,
|
|
707
|
+
y + line_h, _u32(rgb, a), block_rounding)
|
|
708
|
+
|
|
709
|
+
# ── occurrence washes (+ their outline), same rects as the editor ──
|
|
710
|
+
sym_a = Toggles.TextEditor.def_symbol_alpha * alpha
|
|
711
|
+
ol_a = Toggles.TextEditor.def_symbol_outline_alpha * alpha
|
|
712
|
+
ol_b = Toggles.TextEditor.def_symbol_outline_brightness
|
|
713
|
+
ol_t = Toggles.TextEditor.def_symbol_outline_thickness
|
|
714
|
+
mixes = [] # (col_start, col_end, rgb_adjusted_for_text, scale)
|
|
715
|
+
for c0, c1, rgb, scale in spans:
|
|
716
|
+
c0, c1 = max(0, c0), min(len(text), c1)
|
|
717
|
+
if c1 <= c0:
|
|
718
|
+
continue
|
|
719
|
+
sa = _bg_adjust(tuple(rgb[:3]), sym_f)
|
|
720
|
+
sx, ex = x + c0 * char_w, x + c1 * char_w
|
|
721
|
+
draw_list.add_rect_filled(sx - 1, y + 1, ex + 1, y + line_h - 1,
|
|
722
|
+
_u32(sa, sym_a * scale), 3.0)
|
|
723
|
+
if ol_a > 0:
|
|
724
|
+
ol = (min(1.0, sa[0] * ol_b), min(1.0, sa[1] * ol_b), min(1.0, sa[2] * ol_b))
|
|
725
|
+
draw_list.add_rect(sx - 1, y + 1, ex + 1, y + line_h - 1,
|
|
726
|
+
_u32(ol, ol_a * scale), 3.0, thickness=ol_t)
|
|
727
|
+
mixes.append((c0, c1, _bg_adjust(tuple(rgb[:3]), text_f), scale))
|
|
728
|
+
|
|
729
|
+
# ── glyphs: tokens → palette, mixed by the wash under them ──
|
|
730
|
+
mix_k = Toggles.TextEditor.def_text_tint_mix
|
|
731
|
+
default = COLORS["default"]
|
|
732
|
+
try:
|
|
733
|
+
tokens = _tokenize_from(text, line_open)
|
|
734
|
+
except Exception:
|
|
735
|
+
tokens = [(text, "default")]
|
|
736
|
+
if emphasis is not None and dim_alpha is None:
|
|
737
|
+
dim_alpha = Toggles.TextEditor.diff_preview_alpha
|
|
738
|
+
col = 0
|
|
739
|
+
run_parts, run_x, run_col = None, 0.0, 0
|
|
740
|
+
pieces = [] # (col, text, kind) - tokens cut at emphasis boundaries
|
|
741
|
+
for token, kind in tokens:
|
|
742
|
+
if not token:
|
|
743
|
+
continue
|
|
744
|
+
nl = token.find("\n")
|
|
745
|
+
if nl != -1:
|
|
746
|
+
token = token[:nl]
|
|
747
|
+
if not token:
|
|
748
|
+
break
|
|
749
|
+
if kind == "clipped":
|
|
750
|
+
col += len(token)
|
|
751
|
+
continue
|
|
752
|
+
if emphasis is not None:
|
|
753
|
+
for pc, pt, bright in _split_emphasis(col, token, emphasis):
|
|
754
|
+
pieces.append((pc, pt, kind, bright))
|
|
755
|
+
else:
|
|
756
|
+
pieces.append((col, token, kind, True))
|
|
757
|
+
col += len(token)
|
|
758
|
+
if nl != -1:
|
|
759
|
+
break
|
|
760
|
+
for col, token, kind, bright in pieces:
|
|
761
|
+
color = COLORS.get(kind, default)
|
|
762
|
+
for c0, c1, rgb, scale in mixes:
|
|
763
|
+
if c0 <= col < c1:
|
|
764
|
+
color = _mix_packed(color, rgb, mix_k * scale)
|
|
765
|
+
break
|
|
766
|
+
if alpha < 1.0:
|
|
767
|
+
color = _fade_packed(color, alpha)
|
|
768
|
+
if not bright:
|
|
769
|
+
color = _fade_packed(color, dim_alpha)
|
|
770
|
+
tx = x + col * char_w
|
|
771
|
+
if kind == "icon":
|
|
772
|
+
if run_parts is not None:
|
|
773
|
+
draw_list.add_text(run_x, y, run_col, "".join(run_parts))
|
|
774
|
+
run_parts = None
|
|
775
|
+
# Icons aren't monospaced: one per standard cell, nudged 1px left.
|
|
776
|
+
ix = tx
|
|
777
|
+
for ch in token:
|
|
778
|
+
draw_list.add_text(ix - 1, y, color, ch)
|
|
779
|
+
ix += char_w
|
|
780
|
+
elif token.isascii() and "\t" not in token:
|
|
781
|
+
if run_parts is not None and run_col == color:
|
|
782
|
+
run_parts.append(token)
|
|
783
|
+
else:
|
|
784
|
+
if run_parts is not None:
|
|
785
|
+
draw_list.add_text(run_x, y, run_col, "".join(run_parts))
|
|
786
|
+
run_parts, run_x, run_col = [token], tx, color
|
|
787
|
+
else:
|
|
788
|
+
if run_parts is not None:
|
|
789
|
+
draw_list.add_text(run_x, y, run_col, "".join(run_parts))
|
|
790
|
+
run_parts = None
|
|
791
|
+
draw_list.add_text(tx, y, color, token)
|
|
792
|
+
if run_parts is not None:
|
|
793
|
+
draw_list.add_text(run_x, y, run_col, "".join(run_parts))
|
|
794
|
+
return width
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def draw_live_view_overlay(x=0, y=0, w=0, h=0, draw_state=None, char_w=8.0,
|
|
798
|
+
line_px=20.0, node=None, span=None, root=None,
|
|
799
|
+
line_offset=0, jump_to=None, sel_lo=None,
|
|
800
|
+
sel_hi=None, **kwargs):
|
|
801
|
+
"""token_views overlay callback for CallParse nodes (plain function — the
|
|
802
|
+
overlay pass calls it with raw screen coords, no render_func wrapper)."""
|
|
803
|
+
from meltygui.code.live_view import live_values_for
|
|
804
|
+
from meltygui.code.live_view import site_for_line
|
|
805
|
+
from meltygui.editor.live_view_views import _code_end_col
|
|
806
|
+
from meltygui.editor.live_view_views import _draw_marker_at
|
|
807
|
+
from meltygui.editor.live_view_views import _inline_value_text
|
|
808
|
+
from meltygui.editor.live_view_views import _line_in_selection
|
|
809
|
+
from meltygui.editor.live_view_views import _marker_idle_skip
|
|
810
|
+
from meltygui.editor.live_view_views import _stable_key_name
|
|
811
|
+
from meltygui.editor.live_view_views import _store_key_names
|
|
812
|
+
from meltygui.editor.live_view_views import _store_name
|
|
813
|
+
from meltygui.editor.live_view_views import _token_in_selection
|
|
814
|
+
|
|
815
|
+
if getattr(node, "func_name", None) != "live_view":
|
|
816
|
+
return
|
|
817
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
818
|
+
if not Toggles.TextEditor.enable_live_view:
|
|
819
|
+
return
|
|
820
|
+
# Viewport cull FIRST: the parse walk visits every node in the buffer, not
|
|
821
|
+
# just the visible ones - each off-screen marker is a full render_func call
|
|
822
|
+
# for nothing (its latched value will propagate via root_draw_states
|
|
823
|
+
# either way, exactly as when its liveosh scrolls in). Culling before
|
|
824
|
+
# the store lookup also keeps site_for_line (span parse + linemap, per
|
|
825
|
+
# node per frame) off every out-of-view live_view node.
|
|
826
|
+
clip = getattr(draw_state, "abs_clip_rect", None)
|
|
827
|
+
_off_view = clip is not None and (y + line_px < clip[1] or y > clip[3])
|
|
828
|
+
if (_off_view and getattr(draw_state, "_lv_full_overlay_until", 0)
|
|
829
|
+
<= Core.melty.frame_count):
|
|
830
|
+
# _lv_full_overlay_until: non-instrumented-run forward pass - an
|
|
831
|
+
# off-viewport marker with an OPEN window still renders once (with a
|
|
832
|
+
# FROZEN anchor, below) so the next value flows into the window.
|
|
833
|
+
return
|
|
834
|
+
filename = (getattr(root, "file_path", None)
|
|
835
|
+
or getattr(getattr(root, "address", None), "path", None)
|
|
836
|
+
or getattr(jump_to, "path", None))
|
|
837
|
+
if filename is None:
|
|
838
|
+
return
|
|
839
|
+
# span lines are 1-indexed relative to the editor buffer; line_offset is
|
|
840
|
+
# the 0-based file line of buffer line 0 → absolute 1-indexed file line.
|
|
841
|
+
store_obj, key_path = site_for_line(str(filename),
|
|
842
|
+
line_offset + span.start_line)
|
|
843
|
+
if store_obj is None:
|
|
844
|
+
return
|
|
845
|
+
token_cells = max(1, span.end_col - span.start_col)
|
|
846
|
+
if getattr(span, "end_line", span.start_line) != span.start_line:
|
|
847
|
+
# Multi-line call: box just the first line, from the token start to
|
|
848
|
+
# the end of that line's text.
|
|
849
|
+
src_lines = (getattr(root, "source", "") or "").split("\n")
|
|
850
|
+
if 1 <= span.start_line <= len(src_lines):
|
|
851
|
+
token_cells = max(1, len(src_lines[span.start_line - 1].rstrip())
|
|
852
|
+
- span.start_col)
|
|
853
|
+
pad = 2.0
|
|
854
|
+
# Selection predicate in buffer space: span lines are parse-relative,
|
|
855
|
+
# so map through the parse→buffer bridge before comparing with the
|
|
856
|
+
# editor's (line, col) selection bounds. The widget counts as "inside"
|
|
857
|
+
# only when its whole token lies within the selection - a bare caret
|
|
858
|
+
# (no selection) never shows the window.
|
|
859
|
+
_lm = kwargs.get("line_map")
|
|
860
|
+
_sl = _lm(span.start_line) if _lm else span.start_line
|
|
861
|
+
cursor_inside = _token_in_selection(
|
|
862
|
+
_sl, span.start_col, span.start_col + token_cells, sel_lo, sel_hi)
|
|
863
|
+
snap = live_values_for(store_obj)
|
|
864
|
+
# The SHARED per-store name map (generation-keyed) - never a private
|
|
865
|
+
# copy: a stale private map minted names another consumer's fresh map
|
|
866
|
+
# gave to different keys (duplicate view IDs). The fallback name the
|
|
867
|
+
# key_path too, so a not-yet-mapped key still ranks against its twins.
|
|
868
|
+
_me = _store_key_names(store_obj)
|
|
869
|
+
_mname = (f"lvm::{_store_name(store_obj)}"
|
|
870
|
+
f"::{_me.get(key_path) or _stable_key_name(key_path, snap)}")
|
|
871
|
+
_frozen_pos = None
|
|
872
|
+
if _off_view:
|
|
873
|
+
# Forward pass for a culled marker: only proceed when its window is
|
|
874
|
+
# open, and draw at the marker's current absolute position - anchoring
|
|
875
|
+
# the pinned window at the true off-screen anchor parks it at
|
|
876
|
+
# _pinned_base = an editor-bottom clamp (the window "disappears").
|
|
877
|
+
_mreg = getattr(draw_state, "_lv_marker_ds", None)
|
|
878
|
+
_mds = _mreg.get(_mname) if _mreg else None
|
|
879
|
+
_w = getattr(_mds, "_lv_window_ds", None) if _mds else None
|
|
880
|
+
if _mds is None or _w is None or _w.closed:
|
|
881
|
+
return
|
|
882
|
+
_frozen_pos = (_mds.abs_left, _mds.abs_top)
|
|
883
|
+
_value = snap.get(key_path)
|
|
884
|
+
# A marker with an inline value draws every editor repaint (the value
|
|
885
|
+
# bakes into the tile), so it can never take the idle skip.
|
|
886
|
+
if (_frozen_pos is None
|
|
887
|
+
and _inline_value_text(_value) is None
|
|
888
|
+
and _marker_idle_skip(
|
|
889
|
+
draw_state, _mname, x - pad, y - pad,
|
|
890
|
+
token_cells * char_w + 2 * pad, line_px + 2 * pad,
|
|
891
|
+
key_path in snap, cursor_inside, store_obj, key_path,
|
|
892
|
+
(_sl or span.start_line) - 1, True)):
|
|
893
|
+
return
|
|
894
|
+
# Inline pill over a live_view() call: the call is instrumentation, not
|
|
895
|
+
# code worth peeking at, so the card FILLS the entire call span; the
|
|
896
|
+
# value may grow past it only when the span ends its line's code
|
|
897
|
+
# (source split shared with the snapshot overlay's memo, same memo
|
|
898
|
+
# invalidation).
|
|
899
|
+
_memo_ent = draw_state.__dict__.get("_lv_snap_memo")
|
|
900
|
+
_rsrc = getattr(root, "source", "") or ""
|
|
901
|
+
if _memo_ent is None or _memo_ent[0] is not _rsrc or len(_memo_ent) < 3:
|
|
902
|
+
_memo_ent = (_rsrc, {}, _rsrc.split("\n"))
|
|
903
|
+
object.__setattr__(draw_state, "_lv_snap_memo", _memo_ent)
|
|
904
|
+
_lines = _memo_ent[2]
|
|
905
|
+
_ltext = (_lines[span.start_line - 1]
|
|
906
|
+
if 1 <= span.start_line <= len(_lines) else "")
|
|
907
|
+
_tok_end = span.start_col + token_cells
|
|
908
|
+
_overflow = _tok_end >= _code_end_col(_ltext)
|
|
909
|
+
_draw_marker_at(draw_state,
|
|
910
|
+
_frozen_pos if _frozen_pos is not None
|
|
911
|
+
else (x - pad, y - pad),
|
|
912
|
+
cursor_inside,
|
|
913
|
+
(_sl, span.start_col, span.start_col + token_cells),
|
|
914
|
+
"/".join(map(str, key_path)),
|
|
915
|
+
value=_value,
|
|
916
|
+
captured=key_path in snap,
|
|
917
|
+
store_obj=store_obj, key_path=key_path,
|
|
918
|
+
inline_values=True,
|
|
919
|
+
inline_span_w=token_cells * char_w,
|
|
920
|
+
inline_overflow=_overflow, inline_fill=True,
|
|
921
|
+
in_selection=_line_in_selection(_sl, sel_lo, sel_hi),
|
|
922
|
+
caret_line=kwargs.get("caret_line"),
|
|
923
|
+
width=token_cells * char_w + 2 * pad,
|
|
924
|
+
height=line_px + 2 * pad,
|
|
925
|
+
buffer_line=(_sl or span.start_line) - 1,
|
|
926
|
+
name=_mname)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
@render_func(use_cache=False, show_bg=False, shadow=False, with_header=None,
|
|
930
|
+
show_name=False, selectable=False, disable_scroll=True, wrap=True,
|
|
931
|
+
z_offset=4, max_height=32, auto_state=False)
|
|
932
|
+
def draw_live_view_marker(input_value=None, draw_state=None,
|
|
933
|
+
store_obj=None, key_path=None, captured=False,
|
|
934
|
+
code_tree_node=None, auto_open=True,
|
|
935
|
+
inline_values=False, inline_dx=0.0,
|
|
936
|
+
inline_span_w=None, inline_overflow=False,
|
|
937
|
+
inline_fill=False,
|
|
938
|
+
in_selection=False, caret_line=None,
|
|
939
|
+
corner_radius=4.0, value=None,
|
|
940
|
+
left_mouse_double_clicked=False,
|
|
941
|
+
cursor_inside=False, editor_ds=None,
|
|
942
|
+
buffer_line=None, def_node=None, unique=0, **kwargs):
|
|
943
|
+
"""The live-view token widget — draw_bool_token's pattern plus one extra
|
|
944
|
+
call, draw_any(value, mode=WINDOW). `value` is the captured value (None +
|
|
945
|
+
captured=False while the site hasn't run); the window call just forwards
|
|
946
|
+
it and the framework routes it by type. input_value is a cheap STABLE
|
|
947
|
+
TOKEN (the key string), deliberately NOT the value: the wrapper's
|
|
948
|
+
recursion guard tracks non-primitive input_values by id, and a captured
|
|
949
|
+
object that also sits in the render ancestry (draw_state/ds aliases, the
|
|
950
|
+
menu's own target) tripped it — painting "Recursive reference detected"
|
|
951
|
+
over the code — while big values also paid wrapper bookkeeping per
|
|
952
|
+
marker. draw_text positioned this view inline over the symbol and the
|
|
953
|
+
draw_state carries its size — no rect plumbing.
|
|
954
|
+
|
|
955
|
+
(A bare-function version — draw_number_token_plain's pattern — was tried
|
|
956
|
+
and REVERTED: the swoosh connector anchors the value window back to the
|
|
957
|
+
marker's draw_state, so the marker must stay a render_func. Off-viewport
|
|
958
|
+
markers are culled by the overlays, which bounds the wrapper cost to
|
|
959
|
+
visible markers.)
|
|
960
|
+
|
|
961
|
+
`code_tree_node` is the owning scope's dict from draw_text's parse; the
|
|
962
|
+
site's `# [...]` comment is already formatted as a dict there
|
|
963
|
+
(__overrides__['__<key>__']) and IS, 1:1, the window call's **kwargs.
|
|
964
|
+
The box wears its `tint`.
|
|
965
|
+
|
|
966
|
+
Window close: the one place a Modes.WINDOW child differs from a direct
|
|
967
|
+
call — the framework can't see when a parent STOPS calling draw_any (it
|
|
968
|
+
approximates liveness with abs_closed) — so the window ds is tracked by
|
|
969
|
+
hand and, once it exists, called every render with visibility driven
|
|
970
|
+
through the closed= kwarg. (Maybe the framework can own this someday.)
|
|
971
|
+
|
|
972
|
+
`auto_open=False` (the snapshot/param markers) keeps the window closed
|
|
973
|
+
until the box is double-clicked — only explicit live_view() tokens pop
|
|
974
|
+
their value unprompted.
|
|
975
|
+
|
|
976
|
+
Interaction follows the number-widget convention: a single press/click
|
|
977
|
+
passes straight through to the editor (caret placement, selection —
|
|
978
|
+
plain text editing; nothing is declared to latch it away), and the
|
|
979
|
+
widget's own gesture is separate — DOUBLE-click toggles the value
|
|
980
|
+
window. left_mouse_double_clicked is declared (never read) as the
|
|
981
|
+
subscription half: hover-routed — the marker's higher z outranks the
|
|
982
|
+
editor's word-select for the doubled press — and its delivery
|
|
983
|
+
invalidates the tile so the body renders on the frame the raw
|
|
984
|
+
is_mouse_double_clicked read below is true."""
|
|
985
|
+
from meltygui.code.live_view import auto_dim_names_for
|
|
986
|
+
from meltygui.code.live_view import watch
|
|
987
|
+
from meltygui.editor.live_view_views import _NO_VALUE
|
|
988
|
+
from meltygui.editor.live_view_views import _SWATCH_HOLE
|
|
989
|
+
from meltygui.editor.live_view_views import _auto_run_on_user_open
|
|
990
|
+
from meltygui.editor.live_view_views import _def_name
|
|
991
|
+
from meltygui.editor.live_view_views import _display_key
|
|
992
|
+
from meltygui.editor.live_view_views import _drop_captured_value
|
|
993
|
+
from meltygui.editor.live_view_views import _ds_in_window
|
|
994
|
+
from meltygui.editor.live_view_views import _inline_swatch_rgba
|
|
995
|
+
from meltygui.editor.live_view_views import _inline_value_text
|
|
996
|
+
from meltygui.editor.live_view_views import _left_of_window_pos
|
|
997
|
+
from meltygui.editor.live_view_views import _merged_dim_names
|
|
998
|
+
from meltygui.editor.live_view_views import _mouse_in_window_tree
|
|
999
|
+
from meltygui.editor.live_view_views import _override_owner
|
|
1000
|
+
from meltygui.editor.live_view_views import _padded_dim_names
|
|
1001
|
+
from meltygui.editor.live_view_views import _paint_value_pill
|
|
1002
|
+
from meltygui.editor.live_view_views import _pill_tint
|
|
1003
|
+
from meltygui.editor.live_view_views import _stacked_list_value
|
|
1004
|
+
from meltygui.editor.live_view_views import current_live_root
|
|
1005
|
+
from meltygui.editor.live_view_views import release_live_value
|
|
1006
|
+
from meltygui.view.header_view import draw_header
|
|
1007
|
+
|
|
1008
|
+
ds = draw_state
|
|
1009
|
+
# Gutter registry: tell the editor which buffer lines carry a live marker
|
|
1010
|
+
# so its line-number gutter can draw a raw open/close button per line
|
|
1011
|
+
# (see the gutter pass in text_editor / set_marker_open below). Rebuilt
|
|
1012
|
+
# from scratch each frame the overlays render - frame-stamped so stale
|
|
1013
|
+
# entries from a previous parse never linger on the editor ds.
|
|
1014
|
+
if editor_ds is not None and buffer_line is not None:
|
|
1015
|
+
if getattr(editor_ds, "_lv_gutter_frame", None) != Core.melty.frame_count:
|
|
1016
|
+
editor_ds._lv_gutter_frame = Core.melty.frame_count
|
|
1017
|
+
editor_ds._lv_gutter_markers = {}
|
|
1018
|
+
editor_ds._lv_gutter_markers.setdefault(buffer_line, []).append(ds)
|
|
1019
|
+
# Marker-ds registry for the overlays' idle fast path (_marker_idle_skip):
|
|
1020
|
+
# lets them consult this site's state without paying the wrapper.
|
|
1021
|
+
if editor_ds is not None:
|
|
1022
|
+
reg = getattr(editor_ds, "_lv_marker_ds", None)
|
|
1023
|
+
if reg is None:
|
|
1024
|
+
reg = editor_ds._lv_marker_ds = {}
|
|
1025
|
+
reg[ds.name] = ds
|
|
1026
|
+
# Backlink for set_marker_open (the gutter pass only has the marker
|
|
1027
|
+
# ds): the value window anchors to the draw TEXT's left edge, which
|
|
1028
|
+
# only the editor ds knows.
|
|
1029
|
+
ds._lv_editor_ds = editor_ds
|
|
1030
|
+
# First only only: a gray box whose code then runs gets invalidated
|
|
1031
|
+
# on the key's FIRST value, flips green (and auto-opens below, when this
|
|
1032
|
+
# marker auto-opens) — one editor re-render per new key, nothing per
|
|
1033
|
+
# steady-state publish. The invalidation re-runs the overlay, which boxes
|
|
1034
|
+
# the marker and passes the new value in.
|
|
1035
|
+
watch(store_obj, key_path, ds, first_only=True)
|
|
1036
|
+
|
|
1037
|
+
# The comment data, already a dict in the code tree. Statement keys ARE
|
|
1038
|
+
# the symbol name; a `line:N#name` tail (frame snapshots, twin-site
|
|
1039
|
+
# params) carries the name behind the hash - the `# [...]` comment is
|
|
1040
|
+
# stamped in __overrides__ under the STATEMENT key, so resolve by it
|
|
1041
|
+
# either way (this is what keeps comment overrides like tint/cam_zoom
|
|
1042
|
+
# flowing into the value windows after the line-key migration). The
|
|
1043
|
+
# owning dict is found by _override_owner - a loop/if/try-body site's
|
|
1044
|
+
# comment lives in ITS block's nested dict, not the top of the scope.
|
|
1045
|
+
comment_args = {}
|
|
1046
|
+
lookup_key = None
|
|
1047
|
+
live_root = code_tree_node
|
|
1048
|
+
_locator = None
|
|
1049
|
+
if key_path:
|
|
1050
|
+
tail = str(key_path[-1])
|
|
1051
|
+
lookup_key = (tail.split("#", 1)[1]
|
|
1052
|
+
if tail.startswith("line:") and "#" in tail else tail)
|
|
1053
|
+
if isinstance(code_tree_node, dict) and lookup_key:
|
|
1054
|
+
# Locator for current_live_root (def name + start line, the editor
|
|
1055
|
+
# ds only as a fallback during readers), stamped BEFORE the comment
|
|
1056
|
+
# read so this render's own splat also resolves through the code
|
|
1057
|
+
# host's held tree - `code_tree_node` is whatever the cached tabs
|
|
1058
|
+
# body last captured and can lag a reparse by frames.
|
|
1059
|
+
_dname = _def_name(def_node)
|
|
1060
|
+
_locator = None
|
|
1061
|
+
if _dname:
|
|
1062
|
+
_dspan = getattr(def_node, "span", None)
|
|
1063
|
+
_locator = (editor_ds, _dname, getattr(_dspan, "start_line", 0) or 0)
|
|
1064
|
+
# Owner through the editor's per-def index (one BFS per def per
|
|
1065
|
+
# source version), not a BFS per marker; the raw walk stays the
|
|
1066
|
+
# fallback when there's no editor / def to key on.
|
|
1067
|
+
_osrc = None
|
|
1068
|
+
if editor_ds is not None and _locator is not None:
|
|
1069
|
+
_ecd = editor_ds.__dict__.get("code_dict")
|
|
1070
|
+
if not isinstance(_ecd, dict):
|
|
1071
|
+
_ecd = editor_ds.__dict__.get("code_tree")
|
|
1072
|
+
_osrc = getattr(_ecd, "source", None) if isinstance(_ecd, dict) else None
|
|
1073
|
+
live_root = _override_owner(
|
|
1074
|
+
code_tree_node, lookup_key,
|
|
1075
|
+
index_host=editor_ds if _osrc is not None else None,
|
|
1076
|
+
def_key=(_locator[1], _locator[2]) if _locator else None,
|
|
1077
|
+
src=_osrc)
|
|
1078
|
+
object.__setattr__(ds, "live_root", live_root)
|
|
1079
|
+
object.__setattr__(ds, "live_key", lookup_key)
|
|
1080
|
+
object.__setattr__(ds, "_lv_locator", _locator)
|
|
1081
|
+
_resolved = current_live_root(ds)
|
|
1082
|
+
if isinstance(_resolved, dict):
|
|
1083
|
+
live_root = _resolved
|
|
1084
|
+
_ov = live_root.get("__overrides__")
|
|
1085
|
+
_ca = _ov.get(f"__{lookup_key}__") if isinstance(_ov, dict) else None
|
|
1086
|
+
if isinstance(_ca, dict):
|
|
1087
|
+
comment_args = {k: v for k, v in _ca.items()
|
|
1088
|
+
if not (isinstance(k, str) and k.startswith("__"))}
|
|
1089
|
+
|
|
1090
|
+
# A list of same-shape tensors renders as ONE stacked tensor (leading
|
|
1091
|
+
# dim = list index) - the accumulator normally stacks at capture time,
|
|
1092
|
+
# but a raw captured list, its ragged-shape fast path, or a store built
|
|
1093
|
+
# by older code all arrive here as lists; healing at display time makes
|
|
1094
|
+
# "stacked" unconditional.
|
|
1095
|
+
value = _stacked_list_value(value, ds)
|
|
1096
|
+
|
|
1097
|
+
# dim_names is a SPECIAL input for loop sites: an accumulation value
|
|
1098
|
+
# carries auto-named leading dims (one per enclosing loop - stamped in
|
|
1099
|
+
# live_view._stack), and the site's own `# [dim_names=...]` names the
|
|
1100
|
+
# per-iteration value's dims. Prepend auto to user before the window
|
|
1101
|
+
# renders, so `for l_idx ...` over a (head, query, key) accumulator reads
|
|
1102
|
+
# ('l_idx', 'head', 'query', 'key') in the voxel tab. Tensor-shaped
|
|
1103
|
+
# values only - a list accumulator has no dims to name.
|
|
1104
|
+
_auto_dims = auto_dim_names_for(store_obj, key_path)
|
|
1105
|
+
_vkind = type(value).__name__
|
|
1106
|
+
_user_dims = comment_args.get("dim_names")
|
|
1107
|
+
_merge_auto = (_auto_dims if _auto_dims
|
|
1108
|
+
and _vkind in ("Tensor", "ndarray") else None)
|
|
1109
|
+
if _merge_auto:
|
|
1110
|
+
comment_args = dict(comment_args)
|
|
1111
|
+
comment_args["dim_names"] = _merged_dim_names(_merge_auto, _user_dims)
|
|
1112
|
+
# Too few names for the value's dims (or none at all)? pad with
|
|
1113
|
+
# positional dim<i> entries - AFTER the auto merge, so the pad covers
|
|
1114
|
+
# whatever the merged list still leaves out.
|
|
1115
|
+
_ndim = (len(getattr(value, "shape", ()))
|
|
1116
|
+
if _vkind in ("Tensor", "ndarray") else 0)
|
|
1117
|
+
_pad = _padded_dim_names(comment_args.get("dim_names"), _ndim)
|
|
1118
|
+
if _pad:
|
|
1119
|
+
comment_args = dict(comment_args)
|
|
1120
|
+
comment_args["dim_names"] = _pad
|
|
1121
|
+
|
|
1122
|
+
# Input-tab lookup: the context menu resolves this site's inputs off the
|
|
1123
|
+
# draw_state graph (draw_input_tab's live_root branch), so attach the same
|
|
1124
|
+
# OWNING dict the comment-args splat reads - the nested block dict for a
|
|
1125
|
+
# loop-body site - restamped every render like everything else per-site.
|
|
1126
|
+
# set_anywhere's lazy `# []` entry then materializes at the level the
|
|
1127
|
+
# save patch then writes back (a top-of-scope entry for a loop site
|
|
1128
|
+
# would never reach the site). Same name-normalized key as the comment
|
|
1129
|
+
# lookup, so line-keyed sites find their statement entry too.
|
|
1130
|
+
ds.live_root = live_root
|
|
1131
|
+
ds.live_key = lookup_key
|
|
1132
|
+
# Locator for current_live_root (stamped first, before the comment
|
|
1133
|
+
# read): readers that run while this marker ISN'T rendering (replayed
|
|
1134
|
+
# window, set_anywhere from its panel) resolve the owner dict through
|
|
1135
|
+
# the code host's held tree instead of the stamp, which every reparse
|
|
1136
|
+
# orwrites. The owner memo (_lv_owner_dict) is keyed on tree identity
|
|
1137
|
+
# and therefore survives renders - the def lookup runs once per
|
|
1138
|
+
# reparse, not once per frame.
|
|
1139
|
+
object.__setattr__(ds, "_lv_locator", _locator)
|
|
1140
|
+
|
|
1141
|
+
from meltygui.core.windowing.window_visibility import sync_marker_visibility
|
|
1142
|
+
sync_marker_visibility(ds, comment_args)
|
|
1143
|
+
auto_open = comment_args.get("auto_open", auto_open)
|
|
1144
|
+
|
|
1145
|
+
# ── INLINE VALUE: a simple builtin (int/float/str/bool/shortlist/enum)
|
|
1146
|
+
# renders as a text label overlapping the top of its code line instead
|
|
1147
|
+
# of a separate window. Both modes pass inline_values=True (explicit
|
|
1148
|
+
# live_view() tokens and instrumented-run snapshot markers alike):
|
|
1149
|
+
# every boxed symbol with a simple value shows up in place.
|
|
1150
|
+
inline_text = (_inline_value_text(value)
|
|
1151
|
+
if captured and inline_values else None)
|
|
1152
|
+
inline_swatch = None
|
|
1153
|
+
if inline_text is not None:
|
|
1154
|
+
_rgba = _inline_swatch_rgba(value)
|
|
1155
|
+
if _rgba is not None:
|
|
1156
|
+
inline_text = _SWATCH_HOLE + inline_text
|
|
1157
|
+
inline_swatch = (_rgba, 0)
|
|
1158
|
+
# An inline value has NO value window at all - no auto-open, no
|
|
1159
|
+
# preview, no double-click toggle. A window still open (persisted state,
|
|
1160
|
+
# or the value just turned simple) closes through the ordinary closing
|
|
1161
|
+
# draw_any call below. _lv_open resets to None, so a value that later
|
|
1162
|
+
# turns complex (str → tensor between runs) auto-opens again.
|
|
1163
|
+
if inline_text is not None and getattr(ds, "_lv_open", False):
|
|
1164
|
+
ds._lv_open = None
|
|
1165
|
+
# The gutter swaps the magnifier for an info glyph on inline markers
|
|
1166
|
+
# (no open/close left to toggle) - after the _lv_open pass in text.py.
|
|
1167
|
+
if getattr(ds, "_lv_inline", None) != (inline_text is not None):
|
|
1168
|
+
ds._lv_inline = inline_text is not None
|
|
1169
|
+
|
|
1170
|
+
# Manual window-ds tracking (see docstring).
|
|
1171
|
+
win_ds = getattr(ds, "_lv_window_ds", None)
|
|
1172
|
+
if not captured and win_ds is not None and not win_ds.closed:
|
|
1173
|
+
# The captured value vanished (store dropped) while the window was
|
|
1174
|
+
# open: with captured False the draw_any block below never runs, so
|
|
1175
|
+
# nothing else would stamp closed= and the window would linger
|
|
1176
|
+
# orphaned in root_draw_states. Stamp it directly; _lv_open resets to
|
|
1177
|
+
# None so the next value auto-opens it.
|
|
1178
|
+
win_ds.closed = True
|
|
1179
|
+
ds._lv_open = None
|
|
1180
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1181
|
+
request_render()
|
|
1182
|
+
if (getattr(ds, "_lv_open", None) is None and captured and auto_open
|
|
1183
|
+
and inline_text is None):
|
|
1184
|
+
ds._lv_open = True # first value seen → show it without a click
|
|
1185
|
+
elif win_ds is not None and win_ds.closed and getattr(ds, "_lv_open", False):
|
|
1186
|
+
ds._lv_open = False # user closed the window via its own header X
|
|
1187
|
+
# Arm the cursor-dismissed latch too: with the caret still inside the
|
|
1188
|
+
# symbol, preview_show would otherwise re-show the window on the very
|
|
1189
|
+
# next frame - the X X pins first (it's a press inside the window
|
|
1190
|
+
# rect), then closes, and an unarmed latch made the close a no-op.
|
|
1191
|
+
# The latch resets itself once the focused caret leaves the symbol.
|
|
1192
|
+
ds._lv_cursor_dismissed = True
|
|
1193
|
+
# Closing a live view forgets its DATA (the loop accumulator + the
|
|
1194
|
+
# tensor, often hundreds of MB) - the store keeps the marker with a
|
|
1195
|
+
# rerun hint so the widget stays, and the next run refills it.
|
|
1196
|
+
_drop_captured_value(store_obj, key_path)
|
|
1197
|
+
open_now = bool(getattr(ds, "_lv_open", False))
|
|
1198
|
+
|
|
1199
|
+
# ── EDIT-PIN: engaging with a preview window's own UI latches it open.
|
|
1200
|
+
# A cursor-held preview closes the moment the editor loses editor focus -
|
|
1201
|
+
# but the click that starts a param edit (a field in the window's
|
|
1202
|
+
# controls, or a context menu) IS such a focus change, so editing the
|
|
1203
|
+
# view's input params yanked the window (and the panel mid-edit) away.
|
|
1204
|
+
# The PRESS is the trigger, not focus: click processing clears the
|
|
1205
|
+
# editor's focus at frame start, this marker then sees and would stamp
|
|
1206
|
+
# the close, and only at end-of-frame dispatch would the clicked field
|
|
1207
|
+
# render and grab focus - by which point a closed window's panel never
|
|
1208
|
+
# renders at all. So on any engaged mouse press, rect-test the mouse
|
|
1209
|
+
# against the window and its controls and act exactly as if the marker
|
|
1210
|
+
# had been double-clicked; the focus check remains as the late-signal
|
|
1211
|
+
# fallback (e.g. focus handed over without a press). The header X still
|
|
1212
|
+
# unpins via the win_ds.closed branch above.
|
|
1213
|
+
if (captured and not open_now and inline_text is None
|
|
1214
|
+
and win_ds is not None and not win_ds.closed):
|
|
1215
|
+
_m = Core.melty
|
|
1216
|
+
pin = any(f is not None and _ds_in_window(f, win_ds)
|
|
1217
|
+
for f in (_m.focused_ds, _m.text_focused_ds,
|
|
1218
|
+
_m.popover_focused_ds))
|
|
1219
|
+
if not pin and (imgui.is_mouse_down(0) or imgui.is_mouse_clicked(0)
|
|
1220
|
+
or imgui.is_mouse_released(0)
|
|
1221
|
+
or imgui.is_mouse_down(1)
|
|
1222
|
+
or imgui.is_mouse_clicked(1)):
|
|
1223
|
+
_io = imgui.get_io()
|
|
1224
|
+
pin = _mouse_in_window_tree(win_ds, _io.mouse_pos.x,
|
|
1225
|
+
_io.mouse_pos.y)
|
|
1226
|
+
if pin:
|
|
1227
|
+
ds._lv_open = True
|
|
1228
|
+
open_now = True
|
|
1229
|
+
ds.invalidate()
|
|
1230
|
+
|
|
1231
|
+
x, y = imgui.get_cursor_screen_pos()
|
|
1232
|
+
w = max(1.0, ds.width)
|
|
1233
|
+
h = max(1.0, ds.height)
|
|
1234
|
+
io = imgui.get_io()
|
|
1235
|
+
hovered = x <= io.mouse_pos.x < x + w and y <= io.mouse_pos.y < y + h
|
|
1236
|
+
# Hover-edge invalidation (enter/leave only, never per-frame): the
|
|
1237
|
+
# outline is never-stated, so a cached tile must repaint exactly when
|
|
1238
|
+
# visibility changes.
|
|
1239
|
+
if getattr(ds, "_lv_hovered", None) != hovered:
|
|
1240
|
+
ds._lv_hovered = hovered
|
|
1241
|
+
ds.invalidate()
|
|
1242
|
+
|
|
1243
|
+
# PREVIEW: temporarily show the captured marker's live value - same
|
|
1244
|
+
# window, same placement - with double-click below still latching it
|
|
1245
|
+
# open permanently. Two trigger modes on Toggles.TextEditor.
|
|
1246
|
+
# live_hover_preview: ON → mousing over the marker previews and
|
|
1247
|
+
# mouse-leave closes; OFF → the editor TEXT CURSOR coming inside the
|
|
1248
|
+
# symbol previews and caret-leave closes. The hover mode needs a
|
|
1249
|
+
# per-frame keep-alive while previewing (the leave edge can only be
|
|
1250
|
+
# SEEN by a running body - a cached tile never re-tests hover); the
|
|
1251
|
+
# cursor mode doesn't: the caret only moves on frames the editor
|
|
1252
|
+
# renders, and the cursor_inside edge below invalidates the tile.
|
|
1253
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1254
|
+
hover_mode = bool(Toggles.TextEditor.live_hover_preview)
|
|
1255
|
+
_raw_ci = bool(cursor_inside) and not hover_mode
|
|
1256
|
+
# Dismissed latch: an X-closed cursor preview stays dismissed until the
|
|
1257
|
+
# FOCUSED caret genuinely leaves the symbol once. clear_focus alone was
|
|
1258
|
+
# not enough - the close's focus drop raced re-grants, and any regained
|
|
1259
|
+
# focus with the caret still in the symbol instantly re-showed the
|
|
1260
|
+
# window. The latch is positional, so it holds through editor churn;
|
|
1261
|
+
# while the editor is unfocused the overlay passes null cursor coords
|
|
1262
|
+
# (_raw_ci False), so the reset below keys off actual editor focus.
|
|
1263
|
+
_ed_focused = (editor_ds is not None
|
|
1264
|
+
and Core.melty.text_focused_ds is editor_ds)
|
|
1265
|
+
if _ed_focused and not _raw_ci:
|
|
1266
|
+
ds._lv_cursor_dismissed = False
|
|
1267
|
+
cursor_inside = _raw_ci and not getattr(ds, "_lv_cursor_dismissed", False)
|
|
1268
|
+
# Close observed one frame late (window rendered from root_draw_states
|
|
1269
|
+
# while this editor tile was cached): mark dismissal as the last
|
|
1270
|
+
# detection after draw_any below.
|
|
1271
|
+
if (cursor_inside and not open_now and win_ds is not None
|
|
1272
|
+
and win_ds.closed
|
|
1273
|
+
and getattr(ds, "_lv_cursor_preview_shown", False)):
|
|
1274
|
+
Core.melty.clear_focus()
|
|
1275
|
+
ds._lv_cursor_dismissed = True
|
|
1276
|
+
ds._lv_cursor_preview_shown = False
|
|
1277
|
+
cursor_inside = False
|
|
1278
|
+
if getattr(ds, "_lv_cursor_in", None) != cursor_inside:
|
|
1279
|
+
ds._lv_cursor_in = cursor_inside
|
|
1280
|
+
ds.invalidate()
|
|
1281
|
+
preview_show = (captured and not open_now and inline_text is None
|
|
1282
|
+
and ((hovered and hover_mode) or cursor_inside))
|
|
1283
|
+
if preview_show and hover_mode:
|
|
1284
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1285
|
+
ds.invalidate()
|
|
1286
|
+
request_render()
|
|
1287
|
+
|
|
1288
|
+
tint = comment_args.get("tint")
|
|
1289
|
+
if captured:
|
|
1290
|
+
if tint is not None:
|
|
1291
|
+
base = tuple(min(1.0, c + (0.15 if open_now else 0.0))
|
|
1292
|
+
for c in tint[:3])
|
|
1293
|
+
else:
|
|
1294
|
+
base = (0.36, 0.85, 0.46) if open_now else (0.26, 0.62, 0.34)
|
|
1295
|
+
else:
|
|
1296
|
+
base = (0.45, 0.45, 0.45)
|
|
1297
|
+
if hovered:
|
|
1298
|
+
base = tuple(min(1.0, c + 0.18) for c in base)
|
|
1299
|
+
# Outline only under the mouse — the boxes read as clutter when every
|
|
1300
|
+
# instrumented symbol is permanently framed; hover reveals the
|
|
1301
|
+
# affordance. An inline marker skips it: its pill wears the token's
|
|
1302
|
+
# background tint (painted below), and there's no window gesture for
|
|
1303
|
+
# hover to advertise.
|
|
1304
|
+
if hovered and inline_text is None:
|
|
1305
|
+
dl: _DrawList = imgui.get_window_draw_list()
|
|
1306
|
+
dl.add_rect(x, y + 2, x + w, y + h - 3,
|
|
1307
|
+
pack_color(*base, 0.9 if open_now else 0.6),
|
|
1308
|
+
rounding=corner_radius)
|
|
1309
|
+
imgui.dummy(w, h)
|
|
1310
|
+
|
|
1311
|
+
if inline_text is not None:
|
|
1312
|
+
# The label must repaint on every publish - the first_only watch
|
|
1313
|
+
# above fires once; this full watch invalidates per value.
|
|
1314
|
+
watch(store_obj, key_path, ds)
|
|
1315
|
+
# Only the FILL pill (a live_viewed call token - instrumentation
|
|
1316
|
+
# worth painting whole) paints here; comment/snapshot binding
|
|
1317
|
+
# markers paint through the trailing-gap pass instead (see
|
|
1318
|
+
# _draw_usage_labels - the code is repositioned, never covered).
|
|
1319
|
+
# Caret on this line, or the line inside the text selection: show
|
|
1320
|
+
# the real code, nothing painted - typing or deleting under a pill
|
|
1321
|
+
# would be blind. The pill comes back when the caret/selection
|
|
1322
|
+
# leave (the kwarg change re-renders this).
|
|
1323
|
+
if (inline_fill
|
|
1324
|
+
and (caret_line is None or caret_line != buffer_line)
|
|
1325
|
+
and not in_selection):
|
|
1326
|
+
# Drawn IN PLACE in the editor's code font (the current font -
|
|
1327
|
+
# no push), covering the instrumentation call token. The marker rect
|
|
1328
|
+
# wraps the token with a 2 px pad, so the token's text starts
|
|
1329
|
+
# at x + 2.
|
|
1330
|
+
text_x = x + 2.0 + inline_dx
|
|
1331
|
+
text_y = y + 2.0
|
|
1332
|
+
# The editor clipped this body to the token's own rect; the
|
|
1333
|
+
# pill is value-sized and can sit past that (the RHS). Pop out
|
|
1334
|
+
# to the ENCLOSING clip (the editor window) through Melty's own
|
|
1335
|
+
# stack and re-push the SAME rect after: push_clip intersects
|
|
1336
|
+
# with its parent, so pushing the saved (already-intersected)
|
|
1337
|
+
# rect restores it exactly, and the clip bookkeeping the tile
|
|
1338
|
+
# engine reads stays coherent.
|
|
1339
|
+
saved_clip = (Core.melty.clip_stack[-1]
|
|
1340
|
+
if Core.melty.clip_stack else None)
|
|
1341
|
+
if saved_clip is not None:
|
|
1342
|
+
Core.melty.pop_clip()
|
|
1343
|
+
# The pill wears the token's background tint (the comment
|
|
1344
|
+
# tint, else the enclosing def/class, else the file) so an RHS
|
|
1345
|
+
# pill far down the line is colored as this scope's value.
|
|
1346
|
+
_paint_value_pill(inline_text, text_x, text_y,
|
|
1347
|
+
span_width=inline_span_w,
|
|
1348
|
+
allow_overflow=inline_overflow,
|
|
1349
|
+
fill=inline_fill,
|
|
1350
|
+
tint=_pill_tint(editor_ds, buffer_line,
|
|
1351
|
+
str(key_path[-1]) if key_path
|
|
1352
|
+
else None, tint),
|
|
1353
|
+
swatch=inline_swatch)
|
|
1354
|
+
if saved_clip is not None:
|
|
1355
|
+
Core.melty.push_clip(saved_clip)
|
|
1356
|
+
|
|
1357
|
+
# Double-click toggles the value window. Read RAW imgui here (the same
|
|
1358
|
+
# split the single-click version used): the declared event param is the
|
|
1359
|
+
# subscription/wake half - its delivery invalidates the tile so the body
|
|
1360
|
+
# renders on the very frame is_mouse_double_clicked is true - while the raw
|
|
1361
|
+
# read is the single trigger, so the two halves can never toggle twice
|
|
1362
|
+
# for one gesture.
|
|
1363
|
+
if hovered and inline_text is None and imgui.is_mouse_double_clicked(0):
|
|
1364
|
+
open_now = not open_now
|
|
1365
|
+
ds._lv_open = open_now
|
|
1366
|
+
from meltygui.core.windowing.window_visibility import marker_user_visibility
|
|
1367
|
+
marker_user_visibility(ds, not open_now)
|
|
1368
|
+
if open_now:
|
|
1369
|
+
_auto_run_on_user_open(editor_ds, store_obj)
|
|
1370
|
+
if open_now and win_ds is not None and "window_pos" not in comment_args:
|
|
1371
|
+
# Reopening: snap the window back to the LEFT of the editor
|
|
1372
|
+
# window (it may have been dragged onto the code). The window's
|
|
1373
|
+
# real size is known here, so the display clamps are exact.
|
|
1374
|
+
pos = _left_of_window_pos(
|
|
1375
|
+
editor_ds.abs_left if editor_ds is not None else None,
|
|
1376
|
+
x, marker_y=y, win_h=win_ds.height, win_w=win_ds.width)
|
|
1377
|
+
if pos is not None:
|
|
1378
|
+
win_ds.window_pos = pos
|
|
1379
|
+
ds.invalidate()
|
|
1380
|
+
|
|
1381
|
+
# Draw the value window only while it shows - plus ONE closing call when
|
|
1382
|
+
# it just stopped showing (closed=True must be stamped on the ds so the
|
|
1383
|
+
# deferred dispatch discards it; skipping that call would leave an orphan
|
|
1384
|
+
# window rendering from root_draw_states). A window the user X-closed is
|
|
1385
|
+
# already stamped, so a closed marker costs zero draw_any calls per
|
|
1386
|
+
# render - and the full per-publish watch below stops too, leaving only
|
|
1387
|
+
# the cheap first_only watch above.
|
|
1388
|
+
_show = open_now or preview_show
|
|
1389
|
+
if captured and (_show or (win_ds is not None and not win_ds.closed)):
|
|
1390
|
+
# Full (per-publish) watch once a window exists so the value streams
|
|
1391
|
+
# in - the first_only call above only flips the box green.
|
|
1392
|
+
watch(store_obj, key_path, ds)
|
|
1393
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
1394
|
+
# Named by the code line's STABLE name - the marker's own name (raw,
|
|
1395
|
+
# line number stripped), never the full line-keyed path and never
|
|
1396
|
+
# draw_state.line: the name hashes into the unique ID, so a line
|
|
1397
|
+
# number here re-identified the window every time a rerun/edit
|
|
1398
|
+
# re-stamped the site's line.
|
|
1399
|
+
win_kwargs = dict(
|
|
1400
|
+
name=f"{'/'.join(map(_display_key, key_path))}"
|
|
1401
|
+
f"##lv::{ds.name}",
|
|
1402
|
+
mode=Modes.LIVE_WINDOW, closed=not (open_now or preview_show),
|
|
1403
|
+
open_requested=bool(preview_show),
|
|
1404
|
+
with_header=draw_header, disable_scroll=True, return_extras=True,
|
|
1405
|
+
# Anchor like a context menu: pinned to the marker, so the window
|
|
1406
|
+
# tracks it live and takes the pinned base's clamp - it rides the
|
|
1407
|
+
# code only as far as the editor window's edges instead of
|
|
1408
|
+
# chasing the marker off screen. (Swoosh style, alone: these
|
|
1409
|
+
# get the ribbon.) Both anchors are TOP_LEFT so the pinned base is
|
|
1410
|
+
# the marker's top-left - exactly what the window_pos offsets below
|
|
1411
|
+
# are measured from. hide_offscreen=False keeps it drawn once the
|
|
1412
|
+
# marker itself scrolls away: the window is what keeps it on screen.
|
|
1413
|
+
pin_to_clip=Pin.PARENT, anchor=Anchor.TOP_LEFT,
|
|
1414
|
+
parent_anchor=Anchor.TOP_LEFT, hide_offscreen=False,
|
|
1415
|
+
# Edits made anywhere in this window's subtree (params panel,
|
|
1416
|
+
# popup menus) should land on this site's `# [...]` comment -
|
|
1417
|
+
# set_anywhere reads the flag off the window's kwargs (walking
|
|
1418
|
+
# up from nested windows) and creates the binding there if the
|
|
1419
|
+
# comment hasn't set the param yet.
|
|
1420
|
+
preferred_source="code comment")
|
|
1421
|
+
# First creation: anchor the window to the LEFT of the editor's
|
|
1422
|
+
# window, always on top of the code, lifted clear of the editor
|
|
1423
|
+
# bottom (estimated height - the real one doesn't exist yet).
|
|
1424
|
+
# window_pos persists on the spawned window's draw_state
|
|
1425
|
+
# (parent-relative, so it tracks the editor window) - set once; user
|
|
1426
|
+
# drags it preserved after.
|
|
1427
|
+
if win_ds is None and "window_pos" not in comment_args:
|
|
1428
|
+
pos = _left_of_window_pos(
|
|
1429
|
+
editor_ds.abs_left if editor_ds is not None else None,
|
|
1430
|
+
x, marker_y=y)
|
|
1431
|
+
if pos is not None:
|
|
1432
|
+
win_kwargs["window_pos"] = pos
|
|
1433
|
+
elif win_ds is not None:
|
|
1434
|
+
# REUSE the tracked window draw_state: draw_any routes by VALUE
|
|
1435
|
+
# type and folds the render func into the unique, so a value
|
|
1436
|
+
# whose type changed between runs (None→tensor, int→str...) would
|
|
1437
|
+
# otherwise mint a fresh same-named ds - position/size/open
|
|
1438
|
+
# state gone, the old window orphaned in root_draw_states as a
|
|
1439
|
+
# duplicate-ID phantom. Pinning the ds keeps the window's
|
|
1440
|
+
# identity; the wrapper restamps _view_func per call, so the
|
|
1441
|
+
# body still re-routes by the value type.
|
|
1442
|
+
win_kwargs["draw_state"] = win_ds
|
|
1443
|
+
# NEW VALUE → repaint, exactly once per publish: the window is a
|
|
1444
|
+
# deferred nested root, so this call only restamps its kwargs;
|
|
1445
|
+
# the publish's own invalidation (_notify_watchers, during the
|
|
1446
|
+
# run) will arrive BEFORE this marker re-renders; the window
|
|
1447
|
+
# repaints on the new input, and a restamped kwargs is
|
|
1448
|
+
# not itself a dirty signal: the window then held its last
|
|
1449
|
+
# frame (voxels) or the run-start None ("No view for type")
|
|
1450
|
+
# until something else triggered it. Gate on identity: an
|
|
1451
|
+
# unchanged value never invalidates.
|
|
1452
|
+
if (getattr(win_ds, "_raw_input_value", _NO_VALUE) is not value
|
|
1453
|
+
and win_ds._tile_id is not None):
|
|
1454
|
+
Core.melty.cache.invalidate_up(win_ds._tile_id, force=True,
|
|
1455
|
+
max_depth=8)
|
|
1456
|
+
_c, _v, win_ds = draw_any(value, **(win_kwargs | comment_args))
|
|
1457
|
+
# Showstop: if the framework still handed back a different ds
|
|
1458
|
+
# (a path that ignores the pinned draw_state), close the replaced
|
|
1459
|
+
# one on the spot so it can never orphan.
|
|
1460
|
+
_prev_win = getattr(ds, "_lv_window_ds", None)
|
|
1461
|
+
if (_prev_win is not None and _prev_win is not win_ds
|
|
1462
|
+
and not _prev_win.closed):
|
|
1463
|
+
_prev_win.closed = True
|
|
1464
|
+
ds._lv_window_ds = win_ds
|
|
1465
|
+
# The menu usually opens on the WINDOW - stamp the site context there
|
|
1466
|
+
# too (the _parent chain isn't guaranteed to pass through this marker
|
|
1467
|
+
# after a root_draw_states re-dispatch).
|
|
1468
|
+
win_ds.live_root = live_root
|
|
1469
|
+
win_ds.live_key = ds.live_key
|
|
1470
|
+
# Same contract as the marker's (see current_live_root): the window
|
|
1471
|
+
# outlives this render - its replay re-splat and its panel's
|
|
1472
|
+
# set_anywhere must not trust a stamped tree a reparse may have
|
|
1473
|
+
# replaced since; they resolve through the host's held tree (memo
|
|
1474
|
+
# keyed by host identity, so it self-refreshes on every reparse).
|
|
1475
|
+
object.__setattr__(win_ds, "_lv_locator", _locator)
|
|
1476
|
+
from meltygui.core.windowing.window_visibility import override_state, user_window_closed
|
|
1477
|
+
_window_state = override_state(ds)
|
|
1478
|
+
if _window_state.pending_marker_closed is not None:
|
|
1479
|
+
user_window_closed(win_ds, _window_state.pending_marker_closed)
|
|
1480
|
+
_window_state.pending_marker_closed = None
|
|
1481
|
+
# The window's dispatch (Melty.draw, root_draw_states) re-reads
|
|
1482
|
+
# the CURRENT value for this key from the store, so a publish while
|
|
1483
|
+
# this marker is culled off-viewport still swaps the window's tensor
|
|
1484
|
+
# (fresh display, and the previous generation is released on the
|
|
1485
|
+
# spot instead of riding the stale kwargs until the marker next
|
|
1486
|
+
# renders).
|
|
1487
|
+
win_ds._lv_store_obj = store_obj
|
|
1488
|
+
win_ds._lv_key_path = key_path
|
|
1489
|
+
# Auto loop dims for the REPLAY path: the deferred root_draw_states
|
|
1490
|
+
# dispatch re-splats the site's raw `# [...]` comment over the stored
|
|
1491
|
+
# kwargs (meltygui.py, "Live-view comment re-splat"), and would clobber
|
|
1492
|
+
# the merged dim_names above with the comment's un-merged list. Stamp
|
|
1493
|
+
# the raw names and the value's dim count so the replay can redo the
|
|
1494
|
+
# same loop + dim<i> padding.
|
|
1495
|
+
win_ds._lv_auto_dims = _merge_auto
|
|
1496
|
+
win_ds._lv_ndim = _ndim
|
|
1497
|
+
# Window X-close detection: the header's close button runs DURING the
|
|
1498
|
+
# draw_any call above, so a caret-held preview closed this instant
|
|
1499
|
+
# shows as closed=True right after a window it passed closed=False.
|
|
1500
|
+
# Dismiss immediately - deterministic, no dependence on which order
|
|
1501
|
+
# the window and the editor re-render in adjacent frames.
|
|
1502
|
+
if (preview_show and cursor_inside and not open_now
|
|
1503
|
+
and win_ds.closed):
|
|
1504
|
+
Core.melty.clear_focus()
|
|
1505
|
+
ds._lv_cursor_dismissed = True
|
|
1506
|
+
ds._lv_cursor_in = False
|
|
1507
|
+
cursor_inside = False
|
|
1508
|
+
ds.invalidate()
|
|
1509
|
+
# The X on a caret-held preview parks the hint too (same
|
|
1510
|
+
# contract as the latched window above).
|
|
1511
|
+
_drop_captured_value(store_obj, key_path)
|
|
1512
|
+
|
|
1513
|
+
# Stamp whether THIS frame's window visibility is caret-held - the X-close
|
|
1514
|
+
# should only fire for a window the cursor preview was in.
|
|
1515
|
+
ds._lv_cursor_preview_shown = bool(preview_show and cursor_inside)
|
|
1516
|
+
|
|
1517
|
+
# A closed value window must not keep its last tensor (+ GL texture)
|
|
1518
|
+
# alive while the store streams on - release once per close (the flag
|
|
1519
|
+
# re-arms on the next show, when draw_any re-supplies the value).
|
|
1520
|
+
if win_ds is not None and win_ds.closed and not _show:
|
|
1521
|
+
if not getattr(win_ds, "_lv_released", False):
|
|
1522
|
+
win_ds._lv_released = True
|
|
1523
|
+
release_live_value(win_ds)
|
|
1524
|
+
# A closed window no longer earns its stack: drop the key's
|
|
1525
|
+
# accumulator and park the rerun hint in the store (the key
|
|
1526
|
+
# stays, the marker stays green), severing the marker/window
|
|
1527
|
+
# pins so nothing references the value any more.
|
|
1528
|
+
try:
|
|
1529
|
+
from meltygui.code.live_view import park_rerun_hint
|
|
1530
|
+
park_rerun_hint(store_obj, key_path)
|
|
1531
|
+
except Exception as e:
|
|
1532
|
+
print(f"live_view: park hint for {key_path} failed: {e!r}")
|
|
1533
|
+
try:
|
|
1534
|
+
from meltygui.core.runtime.gc_manager import release_cuda_cache_soon
|
|
1535
|
+
release_cuda_cache_soon(label="live window close")
|
|
1536
|
+
except Exception:
|
|
1537
|
+
pass
|
|
1538
|
+
elif win_ds is not None and getattr(win_ds, "_lv_released", False):
|
|
1539
|
+
win_ds._lv_released = False
|
|
1540
|
+
|
|
1541
|
+
# The marker itself must not outlive the value it was handed: the
|
|
1542
|
+
# framework stores this call's kwargs on the ds (_kwargs), and a marker
|
|
1543
|
+
# that isn't rendered again (scrolled off; culled; key pruned) would pin
|
|
1544
|
+
# the tensor until the next time it draws. The value was only ever
|
|
1545
|
+
# needed inside this body (the window got its own copy via draw_any).
|
|
1546
|
+
_kw = ds.__dict__.get("_kwargs")
|
|
1547
|
+
if isinstance(_kw, dict) and _kw.get("value") is not None:
|
|
1548
|
+
_kw["value"] = None
|
|
1549
|
+
|
|
1550
|
+
return False, None
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def draw_snapshot_overlay(x=0, y=0, w=0, h=0, draw_state=None, char_w=8.0,
|
|
1554
|
+
line_px=20.0, node=None, span=None, root=None,
|
|
1555
|
+
line_offset=0, jump_to=None, **kwargs):
|
|
1556
|
+
"""Per-FUNCTION-scope overlay: anchor every captured value that has NO
|
|
1557
|
+
live_view token to anchor to — assignment keys published by an
|
|
1558
|
+
instrumented twin (live_instrument) and line-keyed sites in while/with
|
|
1559
|
+
bodies — with the same marker/window/watcher stack as the call tokens.
|
|
1560
|
+
|
|
1561
|
+
Registered for GeneralParse, so the walk calls it for many nodes; it acts
|
|
1562
|
+
only on def scopes (__cst__ FunctionDef). Deliberately NO identity checks
|
|
1563
|
+
against `root`: the code-host route hands the walk Bubbling proxy wrappers
|
|
1564
|
+
whose identities don't survive re-access, which is exactly how the first
|
|
1565
|
+
root-guarded version of this overlay silently never ran."""
|
|
1566
|
+
from meltygui.code.live_view import live_values_for
|
|
1567
|
+
from meltygui.code.live_view import watch
|
|
1568
|
+
from meltygui.editor.live_view_views import _NO_VALUE
|
|
1569
|
+
from meltygui.editor.live_view_views import _SWATCH_HOLE
|
|
1570
|
+
from meltygui.editor.live_view_views import _draw_marker_at
|
|
1571
|
+
from meltygui.editor.live_view_views import _draw_usage_labels
|
|
1572
|
+
from meltygui.editor.live_view_views import _inline_swatch_rgba
|
|
1573
|
+
from meltygui.editor.live_view_views import _inline_value_text
|
|
1574
|
+
from meltygui.editor.live_view_views import _is_funcdef_node
|
|
1575
|
+
from meltygui.editor.live_view_views import _key_anchor
|
|
1576
|
+
from meltygui.editor.live_view_views import _label_line_index
|
|
1577
|
+
from meltygui.editor.live_view_views import _line_in_selection
|
|
1578
|
+
from meltygui.editor.live_view_views import _marker_idle_skip
|
|
1579
|
+
from meltygui.editor.live_view_views import _node_owns_function
|
|
1580
|
+
from meltygui.editor.live_view_views import _parse_line_band
|
|
1581
|
+
from meltygui.editor.live_view_views import _scope_function
|
|
1582
|
+
from meltygui.editor.live_view_views import _snap_line_to_label
|
|
1583
|
+
from meltygui.editor.live_view_views import _snap_line_to_text
|
|
1584
|
+
from meltygui.editor.live_view_views import _stable_key_name
|
|
1585
|
+
from meltygui.editor.live_view_views import _store_key_names
|
|
1586
|
+
from meltygui.editor.live_view_views import _symbol_cols
|
|
1587
|
+
from meltygui.editor.live_view_views import _token_in_selection
|
|
1588
|
+
from meltygui.editor.live_view_views import is_volume
|
|
1589
|
+
|
|
1590
|
+
if not _is_funcdef_node(node) or span is None:
|
|
1591
|
+
return
|
|
1592
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1593
|
+
live_store = kwargs.get("live_store")
|
|
1594
|
+
if live_store is None and not Toggles.TextEditor.enable_live_view:
|
|
1595
|
+
return
|
|
1596
|
+
if live_store is not None:
|
|
1597
|
+
# PASSED-IN store (draw_text's live_store=, e.g. the stack trace
|
|
1598
|
+
# window): no global resolution at all - the caller computed the
|
|
1599
|
+
# values locally (live_view.frame_value_store), and this overlay
|
|
1600
|
+
# reads only what it was handed. Act on exactly the def the store
|
|
1601
|
+
# was built for: name match (rules out enclosing defs, whose spans
|
|
1602
|
+
# also contain the target's lines) + the store's def line inside
|
|
1603
|
+
# this node's span (rules out unrelated same-named defs).
|
|
1604
|
+
from meltygui.code.libcst_conversion import parse_def_name
|
|
1605
|
+
_def_line = getattr(live_store, "__def_line__", None)
|
|
1606
|
+
if (_def_line is None
|
|
1607
|
+
or parse_def_name(node) != getattr(live_store, "__name__", None)
|
|
1608
|
+
or not (span.start_line + line_offset <= _def_line
|
|
1609
|
+
<= getattr(span, "end_line", span.start_line)
|
|
1610
|
+
+ line_offset)):
|
|
1611
|
+
return
|
|
1612
|
+
fn = live_store
|
|
1613
|
+
else:
|
|
1614
|
+
filename = (getattr(root, "file_path", None)
|
|
1615
|
+
or getattr(getattr(root, "address", None), "path", None)
|
|
1616
|
+
or getattr(jump_to, "path", None))
|
|
1617
|
+
if filename is None:
|
|
1618
|
+
return
|
|
1619
|
+
fn = _scope_function(str(filename), span.start_line + line_offset)
|
|
1620
|
+
if fn is None:
|
|
1621
|
+
return
|
|
1622
|
+
# A NESTED def resolves to its ENCLOSING function (closures attach
|
|
1623
|
+
# their captures to the outer store and are no module var, so the
|
|
1624
|
+
# nearest module-level def at or above the line wins). Its keys are
|
|
1625
|
+
# line-keyed and _key_anchor reads the line alone, so this node's
|
|
1626
|
+
# overlay would repaint every visible key of the outer store with
|
|
1627
|
+
# the SAME view as the outer node's overlay already used - the
|
|
1628
|
+
# red "ID ..." duplicate label over the outer body (09-02). Only the
|
|
1629
|
+
# node that IS the resolved function's own def draws it.
|
|
1630
|
+
if not _node_owns_function(node, fn, span, line_offset):
|
|
1631
|
+
return
|
|
1632
|
+
# Store-level registration before any markers exist: the first
|
|
1633
|
+
# instrumented run's brand-new keys invalidate this editor, the
|
|
1634
|
+
# overlay re-runs, and the markers materialize (closed - these
|
|
1635
|
+
# auto_open=False boxes wait for a click). Without it the first run
|
|
1636
|
+
# stays invisible until an instrument happens. (A passed-in store is
|
|
1637
|
+
# a static snapshot - nothing will ever publish to it, so no watch.)
|
|
1638
|
+
watch(fn, None, draw_state)
|
|
1639
|
+
|
|
1640
|
+
# Parse→buffer line bridge (dispatch passes it while a merge is in
|
|
1641
|
+
# flight): the given y is already mapped, so deriving origin from the
|
|
1642
|
+
# MAPPED start keeps origin == buffer line 1; each anchor then maps
|
|
1643
|
+
# individually - lines above an edit stay, lines below shift, lines
|
|
1644
|
+
# within the changed region skip the frame. Content lookups keep the
|
|
1645
|
+
# PARSE-space line: outside the changed region both texts hold the
|
|
1646
|
+
# identical line, by construction of the diff.
|
|
1647
|
+
_lmap = kwargs.get("line_map")
|
|
1648
|
+
_sl = _lmap(span.start_line) if _lmap else span.start_line
|
|
1649
|
+
if _sl is None:
|
|
1650
|
+
return
|
|
1651
|
+
origin_y = y - (_sl - 1) * line_px
|
|
1652
|
+
origin_x = x - getattr(span, "start_col", 0) * char_w
|
|
1653
|
+
_src = getattr(root, "source", "") or ""
|
|
1654
|
+
# Viewport cull bounds: the store can have a marker per binding in the
|
|
1655
|
+
# def (frame snapshots publish the whole scope), and the walk visits the
|
|
1656
|
+
# scope regardless of scroll - every off-screen marker skipped here is a
|
|
1657
|
+
# full render_func call saved per frame. Latched value windows persist
|
|
1658
|
+
# via root_draw_states without a marker, same as when the whole editor
|
|
1659
|
+
# scrolls away.
|
|
1660
|
+
_clip = getattr(draw_state, "abs_clip_rect", None)
|
|
1661
|
+
if live_store is not None:
|
|
1662
|
+
# PASSED-IN store (the stack trace view's panes): the pane is a
|
|
1663
|
+
# BOUNDED span whose tile bakes once and then blitted while the
|
|
1664
|
+
# PARENT scrolls - its own body doesn't re-run per scroll frame,
|
|
1665
|
+
# so a clip cull here baked only the then-visible band's markers
|
|
1666
|
+
# and they popped in/out as the scroll crossed rows (08-31;
|
|
1667
|
+
# re-confirmed 09-01: with the cull, draw_text's 5.6k-line pane
|
|
1668
|
+
# showed gutter markers only for the first ~180 lines). Render
|
|
1669
|
+
# every marker - so the per-key scan below must stay cheap: for
|
|
1670
|
+
# that pane it runs 5k+ regex scans every selection frame.
|
|
1671
|
+
_clip = None
|
|
1672
|
+
# Snap memo: the label/content relocation scans are O(def lines) per
|
|
1673
|
+
# STALE stamp - with frame snapshots holding a key per occurrence that's
|
|
1674
|
+
# hundreds of the scans per repaint if run hot. Snap results only
|
|
1675
|
+
# change when the text changes, so memoize per (stamp, label) against
|
|
1676
|
+
# the source OBJECT - identity is the content-free change signal (a
|
|
1677
|
+
# reparse builds a new string; edits-in-flight are the _lmap's job).
|
|
1678
|
+
# Per-EDITOR (this draw_state) because the same scope can be overlaid from
|
|
1679
|
+
# several editors at once (same def in two tiles), each with its own
|
|
1680
|
+
# source object - a shared memo would ping-pong between their sources
|
|
1681
|
+
# and rescan every stamp every frame. Raw-written like the other editor
|
|
1682
|
+
# memo caches (_anc_scroll_cache): @live's __setattr__ would run a
|
|
1683
|
+
# value != original_value compare on full value-carrying tuples.
|
|
1684
|
+
# The tuple also carries the module source's line split: splitting the
|
|
1685
|
+
# WHOLE file per FunctionDef per frame is ~2/3 of draw_text in profiling
|
|
1686
|
+
# - same invalidation (source object identity), so it rides the memo.
|
|
1687
|
+
_memo_ent = draw_state.__dict__.get("_lv_snap_memo")
|
|
1688
|
+
if _memo_ent is None or _memo_ent[0] is not _src or len(_memo_ent) < 3:
|
|
1689
|
+
_memo_ent = (_src, {}, _src.split("\n"))
|
|
1690
|
+
object.__setattr__(draw_state, "_lv_snap_memo", _memo_ent)
|
|
1691
|
+
_snap_memo = _memo_ent[1]
|
|
1692
|
+
source_lines = _memo_ent[2]
|
|
1693
|
+
# Exit-line washes: where the last instrumented run CAME OUT.
|
|
1694
|
+
# __live_return_line__ (stamped by live_view.twin_ret / the body-capture
|
|
1695
|
+
# profile hook) washes green; __live_error_line__ ((line, msg, text),
|
|
1696
|
+
# stamped
|
|
1697
|
+
# by live_instrument._stamp_error_line when the run raised) washes red
|
|
1698
|
+
# with the message in a wrapped box flush above the line, right-aligned -
|
|
1699
|
+
# the live-run twin of the editor's routed error markers. Both are absolute file coords, mapped
|
|
1700
|
+
# through the same parse→buffer bridge as the anchors; both cleared at
|
|
1701
|
+
# run start, so a rerun never shows the previous run's exit. A couple of
|
|
1702
|
+
# attribute accesses + at most two rects per frame.
|
|
1703
|
+
_exit_marks = []
|
|
1704
|
+
_ret_mark = getattr(fn, "__live_return_line__", None)
|
|
1705
|
+
if _ret_mark:
|
|
1706
|
+
_rline, _rtext = (_ret_mark if isinstance(_ret_mark, tuple)
|
|
1707
|
+
else (_ret_mark, None))
|
|
1708
|
+
_exit_marks.append((_rline, None, _rtext,
|
|
1709
|
+
(0.157, 0.824, 0.31, 0.16)))
|
|
1710
|
+
_err_mark = getattr(fn, "__live_error_line__", None)
|
|
1711
|
+
if _err_mark:
|
|
1712
|
+
_exit_marks.append((_err_mark[0], _err_mark[1],
|
|
1713
|
+
_err_mark[2] if len(_err_mark) > 2 else None,
|
|
1714
|
+
(0.824, 0.157, 0.157, 0.22)))
|
|
1715
|
+
for _ml_line, _ml_msg, _ml_text, _ml_col in _exit_marks:
|
|
1716
|
+
# Same follow-the-code snap as the markers: the stamp is run-time
|
|
1717
|
+
# coordinates, so if edits moved the statement, re-find it by its
|
|
1718
|
+
# stamped CONTENT inside the def before mapping to buffer space.
|
|
1719
|
+
_mk = ("exit", _ml_line, _ml_text)
|
|
1720
|
+
_rl = _snap_memo.get(_mk)
|
|
1721
|
+
if _rl is None:
|
|
1722
|
+
_rl = _snap_line_to_text(_ml_text, _ml_line - line_offset,
|
|
1723
|
+
source_lines,
|
|
1724
|
+
span.start_line, span.end_line)
|
|
1725
|
+
_snap_memo[_mk] = _rl
|
|
1726
|
+
_rlm = _lmap(_rl) if _lmap else _rl
|
|
1727
|
+
if _rlm is None or _rlm < 1:
|
|
1728
|
+
continue
|
|
1729
|
+
_ry = origin_y + (_rlm - 1) * line_px
|
|
1730
|
+
if _clip is not None and (_ry + line_px < _clip[1]
|
|
1731
|
+
or _ry > _clip[3]):
|
|
1732
|
+
continue
|
|
1733
|
+
_rdl = imgui.get_window_draw_list()
|
|
1734
|
+
_cw = getattr(draw_state, "content_width", 800.0)
|
|
1735
|
+
_rdl.add_rect_filled(
|
|
1736
|
+
origin_x - 4.0, _ry, origin_x + _cw, _ry + line_px,
|
|
1737
|
+
pack_color(*_ml_col))
|
|
1738
|
+
if _ml_msg:
|
|
1739
|
+
# Same treatment as the editor's parse-error box (text_editor's
|
|
1740
|
+
# draw_text): a wrapped, capped-width box sitting flush ABOVE the
|
|
1741
|
+
# line, right-aligned, so the box never covers the code.
|
|
1742
|
+
_pad_x, _pad_y, _margin = 6, 4, 6
|
|
1743
|
+
_bx1 = (_clip[2] if _clip is not None
|
|
1744
|
+
else origin_x + _cw) - _margin
|
|
1745
|
+
_max_w = min(420.0, max(80.0, (_bx1 - origin_x) - 2 * _pad_x))
|
|
1746
|
+
_ts = imgui.calc_text_size(_ml_msg, False, _max_w)
|
|
1747
|
+
_bx0 = _bx1 - (_ts.x + 2 * _pad_x)
|
|
1748
|
+
_by1 = _ry
|
|
1749
|
+
_by0 = _by1 - (_ts.y + 2 * _pad_y)
|
|
1750
|
+
if _clip is not None and _by0 < _clip[1] + _margin:
|
|
1751
|
+
_by0 = _ry + line_px # no room above - box below
|
|
1752
|
+
_by1 = _by0 + _ts.y + 2 * _pad_y
|
|
1753
|
+
_rdl.add_rect_filled(
|
|
1754
|
+
_bx0, _by0, _bx1, _by1,
|
|
1755
|
+
pack_color(0.275, 0.118, 0.157, 0.922), 4.0)
|
|
1756
|
+
_rdl.add_rect(
|
|
1757
|
+
_bx0, _by0, _bx1, _by1,
|
|
1758
|
+
pack_color(0.588, 0.235, 0.275, 1.0), 4.0)
|
|
1759
|
+
_save_cursor = imgui.get_cursor_screen_pos()
|
|
1760
|
+
imgui.set_cursor_screen_pos((_bx0 + _pad_x, _by0 + _pad_y))
|
|
1761
|
+
imgui.push_text_wrap_pos(imgui.get_cursor_pos_x() + _max_w)
|
|
1762
|
+
imgui.text_colored(_ml_msg, 1.0, 0.72, 0.68, 1.0)
|
|
1763
|
+
imgui.pop_text_wrap_pos()
|
|
1764
|
+
imgui.set_cursor_screen_pos(_save_cursor)
|
|
1765
|
+
_sot0 = time.perf_counter()
|
|
1766
|
+
_soc = [0, 0, 0, 0] # keys seen, culled(index+clip), idle-skipped, drawn
|
|
1767
|
+
_snap_vals = live_values_for(fn)
|
|
1768
|
+
_soc[0] = len(_snap_vals)
|
|
1769
|
+
|
|
1770
|
+
# Line-bucketed anchor index: resolving an anchor per key per frame is
|
|
1771
|
+
# O(store) - a frame-snapshotted big def holds THOUSANDS of keys, nearly
|
|
1772
|
+
# all off-viewport, and during an edit burst each also paid an _lmap
|
|
1773
|
+
# call (this was the measured 12-16ms/frame). Anchors only move when the
|
|
1774
|
+
# source or the key set changes, so resolve them ONCE into a sorted
|
|
1775
|
+
# (rel_line, key) list and bisect the visible band per frame. rel_line
|
|
1776
|
+
# is pre-_lmap (parse space); the 64-line slack on the band covers any
|
|
1777
|
+
# plausible in-buffer region shift until the reparse rebuilds _src (which
|
|
1778
|
+
# rebuilds the index - same identity signal as _snap_memo).
|
|
1779
|
+
_ik = (len(_snap_vals), line_offset)
|
|
1780
|
+
_ie = draw_state.__dict__.get("_lv_key_index")
|
|
1781
|
+
# The function is part of the identity; a def view's exec-fn runs
|
|
1782
|
+
# publish to a FRESH function object per run, so a stale index would
|
|
1783
|
+
# iterate the PREVIOUS run's key paths against the new values - every
|
|
1784
|
+
# lookup None, rendered as captured markers (the phantom "second set"),
|
|
1785
|
+
# whose None-routed windows then collide with the real ones. Weakref so
|
|
1786
|
+
# the index never pins a replaced run's function (id() of a dead object
|
|
1787
|
+
# could recycle).
|
|
1788
|
+
if (_ie is None or _ie[0] is not _src or _ie[1] != _ik
|
|
1789
|
+
or len(_ie) < 8 or _ie[4]() is not fn):
|
|
1790
|
+
# Append + ONE sort (C-speed): the first version insort-ed each key
|
|
1791
|
+
# (list.insert, O(n) memmove → O(n²) per rebuild), and a publish
|
|
1792
|
+
# storm - a stack-trace snapshot landing thousands of keys with
|
|
1793
|
+
# renders interleaved - meant a rebuild per render. That froze
|
|
1794
|
+
# the editor the moment a stack was published.
|
|
1795
|
+
_pairs = []
|
|
1796
|
+
_ilabels = getattr(fn, "__live_labels__", None) or {}
|
|
1797
|
+
_cols_memo = draw_state.__dict__.get("_lv_cols_memo")
|
|
1798
|
+
if _cols_memo is None or len(_cols_memo) > 200000:
|
|
1799
|
+
_cols_memo = {}
|
|
1800
|
+
object.__setattr__(draw_state, "_lv_cols_memo", _cols_memo)
|
|
1801
|
+
for key_path in _snap_vals:
|
|
1802
|
+
if not key_path or not isinstance(key_path[-1], str):
|
|
1803
|
+
continue
|
|
1804
|
+
if key_path[-1].split("#", 1)[0] == "live_view()":
|
|
1805
|
+
continue # anchored by its own call-token marker
|
|
1806
|
+
_a = _key_anchor(node, key_path, line_offset)
|
|
1807
|
+
if _a is None:
|
|
1808
|
+
continue
|
|
1809
|
+
_rl = _a[0]
|
|
1810
|
+
if _a[2] is None:
|
|
1811
|
+
tail = key_path[-1]
|
|
1812
|
+
_mk = ("label", tail)
|
|
1813
|
+
_snapped = _snap_memo.get(_mk)
|
|
1814
|
+
if _snapped is None:
|
|
1815
|
+
# Label→line index, built ONCE per source version: after
|
|
1816
|
+
# an edit shifts lines, EVERY stored label mismatches at
|
|
1817
|
+
# the next reparse, and the old per-key def-wide regex
|
|
1818
|
+
# scan cost keys × def-lines (2640 × 2900 ≈ 9.6 SECONDS,
|
|
1819
|
+
# the post-edit baseline). With the index each key is a
|
|
1820
|
+
# dict hit + nearest-line bisect.
|
|
1821
|
+
_lidx = _snap_memo.get(("lidx",))
|
|
1822
|
+
if _lidx is None:
|
|
1823
|
+
_lidx = _label_line_index(
|
|
1824
|
+
source_lines, span.start_line, span.end_line)
|
|
1825
|
+
_snap_memo[("lidx",)] = _lidx
|
|
1826
|
+
label = ((getattr(fn, "__live_labels__", None) or {})
|
|
1827
|
+
.get(key_path)
|
|
1828
|
+
or (tail.split("#", 1)[1] if "#" in tail else None))
|
|
1829
|
+
_snapped = _snap_line_to_label(
|
|
1830
|
+
label, _rl, source_lines,
|
|
1831
|
+
span.start_line, span.end_line, lidx=_lidx)
|
|
1832
|
+
_snap_memo[_mk] = _snapped
|
|
1833
|
+
_rl = _snapped
|
|
1834
|
+
_cols = _symbol_cols(_a, _rl, key_path, source_lines, _ilabels,
|
|
1835
|
+
memo=_cols_memo)
|
|
1836
|
+
if _cols is None:
|
|
1837
|
+
continue
|
|
1838
|
+
_pairs.append((_rl, key_path, (_rl, _cols[0], _cols[1])))
|
|
1839
|
+
_pairs.sort(key=lambda p: p[0])
|
|
1840
|
+
# Stable hash-free cache for the whole snapshot, built WITH the index
|
|
1841
|
+
# (same invalidation) - per-key naming was O(store) hash → O(store²)
|
|
1842
|
+
# per pass on frame-snapshot stores.
|
|
1843
|
+
# The resolved anchor rides with its key: _key_anchor is a
|
|
1844
|
+
# locals-walk per key, and re-running it per line per frame
|
|
1845
|
+
# was O(visible band) of dict descents for every repaint.
|
|
1846
|
+
_ie = (_src, _ik, [p[0] for p in _pairs], [p[1] for p in _pairs],
|
|
1847
|
+
weakref.ref(fn), None, [p[2] for p in _pairs],
|
|
1848
|
+
{p[1]: i for i, p in enumerate(_pairs)})
|
|
1849
|
+
object.__setattr__(draw_state, "_lv_key_index", _ie)
|
|
1850
|
+
_ilines, _ikeys, _ianchors = _ie[2], _ie[3], _ie[6]
|
|
1851
|
+
# Names from the SHARED generation-keyed store map - never the anchor
|
|
1852
|
+
# index's own cache; differently-stale name maps across consumers
|
|
1853
|
+
# minted duplicate value IDs (see _stable_key_names).
|
|
1854
|
+
_skey_names = _store_key_names(fn)
|
|
1855
|
+
if (_clip is not None
|
|
1856
|
+
and getattr(draw_state, "_lv_full_overlay_until", 0)
|
|
1857
|
+
<= Core.melty.frame_count):
|
|
1858
|
+
_blo, _bhi = _parse_line_band(_lmap, _clip, origin_y, line_px)
|
|
1859
|
+
_i0 = bisect.bisect_left(_ilines, _blo)
|
|
1860
|
+
_i1 = bisect.bisect_right(_ilines, _bhi)
|
|
1861
|
+
_cand = _ikeys[_i0:_i1]
|
|
1862
|
+
_cand_anchors = _ianchors[_i0:_i1]
|
|
1863
|
+
_soc[1] = len(_ikeys) - len(_cand)
|
|
1864
|
+
else:
|
|
1865
|
+
# Post-run forward pass renders EVERY key in the loop - the per-key
|
|
1866
|
+
# cull below still drops off-viewport keys unless their marker has
|
|
1867
|
+
# an OPEN window (rendered at the frozen anchor, which forwards the
|
|
1868
|
+
# actual value into the window's draw_any).
|
|
1869
|
+
_cand = _ikeys
|
|
1870
|
+
_cand_anchors = _ianchors
|
|
1871
|
+
|
|
1872
|
+
# Binding-value pills, built by the marker loop and painted by the
|
|
1873
|
+
# trailing-gap pass: (display 0, boundary buffer col, "=value",
|
|
1874
|
+
# symbol length) - usage-label pairs, one per captured target.
|
|
1875
|
+
#
|
|
1876
|
+
# IDLE-PASS MEMO (the no-clip pane below: every key, every frame). An
|
|
1877
|
+
# idle key's whole per-frame outcome is its gutter registration and
|
|
1878
|
+
# its pill, and both only change with the index, a publish, the
|
|
1879
|
+
# geometry or the focus - all in `_pk`. On a memo hit only the ACTIVE
|
|
1880
|
+
# keys run the per-key logic: keys non-idle last pass (hovered, open,
|
|
1881
|
+
# caret-inside - they must observe their state), keys on the mouse's
|
|
1882
|
+
# line (for hover could begin), keys on an exact single-line selection
|
|
1883
|
+
# (`_token_in_selection`); everything else replays. draw_text's
|
|
1884
|
+
# context-menu pane: 5,145 keys → 70 ms a selection frame before.
|
|
1885
|
+
_sel_lo, _sel_hi = kwargs.get("sel_lo"), kwargs.get("sel_hi")
|
|
1886
|
+
_col_shift = kwargs.get("col_shift", 0)
|
|
1887
|
+
_fc = Core.melty.frame_count
|
|
1888
|
+
_full_pass = getattr(draw_state, "_lv_full_overlay_until", 0) > _fc
|
|
1889
|
+
try:
|
|
1890
|
+
_pub_gen = vars(fn).get("__live_pub_gen__", 0)
|
|
1891
|
+
except TypeError:
|
|
1892
|
+
_pub_gen = 0
|
|
1893
|
+
# The line map is either None, the fold projection (a fresh lambda per
|
|
1894
|
+
# frame carrying the layout list in `_d2b` - see draw_text's
|
|
1895
|
+
# _get_fold_lm; the list only changes on a fold toggle, so it is the
|
|
1896
|
+
# identity the memo keys on) or an edit bridge (a change in flight - no
|
|
1897
|
+
# memo, every frame is a potential change).
|
|
1898
|
+
_layout = getattr(_lmap, "_d2b", None) if _lmap is not None else None
|
|
1899
|
+
_lm_key = getattr(_lmap, "_lv_key", None) if _lmap is not None else None
|
|
1900
|
+
_pk = (id(_ie), _pub_gen, origin_x, origin_y, line_px, char_w,
|
|
1901
|
+
_col_shift, Core.melty.text_focused_ds is draw_state,
|
|
1902
|
+
_lm_key)
|
|
1903
|
+
_record = (_clip is None and not _full_pass
|
|
1904
|
+
and (_lmap is None or _lm_key is not None))
|
|
1905
|
+
_pm = draw_state.__dict__.get("_lv_pass_memo")
|
|
1906
|
+
_replay = (_record and _pm is not None and len(_pm) >= 7
|
|
1907
|
+
and _pm[0] == _pk)
|
|
1908
|
+
_bind_pills = []
|
|
1909
|
+
if _replay:
|
|
1910
|
+
_idle_gutter, _pills, _nonidle = _pm[1], _pm[2], _pm[3]
|
|
1911
|
+
_static_gutter, _static_pills = _pm[5], _pm[6]
|
|
1912
|
+
_active = set(_nonidle)
|
|
1913
|
+
|
|
1914
|
+
def _mark_display_line(dl):
|
|
1915
|
+
# display line (1-based) → parse line: identity without folds,
|
|
1916
|
+
# else through the fold layout (0-based parse line per
|
|
1917
|
+
# display line); hidden / out-of-range lines have no keys.
|
|
1918
|
+
if _layout is None:
|
|
1919
|
+
rel = dl
|
|
1920
|
+
elif 0 <= dl - 1 < len(_layout):
|
|
1921
|
+
rel = _layout[dl - 1] + 1
|
|
1922
|
+
else:
|
|
1923
|
+
return
|
|
1924
|
+
_active.update(_ikeys[bisect.bisect_left(_ilines, rel):
|
|
1925
|
+
bisect.bisect_right(_ilines, rel)])
|
|
1926
|
+
if line_px:
|
|
1927
|
+
_mdl = int((imgui.get_io().mouse_pos.y - origin_y) / line_px) + 1
|
|
1928
|
+
for _dl in (_mdl - 1, _mdl, _mdl + 1):
|
|
1929
|
+
_mark_display_line(_dl)
|
|
1930
|
+
if (_sel_lo is not None and _sel_hi is not None
|
|
1931
|
+
and _sel_lo[0] == _sel_hi[0]):
|
|
1932
|
+
_mark_display_line(_sel_lo[0])
|
|
1933
|
+
# Replay the idle keys' gutter registration (same per-frame reset
|
|
1934
|
+
# the loop body / idle skip do) and their pills - from the memo's
|
|
1935
|
+
# STATIC structures, not a rebuild (5k entries a frame): the
|
|
1936
|
+
# gutter map is a ChainMap over a fresh front dict (the active
|
|
1937
|
+
# keys' markers get a filtered copy there so their own registration
|
|
1938
|
+
# can't duplicate the static entry); the pill list is the memo's
|
|
1939
|
+
# own object (copied only when an active key changes it - its
|
|
1940
|
+
# identity keys _stamp_and_paint's merge memo).
|
|
1941
|
+
_pos = _ie[7]
|
|
1942
|
+
if getattr(draw_state, "_lv_gutter_frame", None) != _fc:
|
|
1943
|
+
draw_state._lv_gutter_frame = _fc
|
|
1944
|
+
_front = {}
|
|
1945
|
+
for _k in _active:
|
|
1946
|
+
_ig = _idle_gutter.get(_k)
|
|
1947
|
+
if _ig is not None and _ig[0] not in _front:
|
|
1948
|
+
_front[_ig[0]] = [m for m in _static_gutter.get(_ig[0], ())
|
|
1949
|
+
if m is not _ig[1]]
|
|
1950
|
+
draw_state._lv_gutter_markers = collections.ChainMap(
|
|
1951
|
+
_front, _static_gutter)
|
|
1952
|
+
else:
|
|
1953
|
+
_gm = draw_state._lv_gutter_markers # another frame for first
|
|
1954
|
+
for _k, (_gl, _gmds) in _idle_gutter.items():
|
|
1955
|
+
if _k not in _active:
|
|
1956
|
+
_gm.setdefault(_gl, []).append(_gmds)
|
|
1957
|
+
_bind_pills = _static_pills
|
|
1958
|
+
for _k in _active:
|
|
1959
|
+
_p = _pills.get(_k)
|
|
1960
|
+
# A key non-idle when the memo was taken has no static pill
|
|
1961
|
+
# (it re-adds its own below); an idle one's is pulled so its
|
|
1962
|
+
# re-add can't duplicate it.
|
|
1963
|
+
if _p is not None and _k not in _nonidle:
|
|
1964
|
+
if _bind_pills is _static_pills:
|
|
1965
|
+
_bind_pills = list(_static_pills)
|
|
1966
|
+
try:
|
|
1967
|
+
_bind_pills.remove(_p)
|
|
1968
|
+
except ValueError:
|
|
1969
|
+
pass
|
|
1970
|
+
_loop = [(k, _ianchors[_pos[k]]) for k in _active if k in _pos]
|
|
1971
|
+
_soc[2] = len(_ikeys) - len(_loop)
|
|
1972
|
+
else:
|
|
1973
|
+
_idle_gutter, _pills = {}, {}
|
|
1974
|
+
_static_pills = None
|
|
1975
|
+
_loop = zip(_cand, _cand_anchors)
|
|
1976
|
+
_new_nonidle = set()
|
|
1977
|
+
_mutated = not _replay
|
|
1978
|
+
_created = 0
|
|
1979
|
+
_budget = int(Toggles.TextEditor.live_marker_create_budget)
|
|
1980
|
+
for key_path, anchor in _loop:
|
|
1981
|
+
value = _snap_vals.get(key_path)
|
|
1982
|
+
rel_line, start_col, end_col = anchor
|
|
1983
|
+
_ml = _lmap(rel_line) if _lmap else rel_line
|
|
1984
|
+
if _ml is None:
|
|
1985
|
+
continue # anchor inside the mid-edit region - skip a frame
|
|
1986
|
+
_my = origin_y + (_ml - 1) * line_px
|
|
1987
|
+
_frozen_pos = None
|
|
1988
|
+
if _clip is not None and (_my + line_px < _clip[1] or _my > _clip[3]):
|
|
1989
|
+
# Post-run forward pass (_lv_full_overlay_until): an off-viewport
|
|
1990
|
+
# key whose marker has an OPEN value window still renders once -
|
|
1991
|
+
# at the marker's LAST stamped location, NOT its true off-screen
|
|
1992
|
+
# spot (the pinned window would park at _pinned_base_y's screen-
|
|
1993
|
+
# bottom clamp and "disappear"). Everything else stays culled.
|
|
1994
|
+
_fmds = None
|
|
1995
|
+
if (getattr(draw_state, "_lv_full_overlay_until", 0)
|
|
1996
|
+
> Core.melty.frame_count):
|
|
1997
|
+
_mreg = getattr(draw_state, "_lv_marker_ds", None)
|
|
1998
|
+
_fmds = _mreg.get(
|
|
1999
|
+
f"lvs::{fn.__qualname__}"
|
|
2000
|
+
f"::{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}"
|
|
2001
|
+
) if _mreg else None
|
|
2002
|
+
_fw = getattr(_fmds, "_lv_window_ds", None) if _fmds else None
|
|
2003
|
+
if _fw is None or _fw.closed:
|
|
2004
|
+
_fmds = None
|
|
2005
|
+
if _fmds is None:
|
|
2006
|
+
# TEMP diag: an off-viewport key skipped DURING an active
|
|
2007
|
+
# full pass means its open window didn't reflect the run's
|
|
2008
|
+
# value - name why (no marker ds for the expected key, or
|
|
2009
|
+
# its window closed/missing).
|
|
2010
|
+
if (getattr(draw_state, "_lv_full_overlay_until", 0)
|
|
2011
|
+
> Core.melty.frame_count):
|
|
2012
|
+
from meltygui.core.diagnostics.perf_trace import trace as _ptr
|
|
2013
|
+
_mreg2 = getattr(draw_state, "_lv_marker_ds", None) or {}
|
|
2014
|
+
_mk2 = (f"lvs::{fn.__qualname__}::"
|
|
2015
|
+
f"{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}")
|
|
2016
|
+
_ptr("lv full-pass skip", key=_mk2,
|
|
2017
|
+
have_marker=_mk2 in _mreg2,
|
|
2018
|
+
reg_keys=len(_mreg2))
|
|
2019
|
+
_soc[1] += 1
|
|
2020
|
+
continue # off-viewport - don't draw a marker for it
|
|
2021
|
+
_frozen_pos = (_fmds.abs_left, _fmds.abs_top)
|
|
2022
|
+
pad = 2.0
|
|
2023
|
+
# Selection containment: _ml is in buffer-space, cols are the
|
|
2024
|
+
# boxed symbol span - same test the call-token overlay does.
|
|
2025
|
+
cursor_inside = _token_in_selection(
|
|
2026
|
+
_ml, start_col, end_col, _sel_lo, _sel_hi)
|
|
2027
|
+
_snm = (f"lvs::{fn.__qualname__}"
|
|
2028
|
+
f"::{_skey_names.get(key_path) or _stable_key_name(key_path, _snap_vals)}")
|
|
2029
|
+
_sao = (bool(Toggles.TextEditor.live_auto_open_volumes)
|
|
2030
|
+
and is_volume(value) and key_path not in
|
|
2031
|
+
(getattr(fn, "__frame_snapshot_keys__", None) or ()))
|
|
2032
|
+
# An inline-labeled marker (simple builtin value) paints NOTHING
|
|
2033
|
+
# itself: its pill is tracked here (_bind_pills) and painted raw
|
|
2034
|
+
# by _stamp_and_paint after the gap draw_text laid out. So an idle
|
|
2035
|
+
# one skips its @render_wrapper call like any other marker - once the
|
|
2036
|
+
# body has run at least once with the inline flag (the gutter glyph
|
|
2037
|
+
# and _lv_open button happen there; `_lv_inline` tracks it) - with
|
|
2038
|
+
# the body's full per-key watch replicated so a publish still
|
|
2039
|
+
# repaints the pill. Before this every visible captured binding of
|
|
2040
|
+
# a frame snapshot paid a full wrapper call per line: a big def
|
|
2041
|
+
# could draw_text (one binding on most lines) froze the editor.
|
|
2042
|
+
_btext = _inline_value_text(value)
|
|
2043
|
+
_pill = None
|
|
2044
|
+
if _btext is not None:
|
|
2045
|
+
_brgba = _inline_swatch_rgba(value)
|
|
2046
|
+
_pill = (_ml - 1, end_col + _col_shift,
|
|
2047
|
+
"=" + (_SWATCH_HOLE if _brgba is not None else "") + _btext,
|
|
2048
|
+
max(1, end_col - start_col),
|
|
2049
|
+
(_brgba, 1) if _brgba is not None else None)
|
|
2050
|
+
_mreg0 = draw_state.__dict__.get("_lv_marker_ds")
|
|
2051
|
+
_mds0 = _mreg0.get(_snm) if _mreg0 else None
|
|
2052
|
+
if (_frozen_pos is None
|
|
2053
|
+
and (_btext is None
|
|
2054
|
+
or (_mds0 is not None
|
|
2055
|
+
and getattr(_mds0, "_lv_inline", None) is True))
|
|
2056
|
+
and _marker_idle_skip(
|
|
2057
|
+
draw_state, _snm,
|
|
2058
|
+
origin_x + start_col * char_w - pad, _my - pad,
|
|
2059
|
+
max(1, end_col - start_col) * char_w + 2 * pad,
|
|
2060
|
+
line_px + 2 * pad, True, cursor_inside,
|
|
2061
|
+
fn, key_path, _ml - 1, _sao)):
|
|
2062
|
+
if _pill is not None:
|
|
2063
|
+
watch(fn, key_path, _mds0)
|
|
2064
|
+
if _bind_pills is _static_pills:
|
|
2065
|
+
_bind_pills = list(_static_pills)
|
|
2066
|
+
_bind_pills.append(_pill)
|
|
2067
|
+
if _record:
|
|
2068
|
+
if (_idle_gutter.get(key_path) != (_ml - 1, _mds0)
|
|
2069
|
+
or _pills.get(key_path, _NO_VALUE) != _pill):
|
|
2070
|
+
_mutated = True
|
|
2071
|
+
_idle_gutter[key_path] = (_ml - 1, _mds0)
|
|
2072
|
+
_pills[key_path] = _pill
|
|
2073
|
+
_soc[2] += 1
|
|
2074
|
+
continue
|
|
2075
|
+
if _mds0 is None and _frozen_pos is None and _budget > 0:
|
|
2076
|
+
# FIRST render of this marker (no draw_state yet) - a wrapper
|
|
2077
|
+
# call + DrawState + comment resolve each. A diff expand can
|
|
2078
|
+
# reveal thousands at once (15 s in one case, 09-01), so at
|
|
2079
|
+
# most `_budget` are created per pass; the rest stay pending
|
|
2080
|
+
# (nonidle state → active next pass) and the pills show now.
|
|
2081
|
+
if _created >= _budget:
|
|
2082
|
+
_new_nonidle.add(key_path)
|
|
2083
|
+
if _pill is not None:
|
|
2084
|
+
if _bind_pills is _static_pills:
|
|
2085
|
+
_bind_pills = list(_static_pills)
|
|
2086
|
+
_bind_pills.append(_pill)
|
|
2087
|
+
if _record:
|
|
2088
|
+
_mutated = True
|
|
2089
|
+
_pills[key_path] = _pill
|
|
2090
|
+
if _created == _budget:
|
|
2091
|
+
_created += 1
|
|
2092
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
2093
|
+
draw_state.invalidate()
|
|
2094
|
+
request_render()
|
|
2095
|
+
continue
|
|
2096
|
+
_created += 1
|
|
2097
|
+
_soc[3] += 1
|
|
2098
|
+
_new_nonidle.add(key_path)
|
|
2099
|
+
if _record:
|
|
2100
|
+
if key_path in _idle_gutter or _pills.get(key_path, _NO_VALUE) != _pill:
|
|
2101
|
+
_mutated = True
|
|
2102
|
+
_idle_gutter.pop(key_path, None)
|
|
2103
|
+
_pills[key_path] = _pill
|
|
2104
|
+
# Frozen-anchor path: the marker draws at its previous position so
|
|
2105
|
+
# its pinned window doesn't chase the true off-screen coords.
|
|
2106
|
+
# Auto-open the VOLUMES (3-D tensors → orbiting voxel windows) only
|
|
2107
|
+
# when live_auto_open_volumes is enabled - off by default: with loop
|
|
2108
|
+
# accumulation stacking per-layer tensors into volumes, a run would
|
|
2109
|
+
# pop one window per captured tensor. Scalars/configs always stay
|
|
2110
|
+
# as click-to-open boxes so 19 locals don't bury the code.
|
|
2111
|
+
# Frame-snapshot keys (context-menu capture — tracked in
|
|
2112
|
+
# __frame_snapshot_keys__) never auto-open: opening a menu on a
|
|
2113
|
+
# widget must not spawn a window per captured tensor.
|
|
2114
|
+
# code_tree_node carries the scope dict: the marker reads the site's
|
|
2115
|
+
# comment dict from it and splats it 1:1 onto the value window's
|
|
2116
|
+
# draw_any (never onto the marker's own wrapper — show_bg=True with a
|
|
2117
|
+
# dark tint would paint an opaque bg over the very symbol it boxes).
|
|
2118
|
+
# The simple captured value renders EXACTLY like a usage label: the
|
|
2119
|
+
# gap opens right after the boxed target symbol and the line reads
|
|
2120
|
+
# `edited=False, new_text='...' = get_text(...)` - code repositions,
|
|
2121
|
+
# nothing is covered. Collected here, stamped + painted by
|
|
2122
|
+
# _draw_usage_labels below (sym_len drives the gap-derived ring).
|
|
2123
|
+
if _pill is not None:
|
|
2124
|
+
if _bind_pills is _static_pills:
|
|
2125
|
+
_bind_pills = list(_static_pills)
|
|
2126
|
+
_bind_pills.append(_pill)
|
|
2127
|
+
_draw_marker_at(
|
|
2128
|
+
draw_state,
|
|
2129
|
+
_frozen_pos if _frozen_pos is not None
|
|
2130
|
+
else (origin_x + start_col * char_w - pad, _my - pad),
|
|
2131
|
+
cursor_inside, (_ml, start_col, end_col),
|
|
2132
|
+
"/".join(map(str, key_path)), value=value,
|
|
2133
|
+
captured=True, store_obj=fn, key_path=key_path,
|
|
2134
|
+
width=max(1, end_col - start_col) * char_w + 2 * pad,
|
|
2135
|
+
height=line_px + 2 * pad,
|
|
2136
|
+
code_tree_node=node.get("locals") if isinstance(node, dict) else None,
|
|
2137
|
+
buffer_line=_ml - 1, name=_snm, auto_open=_sao,
|
|
2138
|
+
inline_values=True,
|
|
2139
|
+
in_selection=_line_in_selection(_ml, _sel_lo, _sel_hi),
|
|
2140
|
+
caret_line=kwargs.get("caret_line"), def_node=node)
|
|
2141
|
+
if _record:
|
|
2142
|
+
if _mutated or _replay and _new_nonidle != _nonidle:
|
|
2143
|
+
# Rebuild the static replay structures (a key changed state).
|
|
2144
|
+
_static_gutter = {}
|
|
2145
|
+
for _k, (_gl, _gmds) in _idle_gutter.items():
|
|
2146
|
+
_static_gutter.setdefault(_gl, []).append(_gmds)
|
|
2147
|
+
_static_pills = [p for _k, p in _pills.items()
|
|
2148
|
+
if p is not None and _k not in _new_nonidle]
|
|
2149
|
+
# `_lmap` (and what it was built from) rides along so the ids in
|
|
2150
|
+
# _pk stay pinned.
|
|
2151
|
+
object.__setattr__(draw_state, "_lv_pass_memo",
|
|
2152
|
+
(_pk, _idle_gutter, _pills, _new_nonidle, _lmap,
|
|
2153
|
+
_static_gutter, _static_pills))
|
|
2154
|
+
# Inline USAGE labels: `seq_len=384` inserted after every later
|
|
2155
|
+
# occurrence of a captured symbol - the code text is SHIFTED to make
|
|
2156
|
+
# room (display-time only - see live_usage + the positional-trail
|
|
2157
|
+
# machinery in draw_text's _window/_build_vcols).
|
|
2158
|
+
try:
|
|
2159
|
+
_draw_usage_labels(draw_state, fn, node, span, source_lines,
|
|
2160
|
+
_snap_vals, _ilines, _ikeys, origin_x, origin_y,
|
|
2161
|
+
char_w, line_px, _lmap, _clip,
|
|
2162
|
+
kwargs.get("col_shift", 0),
|
|
2163
|
+
binding_pills=_bind_pills)
|
|
2164
|
+
except Exception as e:
|
|
2165
|
+
print(f"live_view: usage labels failed: {e!r}", file=sys.stderr)
|
|
2166
|
+
# TEMP perf: one line per slow enough pass (keys=store size for this
|
|
2167
|
+
# def, culled=off-viewport, idle=fast-id skips, drawn=full wrapper calls).
|
|
2168
|
+
_soms = (time.perf_counter() - _sot0) * 1000.0
|
|
2169
|
+
if _soms >= 2.0:
|
|
2170
|
+
from meltygui.core.diagnostics.perf_trace import trace as _sotrace
|
|
2171
|
+
_sotrace("snapshot_overlay", fn=getattr(fn, "__qualname__", "?"),
|
|
2172
|
+
ms=round(_soms, 1), keys=_soc[0], culled=_soc[1],
|
|
2173
|
+
idle=_soc[2], drawn=_soc[3])
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
@render_func(use_cache=False, show_bg=False, shadow=False, selectable=False,
|
|
2177
|
+
with_header=None, show_name=False)
|
|
2178
|
+
def draw_function_live(input_value, draw_state=None, unique=None,
|
|
2179
|
+
source_mode=None, column_edges=None, run_in_thread=True,
|
|
2180
|
+
**kwargs):
|
|
2181
|
+
"""draw_function with transparent instrumentation, source side by side:
|
|
2182
|
+
the instrumented twin (live_instrument) runs AUTOMATICALLY — on first
|
|
2183
|
+
view, on every hotswap (id(fn.__code__) is the auto_run token, so a save
|
|
2184
|
+
in the editor compiles AND runs in one go), and on param edits — every
|
|
2185
|
+
assignment publishes one snapshot to the ORIGINAL function's store, and
|
|
2186
|
+
the editor column shows the ORIGINAL code with the snapshot overlay
|
|
2187
|
+
anchoring each captured value at its line.
|
|
2188
|
+
|
|
2189
|
+
`run_in_thread=True` (the default) runs the twin on a worker so a long
|
|
2190
|
+
pass (live_view_forward's full forward pass) never blocks the render
|
|
2191
|
+
loop. The live views need no special handling for this: each publis
|
|
2192
|
+
invalidates its watcher draw_states from the worker and wakes the loop
|
|
2193
|
+
(the terminal-reader pattern in live_view._notify_watchers), and all
|
|
2194
|
+
window rendering / voxel uploads happen on the GL thread next frame.
|
|
2195
|
+
|
|
2196
|
+
`source_mode` picks the source column's route: FILE_TREE (the default)
|
|
2197
|
+
is the text-only editor; NEW_CODE is the full code_file_io display —
|
|
2198
|
+
the draw_collection structured pane and the live-overlay text pane at
|
|
2199
|
+
the same time (live_view_forward uses it).
|
|
2200
|
+
|
|
2201
|
+
Ctrl+Enter over the lab presses the Run button (request_run) —
|
|
2202
|
+
overriding the global Ctrl+Enter's Pending-Saves-window flow while the
|
|
2203
|
+
mouse is here. The run itself already executes the latest source: the
|
|
2204
|
+
twin compiles from the pending in-memory text."""
|
|
2205
|
+
from meltygui.editor.live_view_views import _run_proxy
|
|
2206
|
+
from meltygui.editor.live_view_views import request_run
|
|
2207
|
+
|
|
2208
|
+
fn = input_value
|
|
2209
|
+
try:
|
|
2210
|
+
fn = inspect.unwrap(fn)
|
|
2211
|
+
except Exception:
|
|
2212
|
+
pass
|
|
2213
|
+
|
|
2214
|
+
if not callable(fn) or getattr(fn, "__code__", None) is None:
|
|
2215
|
+
imgui.text("draw_function_live: needs a plain function")
|
|
2216
|
+
return False, input_value
|
|
2217
|
+
|
|
2218
|
+
# Ctrl+Enter over the lab: press the Run button. registered BLOCKING
|
|
2219
|
+
# with a priority well above draw_main's non_blocking root handler
|
|
2220
|
+
# (512 - but any on-screen depth is < 512, so this always sorts first),
|
|
2221
|
+
# which stops the event chain at this view: the global re-save-all
|
|
2222
|
+
# window flow never fires while the mouse is here. This hook only
|
|
2223
|
+
# covers frames where the body renders; the blit-cached half is
|
|
2224
|
+
# draw_main's root BVH fallback → request_run (dual dispatch).
|
|
2225
|
+
if draw_state.on_action("ctrl_enter_down", priority_delta=1024):
|
|
2226
|
+
request_run(draw_state)
|
|
2227
|
+
|
|
2228
|
+
from meltygui.view.code_view import draw_function
|
|
2229
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
2230
|
+
from meltygui.core.layout.column_core import ColumnLayout
|
|
2231
|
+
from meltygui.core.layout.column_core import MIN_ROW_HEIGHT
|
|
2232
|
+
from meltygui.core.rendering.mode import Mode
|
|
2233
|
+
# Runner | source: a shared edge system (ColumnLayout): the divider is
|
|
2234
|
+
# a draggable line in the window's flat collision solve, and each column
|
|
2235
|
+
# manages its own height - no _columns_top capture to race with
|
|
2236
|
+
# cache-skipped siblings. Columns pin to the visible viewport so long
|
|
2237
|
+
# functions clip inside their cell instead of growing past the window
|
|
2238
|
+
# bottom. A NEW_CODE source column nests its own structured+text
|
|
2239
|
+
# ColumnLayout inside cell 1; the cell's edge dicts pass down with the
|
|
2240
|
+
# call (left_edge/right_edge, by reference - code_file_io forwards them
|
|
2241
|
+
# like jump_to), so the nested row's far edges ARE this row's divider and
|
|
2242
|
+
# right edge and can never drift apart from them.
|
|
2243
|
+
top_y = draw_state.abs_top + 0
|
|
2244
|
+
imgui.set_cursor_screen_pos((draw_state.abs_left, top_y))
|
|
2245
|
+
cols = ColumnLayout(draw_state, 2, column_edges=column_edges,
|
|
2246
|
+
column_widths=[244])
|
|
2247
|
+
clip = cols.clip if cols.clip is not None else draw_state.abs_clip_rect
|
|
2248
|
+
avail = (max(MIN_ROW_HEIGHT, clip[3] - cols.top) if clip is not None
|
|
2249
|
+
else 400.0)
|
|
2250
|
+
inner_h = avail - 2 * cols.padding
|
|
2251
|
+
with cols.cell(0, height=avail) as col_w:
|
|
2252
|
+
draw_function(_run_proxy(fn), height=inner_h, width=col_w, temp=True,
|
|
2253
|
+
name=f"{fn.__name__} runner", run_in_thread=run_in_thread, rounding=None)
|
|
2254
|
+
with cols.cell(1, height=avail) as col_w:
|
|
2255
|
+
draw_any(fn, mode=source_mode or Mode.FILE_TREE, height=inner_h,
|
|
2256
|
+
width=col_w, name=f"{fn.__name__} live source",
|
|
2257
|
+
left_edge=cols.edges[1], right_edge=cols.edges[2])
|
|
2258
|
+
cols.finish()
|
|
2259
|
+
return False, input_value
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
@render_func
|
|
2263
|
+
def draw_source_preview(input_value=None, draw_state=None, preview: SourcePreviewState = None):
|
|
2264
|
+
from meltygui.view.text_view import draw_text
|
|
2265
|
+
from meltygui.core.melty import Melty
|
|
2266
|
+
from meltygui.code.new_converters import code_hosts_for
|
|
2267
|
+
if input_value is not None:
|
|
2268
|
+
preview.path, preview.line, preview.token = input_value
|
|
2269
|
+
if preview.path is None:
|
|
2270
|
+
return False, input_value
|
|
2271
|
+
path = Path(preview.path)
|
|
2272
|
+
text = Melty.read_code(path)
|
|
2273
|
+
if text is None:
|
|
2274
|
+
text = ''
|
|
2275
|
+
code_dict, dict_host = None, None
|
|
2276
|
+
if path.suffix == '.py':
|
|
2277
|
+
_, dict_host = code_hosts_for(path)
|
|
2278
|
+
code_dict = dict_host._held()
|
|
2279
|
+
from meltygui.editor.source_ui import _RowSpan
|
|
2280
|
+
_, _, pane = draw_text(text, name='source', editable=False, code_dict=code_dict, return_extras=True,
|
|
2281
|
+
jump_to=_RowSpan(0, path), show_header=False, width=draw_state.width,
|
|
2282
|
+
height=draw_state.height, show_widgets=True)
|
|
2283
|
+
if preview.line is not None and pane is not None and text:
|
|
2284
|
+
from meltygui.editor.text_editor import fold_project_jump
|
|
2285
|
+
lines = text.split('\n')
|
|
2286
|
+
row = max(0, min(int(preview.line) - 1, len(lines) - 1))
|
|
2287
|
+
offset = sum(len(line) + 1 for line in lines[:row])
|
|
2288
|
+
if preview.token and preview.token in lines[row]:
|
|
2289
|
+
offset += lines[row].index(preview.token)
|
|
2290
|
+
offset, row = fold_project_jump(pane, text, offset, row)
|
|
2291
|
+
pane.text_cursor_pos = offset
|
|
2292
|
+
pane.text_selection_start = pane.text_selection_end = offset
|
|
2293
|
+
pane.invalidate()
|
|
2294
|
+
preview.line = None
|
|
2295
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
2296
|
+
request_render()
|
|
2297
|
+
return False, input_value
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
def draw_pending_preview():
|
|
2301
|
+
import meltygui.editor.source_preview
|
|
2302
|
+
|
|
2303
|
+
request, meltygui.editor.source_preview._pending = meltygui.editor.source_preview._pending, None
|
|
2304
|
+
draw_source_preview(request, name='Source preview', closable=True,
|
|
2305
|
+
open_requested=request is not None, width=900, height=650)
|
|
2306
|
+
|
|
2307
|
+
|
|
2308
|
+
@render_func(use_cache=True, show_bg=True, shadow=True, selectable=False, temp=True,
|
|
2309
|
+
closable=True, melty_window=False, auto_resize=False, with_header=None,
|
|
2310
|
+
min_width=Toggles.UsagePicker.min_width, swoosh=False, min_height=Toggles.UsagePicker.min_height,
|
|
2311
|
+
enforce_max_height=True, # the content height cap holds mid-drag
|
|
2312
|
+
is_default_for=UsagePickerModel, tint=(0.071, 0.354, 0.511))
|
|
2313
|
+
def draw_usage_picker(input_value: UsagePickerModel, draw_state,
|
|
2314
|
+
row_height=Toggles.UsagePicker.row_height, row_gap=Toggles.UsagePicker.row_gap, tree_indent=16.0,
|
|
2315
|
+
code_font=Font.FONTAWESOME_MONO_19, collapsible=False,
|
|
2316
|
+
group_tint=None, change_kinds=None, **kwargs):
|
|
2317
|
+
"""Paint the picker rows Code-tab style. Hover moves the highlight only
|
|
2318
|
+
while the pointer MOVES over the window (a resting pointer never steals
|
|
2319
|
+
the keyboard cursor); a click on a row sets `model.picked` for draw_text
|
|
2320
|
+
to consume. Returns (True, model) on a pick."""
|
|
2321
|
+
from meltygui.editor.usage_picker import paint_usage_rows
|
|
2322
|
+
|
|
2323
|
+
return paint_usage_rows(
|
|
2324
|
+
input_value, draw_state, row_height=row_height, row_gap=row_gap,
|
|
2325
|
+
tree_indent=tree_indent, code_font=code_font, collapsible=collapsible,
|
|
2326
|
+
group_tint=group_tint, change_kinds=change_kinds)
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
@render_func(is_default_for=types.ModuleType, use_cache=True,
|
|
2330
|
+
show_bg=True, with_header=draw_header, with_footer=draw_footer)
|
|
2331
|
+
def draw_module(input_value: types.ModuleType, draw_state, **kwargs):
|
|
2332
|
+
imgui.text(f"Module: {input_value.__name__}")
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
@render_func(is_default_for=(type), tint=(0.928, 0.836, 0.655, 0.308), use_cache=True,
|
|
2336
|
+
header_single_line=True, show_name=True, temp=True, is_tree=False, shadow=False,
|
|
2337
|
+
show_bg=True, with_header=draw_header)
|
|
2338
|
+
def draw_type_name(input_value, **kwargs):
|
|
2339
|
+
try:
|
|
2340
|
+
if isinstance(input_value, str):
|
|
2341
|
+
imgui.text(f"{input_value}")
|
|
2342
|
+
|
|
2343
|
+
else:
|
|
2344
|
+
imgui.text(f"{input_value.__name__}")
|
|
2345
|
+
except Exception as e:
|
|
2346
|
+
imgui.text(f"Error displaying type: {e}")
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
@render_func(use_cache=True, is_default_for=SymbolUsage)
|
|
2350
|
+
def draw_symbol_usage(input_value):
|
|
2351
|
+
imgui.text(str(input_value))
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
@render_func(is_default_for=(property))
|
|
2355
|
+
def draw_property(input_value: property, draw_state, **kwargs):
|
|
2356
|
+
imgui.text_colored(f"Property: {input_value.fget.__name__}", 1.0, 0.5, 0.0, 1.0)
|
|
2357
|
+
|
|
2358
|
+
|
|
2359
|
+
@render_func(show_bg=True, align_header=False, use_cache=True, shadow=False,
|
|
2360
|
+
with_header=draw_header)
|
|
2361
|
+
def draw_type(input_value: type, **kwargs):
|
|
2362
|
+
from meltygui.view.collection_view import draw_collection
|
|
2363
|
+
|
|
2364
|
+
try:
|
|
2365
|
+
class_vars = {**{k: getattr(input_value, k) for k in vars(input_value)}}
|
|
2366
|
+
|
|
2367
|
+
changed, new_dict = draw_collection(class_vars, real_type=input_value, disable_scroll=True,
|
|
2368
|
+
name=f"Class: {input_value.__name__}")
|
|
2369
|
+
|
|
2370
|
+
if changed:
|
|
2371
|
+
for k, v in new_dict.items():
|
|
2372
|
+
if k.startswith("_"):
|
|
2373
|
+
continue
|
|
2374
|
+
try:
|
|
2375
|
+
imgui.text(f"Setting attribute {k} to value {v} on class {input_value.__name__}")
|
|
2376
|
+
setattr(input_value, k, v)
|
|
2377
|
+
except Exception as e:
|
|
2378
|
+
imgui.text(f"Error setting attribute {k} on class {input_value.__name__}: {e}")
|
|
2379
|
+
except Exception as e:
|
|
2380
|
+
imgui.text(f"Error rendering type {input_value}: {e}")
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
@render_func(is_default_for=UsageRef, use_cache=True, shadow=True, z_offset=2, show_bg=True, with_header=draw_header,
|
|
2384
|
+
is_tree=True, tint=(0.11, 0.1, 0.16))
|
|
2385
|
+
def draw_usage(input_value: UsageRef):
|
|
2386
|
+
imgui.text(
|
|
2387
|
+
f"{input_value.path} {input_value.line}:{input_value.column} {input_value.scope} {input_value.module_name}")
|
|
2388
|
+
|
|
2389
|
+
return False, input_value
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
@render_func(is_default_for=(Comment), shadow=False, header_same_line=True, initial={"expanded": False}, icon="",
|
|
2393
|
+
is_tree=True, show_name=False, indent_size=8, selectable=False, use_cache=False,
|
|
2394
|
+
tint=(0.137, 0.683, 0.299, 0.708),
|
|
2395
|
+
show_bg=False, with_header=draw_header, temp=False, expanded_mode=ExpandMode.MANUAL)
|
|
2396
|
+
def draw_comment(input_value: Comment, draw_state, style_manager, cursor_hover=False, font=Font.JETBRAINS_MONO_13):
|
|
2397
|
+
changed, value = False, input_value
|
|
2398
|
+
|
|
2399
|
+
imgui.dummy(0, 0)
|
|
2400
|
+
depth = max(0.3, Core.melty.bg_depth)
|
|
2401
|
+
depth_scale = 0.047
|
|
2402
|
+
|
|
2403
|
+
# [tint=(0.883, 0.712, 0.206, 0.34)]
|
|
2404
|
+
name_style = {
|
|
2405
|
+
'value': -0.420, 'saturation': 1.06,
|
|
2406
|
+
'alpha': 0.047, 'max_value': 0.704,
|
|
2407
|
+
'depth_factor': 0.34
|
|
2408
|
+
}
|
|
2409
|
+
depth_intensity = float(depth) * depth_scale
|
|
2410
|
+
name_style['value'] = depth_intensity * name_style['depth_factor'] + name_style['value']
|
|
2411
|
+
|
|
2412
|
+
# [tint=(0.767, 0.379, 0.379)]
|
|
2413
|
+
alpha = 1.02
|
|
2414
|
+
|
|
2415
|
+
sat_depth_factor = 0.0
|
|
2416
|
+
sat_depth_offset = 0.188
|
|
2417
|
+
sat_shift = float(depth + sat_depth_offset) * sat_depth_factor
|
|
2418
|
+
name_style['saturation'] = name_style['saturation'] + sat_shift
|
|
2419
|
+
|
|
2420
|
+
name_color = style_manager.make_color_style_value(input=name_style)
|
|
2421
|
+
|
|
2422
|
+
# A grouped multi-line comment is a '\n'-joined run of '# ' lines; strip the
|
|
2423
|
+
# '#'/'# ' prefix from EACH line so it displays as clean prose, not just the
|
|
2424
|
+
# first (str[2:] would leave a stray '#' on every continuation line).
|
|
2425
|
+
def _strip_hash(ln):
|
|
2426
|
+
return ln[2:] if ln.startswith("# ") else (ln[1:] if ln.startswith("#") else ln)
|
|
2427
|
+
|
|
2428
|
+
display = "\n".join(_strip_hash(ln) for ln in str(input_value).split("\n"))
|
|
2429
|
+
|
|
2430
|
+
# The framework header (is_tree=True) owns the expand/collapse button.
|
|
2431
|
+
# ExpandMode.MANUAL keeps this body visible while collapsed, with the
|
|
2432
|
+
# first line standing in for the whole comment.
|
|
2433
|
+
if "\n" in display and not draw_state.expanded:
|
|
2434
|
+
# Collapsed: one line truncated to the available width - never let it
|
|
2435
|
+
# spill onto a second row.
|
|
2436
|
+
flat = " ".join(display.split("\n"))
|
|
2437
|
+
avail = draw_state.abs_left + draw_state.width - imgui.get_cursor_screen_pos().x
|
|
2438
|
+
if imgui.calc_text_size(flat).x > avail:
|
|
2439
|
+
lo, hi = 0, len(flat)
|
|
2440
|
+
while lo < hi:
|
|
2441
|
+
mid = (lo + hi + 1) // 2
|
|
2442
|
+
if imgui.calc_text_size(flat[:mid]).x <= avail:
|
|
2443
|
+
lo = mid
|
|
2444
|
+
else:
|
|
2445
|
+
hi = mid - 1
|
|
2446
|
+
flat = flat[:lo].rstrip()
|
|
2447
|
+
imgui.push_style_color(imgui.COLOR_TEXT, *name_color[:3], alpha)
|
|
2448
|
+
imgui.text(flat)
|
|
2449
|
+
imgui.pop_style_color()
|
|
2450
|
+
else:
|
|
2451
|
+
imgui.push_text_wrap_pos(draw_state.abs_left + draw_state.width)
|
|
2452
|
+
imgui.push_style_color(imgui.COLOR_TEXT, *name_color[:3], alpha)
|
|
2453
|
+
imgui.text_wrapped(display)
|
|
2454
|
+
imgui.pop_style_color()
|
|
2455
|
+
imgui.pop_text_wrap_pos()
|
|
2456
|
+
|
|
2457
|
+
if changed:
|
|
2458
|
+
return True, value
|
|
2459
|
+
return False, input_value
|
|
2460
|
+
|
|
2461
|
+
|
|
2462
|
+
@render_func(is_default_for=(Parameter), wraps=render_func, with_header=draw_header)
|
|
2463
|
+
def draw_parameter(input_value):
|
|
2464
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
2465
|
+
|
|
2466
|
+
parameter_default = input_value.default
|
|
2467
|
+
if parameter_default is inspect.Parameter.empty:
|
|
2468
|
+
imgui.same_line()
|
|
2469
|
+
imgui.text("<No Default>")
|
|
2470
|
+
else:
|
|
2471
|
+
return draw_any(parameter_default, show_name=False, show_add_delete=False)
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
@render_func(is_default_for=(types.FunctionType, types.MethodType), z_offset=0, use_cache=True,
|
|
2475
|
+
show_add_delete=False, selectable=False, show_bg=True,
|
|
2476
|
+
parent_show_add_delete=False, is_tree=False, show_name=False, with_header=draw_header)
|
|
2477
|
+
def draw_function(input_value, name, draw_state, unique, auto_run=None, wrap=False,
|
|
2478
|
+
show_run_button=True, run_in_thread=False, result_fade_frames=None,
|
|
2479
|
+
**kwargs):
|
|
2480
|
+
"""`auto_run`: opt-in compile-and-run — pass any comparable version token
|
|
2481
|
+
(e.g. id(fn.__code__)); the function runs whenever the token CHANGES or a
|
|
2482
|
+
parameter is edited, no button click. The token is stored before running
|
|
2483
|
+
so a throwing function doesn't retry every frame. `show_run_button=False`
|
|
2484
|
+
drops the named run button (the streamlined live-lab look).
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
`run_in_thread=True` runs the function on a daemon worker instead of
|
|
2488
|
+
blocking the render loop (long model passes). Single-flight: a click or
|
|
2489
|
+
auto_run while a run is in flight is skipped — but the auto_run token is
|
|
2490
|
+
only latched when a run actually starts, so a hotswap landing mid-run
|
|
2491
|
+
re-fires on completion instead of being lost. The worker only writes
|
|
2492
|
+
draw_state attrs and uses the cross-thread invalidation path (the
|
|
2493
|
+
Background.run completion pattern); all rendering stays on the GL thread."""
|
|
2494
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
2495
|
+
from meltygui.utils.render_utils import print_colored_traceback
|
|
2496
|
+
from meltygui.view.collection_view import draw_collection
|
|
2497
|
+
from meltygui.view.control_view import button
|
|
2498
|
+
from meltygui.core.rendering.render_dispatch import _RUN_RESULT_HOLDERS
|
|
2499
|
+
from meltygui.core.rendering.render_dispatch import _format_run_error
|
|
2500
|
+
from meltygui.core.rendering.render_dispatch import _respond_to_cuda_oom
|
|
2501
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
2502
|
+
from meltygui.core.rendering.render_dispatch import is_run_busy
|
|
2503
|
+
from meltygui.core.rendering.render_dispatch import run_busy_begin
|
|
2504
|
+
from meltygui.core.rendering.render_dispatch import run_busy_end
|
|
2505
|
+
|
|
2506
|
+
if not callable(input_value):
|
|
2507
|
+
imgui.text("Not a callable function")
|
|
2508
|
+
return False, input_value
|
|
2509
|
+
params_edited = False
|
|
2510
|
+
try:
|
|
2511
|
+
signature = inspect.signature(input_value)
|
|
2512
|
+
params = signature.parameters
|
|
2513
|
+
if len(draw_state.params) != len(params):
|
|
2514
|
+
param_dict = {}
|
|
2515
|
+
|
|
2516
|
+
for name, param in params.items():
|
|
2517
|
+
if name == 'kwargs':
|
|
2518
|
+
continue
|
|
2519
|
+
if param.default is not inspect.Parameter.empty:
|
|
2520
|
+
param_dict[name] = param.default
|
|
2521
|
+
else:
|
|
2522
|
+
param_type = param.annotation
|
|
2523
|
+
default_value = param.default
|
|
2524
|
+
if default_value is not inspect.Parameter.empty:
|
|
2525
|
+
param_dict[name] = default_value
|
|
2526
|
+
else:
|
|
2527
|
+
if name in Core.melty.global_attrs:
|
|
2528
|
+
param_dict[name] = Core.melty.global_attrs[name]
|
|
2529
|
+
|
|
2530
|
+
draw_state.params = param_dict
|
|
2531
|
+
if len(draw_state.params) > 0:
|
|
2532
|
+
from meltygui.core.rendering.mode import Mode
|
|
2533
|
+
changed, new_val = draw_collection(draw_state.params, name="Parameters", initial={"expanded": True},
|
|
2534
|
+
use_cache=True,
|
|
2535
|
+
mode=Mode.FUNCTION_PARAMS,
|
|
2536
|
+
show_add_delete=False, shadow=True, z_offset=1,
|
|
2537
|
+
parent_show_add_delete=False,
|
|
2538
|
+
horizontal=False, wrap=wrap,
|
|
2539
|
+
child_kwargs={"max_width": 397, "shadow": False,
|
|
2540
|
+
"show_bg": False, "use_cache": True, "z_offset": 0.0
|
|
2541
|
+
}, tint=(0.34, 0.40, 0.45))
|
|
2542
|
+
if changed:
|
|
2543
|
+
draw_state.params = new_val
|
|
2544
|
+
params_edited = True
|
|
2545
|
+
except Exception as e:
|
|
2546
|
+
imgui.text(f"Error inspecting function parameters: {e}")
|
|
2547
|
+
draw_state.params = {}
|
|
2548
|
+
|
|
2549
|
+
sees_this = 0
|
|
2550
|
+
|
|
2551
|
+
# A pkl written while the old misc-latch was active reloads as busy -
|
|
2552
|
+
# scrub it; the latch is _RUN_BUSY now (never persisted).
|
|
2553
|
+
draw_state.misc.pop("_run_busy", None)
|
|
2554
|
+
|
|
2555
|
+
def _run():
|
|
2556
|
+
if run_in_thread:
|
|
2557
|
+
if not run_busy_begin(draw_state):
|
|
2558
|
+
return # single-flight: one run per runner at a time
|
|
2559
|
+
# Snapshot params so a mid-run edit can't give the worker a
|
|
2560
|
+
# half-updated dict; never run a @render_func WRAPPER off-thread
|
|
2561
|
+
# (it mutates process-global Melty stacks - see run_in_background),
|
|
2562
|
+
# otherwise take the bare function.
|
|
2563
|
+
params = dict(draw_state.params)
|
|
2564
|
+
fn = getattr(input_value, "__wrapped__", input_value)
|
|
2565
|
+
|
|
2566
|
+
def _worker():
|
|
2567
|
+
try:
|
|
2568
|
+
draw_state.result = fn(**params)
|
|
2569
|
+
_RUN_RESULT_HOLDERS.add(draw_state)
|
|
2570
|
+
draw_state.misc["_result_frame"] = Melty.frame_count
|
|
2571
|
+
draw_state.misc.pop("_run_error", None)
|
|
2572
|
+
except Exception as e:
|
|
2573
|
+
draw_state.misc["_run_error"] = _format_run_error(e)
|
|
2574
|
+
print(f"Error calling function '{input_value.__name__}': {e}")
|
|
2575
|
+
print_colored_traceback(*sys.exc_info())
|
|
2576
|
+
_respond_to_cuda_oom(e, input_value.__name__)
|
|
2577
|
+
finally:
|
|
2578
|
+
run_busy_end(draw_state)
|
|
2579
|
+
# invalidate_up_current reads the live render stack - only
|
|
2580
|
+
# valid mid-render on the GL thread. Off-thread completion
|
|
2581
|
+
# marks the runner's subtree by tile id (force: the result
|
|
2582
|
+
# pane is a cached descendant) and wakes the loop; the
|
|
2583
|
+
# validation itself happens on the render thread.
|
|
2584
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
2585
|
+
Melty.cache.invalidate_up(
|
|
2586
|
+
draw_state._tile_id, force=True,
|
|
2587
|
+
note=Note(name="draw_function run complete",
|
|
2588
|
+
reason=f"func={input_value.__name__}",
|
|
2589
|
+
tint=(0, 0, 1)))
|
|
2590
|
+
request_render()
|
|
2591
|
+
|
|
2592
|
+
threading.Thread(target=_worker, daemon=True,
|
|
2593
|
+
name=f"draw_function:{input_value.__name__}").start()
|
|
2594
|
+
Core.melty.cache.invalidate_up_current(force=True) # show spinner now
|
|
2595
|
+
request_render()
|
|
2596
|
+
return
|
|
2597
|
+
try:
|
|
2598
|
+
draw_state.result = input_value(**draw_state.params)
|
|
2599
|
+
_RUN_RESULT_HOLDERS.add(draw_state)
|
|
2600
|
+
draw_state.misc["_result_frame"] = Melty.frame_count
|
|
2601
|
+
draw_state.misc.pop("_run_error", None)
|
|
2602
|
+
Core.melty.cache.invalidate_up_current(force=True)
|
|
2603
|
+
except Exception as e:
|
|
2604
|
+
# Surfaced in the UI (red text where the result goes), anchored at
|
|
2605
|
+
# the deepest frame in PROJECT code - the line the user can fix.
|
|
2606
|
+
draw_state.misc["_run_error"] = _format_run_error(e)
|
|
2607
|
+
Core.melty.cache.invalidate_up_current(force=True)
|
|
2608
|
+
print(f"Error calling function '{input_value.__name__}': {e}")
|
|
2609
|
+
print_colored_traceback(*sys.exc_info())
|
|
2610
|
+
_respond_to_cuda_oom(e, input_value.__name__)
|
|
2611
|
+
|
|
2612
|
+
busy = run_in_thread and is_run_busy(draw_state)
|
|
2613
|
+
# One-offed run request (draw_function_live's Ctrl+Enter - the
|
|
2614
|
+
# hotkey IS the Run button): always popped, so it can't replay on later
|
|
2615
|
+
# frames; dropped while busy, matching a click during a threaded run.
|
|
2616
|
+
if draw_state.misc.pop("_run_requested", None) and not busy:
|
|
2617
|
+
_run()
|
|
2618
|
+
if auto_run is not None and not busy and (
|
|
2619
|
+
params_edited or draw_state.misc.get("_auto_run_ver") != auto_run):
|
|
2620
|
+
draw_state.misc["_auto_run_ver"] = auto_run
|
|
2621
|
+
_run()
|
|
2622
|
+
|
|
2623
|
+
imgui.new_line()
|
|
2624
|
+
if show_run_button and button(f"Run {input_value.__name__}()##{unique}", icon=kwargs.get("icon", ""), height=35,
|
|
2625
|
+
bg_offset=0, tint=(0.499, 0.844, 0.488, 0.32), shadow=True, rounding=None)[0]:
|
|
2626
|
+
_run()
|
|
2627
|
+
|
|
2628
|
+
# Fading result (result_fade_frames): the check mark + result text hold,
|
|
2629
|
+
# then fade out and clear - modeled on code_file_io's recompile_status
|
|
2630
|
+
# (frame-based fade, invalidate + request_render pump while fading).
|
|
2631
|
+
# None (default) keeps the persistent result pane.
|
|
2632
|
+
result_fade = 1.0
|
|
2633
|
+
if result_fade_frames and draw_state.result is not None:
|
|
2634
|
+
shown_for = float(Melty.frame_count
|
|
2635
|
+
- draw_state.misc.get("_result_frame", Melty.frame_count))
|
|
2636
|
+
result_fade = min(1.0, max(0.0, 2.0 - shown_for / float(result_fade_frames)))
|
|
2637
|
+
if result_fade > 0.01:
|
|
2638
|
+
draw_state.invalidate()
|
|
2639
|
+
request_render()
|
|
2640
|
+
else:
|
|
2641
|
+
draw_state.result = None
|
|
2642
|
+
draw_state.misc.pop("_result_frame", None)
|
|
2643
|
+
|
|
2644
|
+
if run_in_thread and is_run_busy(draw_state):
|
|
2645
|
+
imgui.same_line(spacing=10)
|
|
2646
|
+
imgui.text_colored("", 0.55, 0.75, 1.0, 1.0)
|
|
2647
|
+
imgui.new_line()
|
|
2648
|
+
elif draw_state.result is not None:
|
|
2649
|
+
imgui.same_line(spacing=10)
|
|
2650
|
+
imgui.text_colored("", 0.55, 0.75, 1.0, result_fade)
|
|
2651
|
+
if result_fade_frames and isinstance(draw_state.result, str):
|
|
2652
|
+
# Fading summary rides the button row inline, so hiding it never
|
|
2653
|
+
# reflows the content below (the row holds its height).
|
|
2654
|
+
imgui.same_line(spacing=8)
|
|
2655
|
+
imgui.text_colored(draw_state.result, 0.55, 0.75, 1.0, result_fade)
|
|
2656
|
+
imgui.new_line()
|
|
2657
|
+
else:
|
|
2658
|
+
imgui.same_line()
|
|
2659
|
+
imgui.text_colored(" ", 0.55, 0.75, 1.0, 1.0)
|
|
2660
|
+
imgui.new_line()
|
|
2661
|
+
|
|
2662
|
+
run_error = draw_state.misc.get("_run_error")
|
|
2663
|
+
if run_error:
|
|
2664
|
+
imgui.push_text_wrap_pos(0.0)
|
|
2665
|
+
imgui.text_colored(run_error, 1.0, 0.45, 0.40, 1.0)
|
|
2666
|
+
imgui.pop_text_wrap_pos()
|
|
2667
|
+
|
|
2668
|
+
if draw_state.result is not None and not (result_fade_frames
|
|
2669
|
+
and isinstance(draw_state.result, str)):
|
|
2670
|
+
imgui.text_colored(" Result", *(1.0, 1.0, 1.0, 0.5))
|
|
2671
|
+
imgui.set_cursor_pos_y(imgui.get_cursor_pos_y() - 15)
|
|
2672
|
+
draw_any(draw_state.result, name="Result", header_same_line=True, show_header=False,
|
|
2673
|
+
show_add_delete=False)
|
|
2674
|
+
|
|
2675
|
+
# pop_style_var(3)
|
|
2676
|
+
|
|
2677
|
+
return False, input_value
|