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,281 @@
|
|
|
1
|
+
"""Timeline logging for diagnosing cross-thread slowness (symbol index / code
|
|
2
|
+
host load). NOT a profiler: many interacting threads plus GIL-bound parsing make
|
|
3
|
+
sampling misleading, so instead every meaningful unit of work writes one line
|
|
4
|
+
with wall-clock time, frame count, and thread label — the log reads as a single
|
|
5
|
+
interleaved timeline. A gap in frame numbers while a worker line is open = the
|
|
6
|
+
GIL was held; overlapping spans show which threads stacked up.
|
|
7
|
+
|
|
8
|
+
12:34:56.789 f001234 [render ] ensure_index: spawn recompute file=toggles.py
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from meltygui.core.diagnostics.perf_trace import trace, trace_rl, span, once
|
|
12
|
+
|
|
13
|
+
trace("warmer build start", files=150)
|
|
14
|
+
with span("cold compute", file=name): # logs "... took 812.4ms" on exit
|
|
15
|
+
...
|
|
16
|
+
with span("probe", min_ms=2.0): ... # silent unless >= 2ms
|
|
17
|
+
trace_rl("slow-probe", "probe slow", ...) # at most 1 line/sec per key
|
|
18
|
+
if once(("host", name)): trace(...) # once per key per session
|
|
19
|
+
|
|
20
|
+
Gate: Toggles.symbol_perf_log (missing Toggles => enabled). Sink is LOG_PATH,
|
|
21
|
+
append mode with a session header — append (not truncate) so a jedi-pool child
|
|
22
|
+
process importing this module can't wipe the parent's log mid-run. Never raises:
|
|
23
|
+
a logging failure must not take down the render loop.
|
|
24
|
+
|
|
25
|
+
Everything here is stdlib-only; project state is read via sys.modules (no
|
|
26
|
+
imports) so this module is importable from anywhere without cycles.
|
|
27
|
+
"""
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
import threading
|
|
31
|
+
import time
|
|
32
|
+
from meltygui.core.runtime.paths import debug_log_path
|
|
33
|
+
|
|
34
|
+
LOG_PATH = debug_log_path("lsd_symbol_perf.log")
|
|
35
|
+
_MAX_CARRYOVER_BYTES = 5 * 1024 * 1024 # start fresh when the file grows past this
|
|
36
|
+
|
|
37
|
+
_lock = threading.Lock()
|
|
38
|
+
_rl_last: dict = {} # rate-limit key -> last emit monotonic
|
|
39
|
+
_once_keys: set = set() # keys already emitted via once()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _enabled() -> bool:
|
|
43
|
+
try:
|
|
44
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
45
|
+
return bool(Toggles.symbol_perf_log)
|
|
46
|
+
except Exception:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def enabled() -> bool:
|
|
51
|
+
"""Public gate for callers that do per-frame work BEYOND logging (e.g.
|
|
52
|
+
the GPU frame timer's query objects) — same toggle as trace()."""
|
|
53
|
+
return _enabled()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _open_log():
|
|
57
|
+
"""One line-buffered append handle per process, adopted across hotswap /
|
|
58
|
+
dual-import via sys (the established sharing pattern for process singletons)."""
|
|
59
|
+
fh = getattr(sys, "_symbol_perf_fh", None)
|
|
60
|
+
if fh is not None:
|
|
61
|
+
return fh
|
|
62
|
+
try:
|
|
63
|
+
mode = "a"
|
|
64
|
+
try:
|
|
65
|
+
if os.path.getsize(LOG_PATH) > _MAX_CARRYOVER_BYTES:
|
|
66
|
+
mode = "w"
|
|
67
|
+
except OSError:
|
|
68
|
+
pass
|
|
69
|
+
fh = open(LOG_PATH, mode, buffering=1)
|
|
70
|
+
fh.write(f"\n=== session start pid={os.getpid()} "
|
|
71
|
+
f"{time.strftime('%Y-%m-%d %X')} ===\n")
|
|
72
|
+
except Exception:
|
|
73
|
+
fh = False # sentinel: don't retry every call
|
|
74
|
+
sys._symbol_perf_fh = fh
|
|
75
|
+
return fh
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _frame() -> int:
|
|
79
|
+
mel = (sys.modules.get("meltygui.core.melty")
|
|
80
|
+
or sys.modules.get("lsd.gl_gui.melty"))
|
|
81
|
+
try:
|
|
82
|
+
return mel.Melty.frame_count if mel is not None else -1
|
|
83
|
+
except Exception:
|
|
84
|
+
return -1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _thread_label() -> str:
|
|
88
|
+
t = threading.current_thread()
|
|
89
|
+
try:
|
|
90
|
+
gs = (sys.modules.get("meltygui.core.graphics.gl_state")
|
|
91
|
+
or sys.modules.get("lsd.gl_gui.gl_state"))
|
|
92
|
+
if gs is not None and getattr(gs, "_gl_thread", None) is t:
|
|
93
|
+
return "render"
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
name = t.name
|
|
97
|
+
if name == "MainThread":
|
|
98
|
+
return "main"
|
|
99
|
+
# ThreadPoolExecutor-0_3 -> pool_3 (Background pool workers)
|
|
100
|
+
if name.startswith("ThreadPoolExecutor"):
|
|
101
|
+
return "pool_" + name.rsplit("_", 1)[-1]
|
|
102
|
+
return name
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _fmt_fields(fields: dict) -> str:
|
|
106
|
+
if not fields:
|
|
107
|
+
return ""
|
|
108
|
+
parts = []
|
|
109
|
+
for k, v in fields.items():
|
|
110
|
+
if isinstance(v, float):
|
|
111
|
+
v = f"{v:.1f}"
|
|
112
|
+
parts.append(f"{k}={v}")
|
|
113
|
+
return " " + " ".join(parts)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def trace(msg: str, **fields):
|
|
117
|
+
"""One timeline line. Swallows every failure."""
|
|
118
|
+
if not _enabled():
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
fh = _open_log()
|
|
122
|
+
if not fh:
|
|
123
|
+
return
|
|
124
|
+
now = time.time()
|
|
125
|
+
ts = time.strftime("%H:%M:%S", time.localtime(now)) + f".{int(now % 1 * 1000):03d}"
|
|
126
|
+
line = (f"{ts} f{_frame():06d} [{_thread_label():<15}] "
|
|
127
|
+
f"{msg}{_fmt_fields(fields)}\n")
|
|
128
|
+
with _lock:
|
|
129
|
+
fh.write(line)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def trace_rl(key, msg: str, min_interval: float = 1.0, **fields):
|
|
135
|
+
"""Rate-limited trace: at most one line per `min_interval` seconds per key.
|
|
136
|
+
For per-frame paths that are only interesting when they stay slow."""
|
|
137
|
+
if not _enabled():
|
|
138
|
+
return
|
|
139
|
+
try:
|
|
140
|
+
now = time.monotonic()
|
|
141
|
+
last = _rl_last.get(key)
|
|
142
|
+
if last is not None and now - last < min_interval:
|
|
143
|
+
return
|
|
144
|
+
_rl_last[key] = now
|
|
145
|
+
except Exception:
|
|
146
|
+
return
|
|
147
|
+
trace(msg, **fields)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def once(key) -> bool:
|
|
151
|
+
"""True the first time `key` is seen this session — for once-only lines."""
|
|
152
|
+
try:
|
|
153
|
+
if key in _once_keys:
|
|
154
|
+
return False
|
|
155
|
+
_once_keys.add(key)
|
|
156
|
+
return True
|
|
157
|
+
except Exception:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class span:
|
|
162
|
+
"""Context manager logging '<label> took Xms' on exit (only when >= min_ms).
|
|
163
|
+
Extra context can be attached mid-span via .add(k=v); an exception inside
|
|
164
|
+
the span is noted on the line and re-raised."""
|
|
165
|
+
|
|
166
|
+
__slots__ = ("label", "min_ms", "fields", "t0", "c0")
|
|
167
|
+
|
|
168
|
+
def __init__(self, label: str, min_ms: float = 0.0, **fields):
|
|
169
|
+
self.label = label
|
|
170
|
+
self.min_ms = min_ms
|
|
171
|
+
self.fields = fields
|
|
172
|
+
self.t0 = 0.0
|
|
173
|
+
self.c0 = 0.0
|
|
174
|
+
|
|
175
|
+
def add(self, **fields):
|
|
176
|
+
self.fields.update(fields)
|
|
177
|
+
|
|
178
|
+
def __enter__(self):
|
|
179
|
+
self.t0 = time.monotonic()
|
|
180
|
+
self.c0 = time.thread_time()
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def __exit__(self, exc_type, exc, tb):
|
|
184
|
+
try:
|
|
185
|
+
dt_ms = (time.monotonic() - self.t0) * 1000.0
|
|
186
|
+
if dt_ms >= self.min_ms:
|
|
187
|
+
# cpu ≪ wall on a slow span = this thread was GIL-starved, not
|
|
188
|
+
# doing the work - the span label is then the victim, not the
|
|
189
|
+
# culprit (see the 2026-07-31 stall hunts).
|
|
190
|
+
cpu_ms = (time.thread_time() - self.c0) * 1000.0
|
|
191
|
+
suffix = " EXC=" + exc_type.__name__ if exc_type is not None else ""
|
|
192
|
+
trace(f"{self.label} took {dt_ms:.1f}ms (cpu {cpu_ms:.1f}ms){suffix}",
|
|
193
|
+
**self.fields)
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Stall watchdog: what is the render thread blocked on? ──────────────────────
|
|
200
|
+
# Wall≫cpu slow frames (the post-boot 1000ms+ bursts) mean the render thread
|
|
201
|
+
# is WAITING - lock, GIL, GL/present backpressure - and the per-phase spans
|
|
202
|
+
# can't pin on what. This daemon samples Melty's clock; when it sits still
|
|
203
|
+
# past `threshold_s` while the render thread is mid-frame (NOT parked in
|
|
204
|
+
# glfw wait_events - an idle studio is not a stall), it dumps every thread's
|
|
205
|
+
# current call stack to the log. One dump per stall, re-armed when the frame
|
|
206
|
+
# counter moves; a second dump is forced if the stall passes 3x threshold.
|
|
207
|
+
# Cost when healthy: one attribute read per poll (20Hz). Same toggle as trace.
|
|
208
|
+
|
|
209
|
+
def _render_thread():
|
|
210
|
+
gs = (sys.modules.get("meltygui.core.graphics.gl_state")
|
|
211
|
+
or sys.modules.get("lsd.gl_gui.gl_state"))
|
|
212
|
+
return getattr(gs, "_gl_thread", None) if gs is not None else None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _dump_all_stacks(reason: str):
|
|
216
|
+
import traceback
|
|
217
|
+
frames = sys._current_frames()
|
|
218
|
+
render_t = _render_thread()
|
|
219
|
+
for t in threading.enumerate():
|
|
220
|
+
frame = frames.get(t.ident)
|
|
221
|
+
if frame is None:
|
|
222
|
+
continue
|
|
223
|
+
stack = traceback.extract_stack(frame)[-10:]
|
|
224
|
+
chain = " <- ".join(
|
|
225
|
+
f"{fs.filename.rsplit('/', 1)[-1]}:{fs.lineno} {fs.name}"
|
|
226
|
+
for fs in reversed(stack))
|
|
227
|
+
label = "render" if t is render_t else t.name
|
|
228
|
+
trace(f"STALL {reason} [{label}] {chain}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _stall_watchdog(threshold_s: float, poll_s: float):
|
|
232
|
+
last_count = -1
|
|
233
|
+
still_since = time.monotonic()
|
|
234
|
+
dumped = 0
|
|
235
|
+
while True:
|
|
236
|
+
time.sleep(poll_s)
|
|
237
|
+
try:
|
|
238
|
+
if not _enabled():
|
|
239
|
+
continue
|
|
240
|
+
count = _frame()
|
|
241
|
+
now = time.monotonic()
|
|
242
|
+
if count != last_count:
|
|
243
|
+
last_count = count
|
|
244
|
+
still_since = now
|
|
245
|
+
dumped = 0
|
|
246
|
+
continue
|
|
247
|
+
stalled_s = now - still_since
|
|
248
|
+
want = 1 if stalled_s >= threshold_s else 0
|
|
249
|
+
if want and stalled_s >= threshold_s * 3:
|
|
250
|
+
want = 2
|
|
251
|
+
if dumped >= want:
|
|
252
|
+
continue
|
|
253
|
+
render_t = _render_thread()
|
|
254
|
+
frame = sys._current_frames().get(render_t.ident) if render_t else None
|
|
255
|
+
if frame is None:
|
|
256
|
+
continue
|
|
257
|
+
# Parked between frames = idle, not a stall. wait_events blocks
|
|
258
|
+
# there; poll_events/sleep cover some launcher-style loops.
|
|
259
|
+
names = set()
|
|
260
|
+
f = frame
|
|
261
|
+
while f is not None and len(names) < 12:
|
|
262
|
+
names.add(f.f_code.co_name)
|
|
263
|
+
f = f.f_back
|
|
264
|
+
if {"wait_events", "poll_events"} & names:
|
|
265
|
+
still_since = now
|
|
266
|
+
continue
|
|
267
|
+
dumped = want
|
|
268
|
+
_dump_all_stacks(f"{stalled_s:.2f}s frame={count}")
|
|
269
|
+
except Exception:
|
|
270
|
+
pass # the watchdog must never hurt the app
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def ensure_stall_watchdog(threshold_s: float = 0.35, poll_s: float = 0.05):
|
|
274
|
+
"""Idempotent, process-lifetime (sys-guarded like the log handle — an
|
|
275
|
+
in-process studio restart adopts the running one instead of stacking)."""
|
|
276
|
+
if getattr(sys, "_lsd_stall_watchdog", None) is not None:
|
|
277
|
+
return
|
|
278
|
+
t = threading.Thread(target=_stall_watchdog, args=(threshold_s, poll_s),
|
|
279
|
+
name="stall-watchdog", daemon=True)
|
|
280
|
+
sys._lsd_stall_watchdog = t
|
|
281
|
+
t.start()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Callable, Any
|
|
3
|
+
from collections import deque, defaultdict
|
|
4
|
+
|
|
5
|
+
from meltygui.core.melty import Melty
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def profile(func: Callable) -> Callable:
|
|
9
|
+
"""
|
|
10
|
+
Decorator that profiles a function line-by-line and stores results in Melty.profiles_results.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
@profile
|
|
14
|
+
def my_function():
|
|
15
|
+
# your code here
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
The profiling results will be available at:
|
|
19
|
+
Melty.profiles_results['my_function'][-1] # Most recent call
|
|
20
|
+
|
|
21
|
+
Each result is a list of (line_of_code, time_ms) tuples sorted by time.
|
|
22
|
+
|
|
23
|
+
Note: Requires line_profiler package. Install with: pip install line_profiler
|
|
24
|
+
"""
|
|
25
|
+
# Track if we're already profiling this function (for recursive calls)
|
|
26
|
+
_profiling = False
|
|
27
|
+
|
|
28
|
+
@functools.wraps(func)
|
|
29
|
+
def wrapper(*args, **kwargs):
|
|
30
|
+
nonlocal _profiling
|
|
31
|
+
|
|
32
|
+
# Only profile at the top level to avoid nested profiler conflicts
|
|
33
|
+
if _profiling:
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from line_profiler import LineProfiler
|
|
38
|
+
except ImportError:
|
|
39
|
+
# Fallback: just run the function without profiling
|
|
40
|
+
print("Warning: line_profiler not installed. Install with: pip install line_profiler")
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
if func.__name__ not in Melty.profiles_results:
|
|
44
|
+
# Create a line profiler
|
|
45
|
+
profiler = LineProfiler()
|
|
46
|
+
profiler.add_function(func)
|
|
47
|
+
|
|
48
|
+
# Profile the function execution
|
|
49
|
+
_profiling = True
|
|
50
|
+
try:
|
|
51
|
+
profiler.enable()
|
|
52
|
+
result = func(*args, **kwargs)
|
|
53
|
+
profiler.disable()
|
|
54
|
+
finally:
|
|
55
|
+
_profiling = False
|
|
56
|
+
|
|
57
|
+
# Extract the line-by-line stats
|
|
58
|
+
filtered_results = []
|
|
59
|
+
stats = profiler.get_stats()
|
|
60
|
+
|
|
61
|
+
# stats.timings is a dict: {(filename, line_start, func_name): [(lineno, nhits, time), ...]}
|
|
62
|
+
for key, timings in stats.timings.items():
|
|
63
|
+
filename, line_start, func_name_inner = key
|
|
64
|
+
|
|
65
|
+
# Read the source code
|
|
66
|
+
import linecache
|
|
67
|
+
|
|
68
|
+
for lineno, nhits, time in timings:
|
|
69
|
+
if nhits > 0: # Only include lines that were executed
|
|
70
|
+
source_line = linecache.getline(filename, lineno).strip()
|
|
71
|
+
if source_line:
|
|
72
|
+
# line_profiler time is in units (typically nanoseconds, 1e-09)
|
|
73
|
+
# Convert to milliseconds: time * unit * 1000 (to go from seconds to ms)
|
|
74
|
+
time_ms = time * stats.unit * 1000
|
|
75
|
+
filtered_results.append((source_line, time_ms, lineno))
|
|
76
|
+
|
|
77
|
+
# Sort by time (slowest first)
|
|
78
|
+
filtered_results.sort(key=lambda x: x[1], reverse=True)
|
|
79
|
+
|
|
80
|
+
# Store the profile results (deque automatically to max 5 items)
|
|
81
|
+
Melty.profiles_results[func.__name__] = filtered_results
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
else:
|
|
85
|
+
return func(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
return wrapper
|
|
88
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Bounded, always-on resize diagnostics; records metadata, never view values.
|
|
2
|
+
|
|
3
|
+
Each process writes .melty/resize-<pid>.log (JSON lines, two 4 MiB backups).
|
|
4
|
+
No draw-state references are retained and logging failures cannot break a drag.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from logging.handlers import RotatingFileHandler
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def record(stage, window, event=None, error=False, **details):
|
|
16
|
+
try:
|
|
17
|
+
from meltygui.core.melty import Melty
|
|
18
|
+
logger = logging.getLogger(f"meltygui.resize.{os.getpid()}")
|
|
19
|
+
if not logger.handlers:
|
|
20
|
+
from meltygui.core.runtime.paths import cache_root
|
|
21
|
+
path = cache_root()
|
|
22
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
handler = RotatingFileHandler(path / f"resize-{os.getpid()}.log",
|
|
24
|
+
maxBytes=4 * 1024 * 1024, backupCount=2)
|
|
25
|
+
logger.addHandler(handler)
|
|
26
|
+
logger.setLevel(logging.INFO)
|
|
27
|
+
logger.propagate = False
|
|
28
|
+
entry = dict(time=time.time(), frame=Melty.frame_count, stage=stage,
|
|
29
|
+
window=str(getattr(window, 'id', None)), identity=id(window),
|
|
30
|
+
name=str(getattr(window, 'name', ''))[:200])
|
|
31
|
+
for key in ('window_pos', 'width', 'height', 'abs_left', 'abs_top',
|
|
32
|
+
'min_width', 'min_height', 'closed', 'expanded', 'use_cache',
|
|
33
|
+
'freeze_resize', 'size_change', '_frame_pinned',
|
|
34
|
+
'_initial_window_size', '_initial_window_pos_resize',
|
|
35
|
+
'_resize_from_top_left', '_resize_target_edge_x0', '_resize_target_row_y0'):
|
|
36
|
+
entry[key] = getattr(window, key, None)
|
|
37
|
+
for key, axis in (('_resize_target_edge', 'x'), ('_resize_target_row', 'y')):
|
|
38
|
+
edge = getattr(window, key, None)
|
|
39
|
+
entry[key] = None if edge is None else (id(edge), edge.get(axis))
|
|
40
|
+
if event is not None:
|
|
41
|
+
entry['event'] = {key: getattr(event, key, None)
|
|
42
|
+
for key in ('x', 'y', 'dx', 'dy', 'total_dx', 'total_dy')}
|
|
43
|
+
if error:
|
|
44
|
+
entry['traceback'] = traceback.format_exc()
|
|
45
|
+
import meltygui.core.windowing.os_frame as os_frame
|
|
46
|
+
entry['geometry_mode'] = os_frame._STATE['mode']
|
|
47
|
+
entry['geometry_generation'] = os_frame._STATE['generation']
|
|
48
|
+
entry['os_expected'] = list(os_frame._STATE['expected'])
|
|
49
|
+
entry['os_unapplied'] = list(os_frame._STATE['unapplied'])
|
|
50
|
+
entry.update(details)
|
|
51
|
+
logger.info(json.dumps(entry, default=lambda value: f'<{type(value).__name__}>'))
|
|
52
|
+
except Exception:
|
|
53
|
+
# Diagnostics must never turn an otherwise valid frame into a failure.
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def edges(window, axis):
|
|
58
|
+
"""Small identity/geometry snapshot; never serialize a draw_state or its data."""
|
|
59
|
+
registry = getattr(window, '_edge_views' if axis == 'x' else '_row_views', {})
|
|
60
|
+
return [{"owner": str(key), "closed": getattr(view, 'closed', False),
|
|
61
|
+
"edges": [(id(edge), edge.get(axis)) for edge in edge_list]}
|
|
62
|
+
for key, (view, edge_list) in registry.items()]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Region screenshot tool — Ctrl+Shift+3 (draw_main's root hotkey) or
|
|
2
|
+
Actions.screenshot arms it; a crosshair follows the cursor on the overlay
|
|
3
|
+
draw list; click-drag a box; on release the pixels inside the box are read
|
|
4
|
+
from the main framebuffer (GL_BACK, after the frame is fully composited — the
|
|
5
|
+
deferred capture queue in screenshot.py), saved as a PNG under the configured
|
|
6
|
+
shot dir, added to the code editor as a tab (its ImageCodec view) WITHOUT
|
|
7
|
+
switching to it, and its path is put on the clipboard.
|
|
8
|
+
|
|
9
|
+
State is module-level (one tool, never more than one capture in flight);
|
|
10
|
+
`draw(draw_state)` runs from draw_main every frame and is a no-op unless
|
|
11
|
+
armed. While armed the tool owns the mouse: full-screen BLOCKING left-button
|
|
12
|
+
subscriptions at a priority above every window and blocker, so the press
|
|
13
|
+
that starts the box can't land on whatever sits under the cursor. Esc (or
|
|
14
|
+
the hotkey again) cancels.
|
|
15
|
+
|
|
16
|
+
The crosshair + camera are the CURSOR IMAGE while armed (glfw.create_cursor
|
|
17
|
+
from a PIL render, hotspot at the crosshair centre), not overlay drawing:
|
|
18
|
+
anything drawn in a frame uses the frame-start mouse sample and shows a
|
|
19
|
+
frame later, while the compositor moves the cursor plane with zero latency
|
|
20
|
+
— overlay crosshairs visibly trailed the pointer. The cursor plane is the
|
|
21
|
+
one thing that can sit exactly where the pointer is. Only the drag box
|
|
22
|
+
(anchored at the press) is drawn on the overlay, with a readout pill beside
|
|
23
|
+
it — top-left `x, y` and `w × h` in window points — so the tool doubles as
|
|
24
|
+
a measure tool (drag over a thing, read its rect off the label, Esc).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import meltygui.core.windowing.window_api as glfw
|
|
28
|
+
import meltygui_imgui as imgui
|
|
29
|
+
from meltygui.hdr_color import pack_color
|
|
30
|
+
|
|
31
|
+
import meltygui.core.input.mouse_cursor as mouse_cursor
|
|
32
|
+
from meltygui.core.melty import Melty
|
|
33
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
34
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
35
|
+
|
|
36
|
+
# Above every window / blocker (the live lab's blocking handlers use 1024).
|
|
37
|
+
_PRIORITY_DELTA = 4096
|
|
38
|
+
_MIN_BOX_PX = 3 # smaller than this on release = a click, not a box
|
|
39
|
+
_ICON = "\uf030" # FA camera (baked into the cursor image)
|
|
40
|
+
_BOX_COLOR = (0.35, 0.75, 1.0, 1.0)
|
|
41
|
+
_FILL_COLOR = (0.35, 0.75, 1.0, 0.12)
|
|
42
|
+
_CURSOR_SIZE, _CURSOR_HOT, _CURSOR_ARM, _CURSOR_GAP = 64, 20, 16, 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RegionScreenshot:
|
|
46
|
+
armed = False
|
|
47
|
+
start = None # (x, y) in points where the drag began; None = no box yet
|
|
48
|
+
cursor = None # *cursor* (built once, render thread)
|
|
49
|
+
cursor_set = False # our cursor is the window's current cursor
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_cursor_image():
|
|
53
|
+
"""Crosshair (gap around the hotspot, dark halo under a white hairline)
|
|
54
|
+
with the FA camera glyph below-right — the same glyph Actions.screenshot
|
|
55
|
+
wears. Returns (PIL image, hotspot)."""
|
|
56
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
57
|
+
from meltygui.core.styling.fonts import _RESOURCES
|
|
58
|
+
size, hot, arm, gap = _CURSOR_SIZE, _CURSOR_HOT, _CURSOR_ARM, _CURSOR_GAP
|
|
59
|
+
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
60
|
+
d = ImageDraw.Draw(img)
|
|
61
|
+
for col, w in (((0, 0, 0, 140), 3), ((255, 255, 255, 230), 1)):
|
|
62
|
+
for seg in ((hot - arm, hot, hot - gap, hot), (hot + gap, hot, hot + arm, hot),
|
|
63
|
+
(hot, hot - arm, hot, hot - gap), (hot, hot + gap, hot, hot + arm)):
|
|
64
|
+
d.line(seg, fill=col, width=w)
|
|
65
|
+
font = ImageFont.truetype(str(_RESOURCES / "fontawesome-webfont.ttf"), 22)
|
|
66
|
+
gx, gy = hot + 9, hot + 7
|
|
67
|
+
d.text((gx + 1, gy + 1), _ICON, font=font, fill=(0, 0, 0, 150))
|
|
68
|
+
d.text((gx, gy), _ICON, font=font, fill=(255, 255, 255, 235))
|
|
69
|
+
return img, hot
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _set_tool_cursor(on):
|
|
73
|
+
"""Swap the window cursor to the crosshair/camera image (on) or back to
|
|
74
|
+
the default (off). GLFW calls belong on the event-pumping thread — the
|
|
75
|
+
render thread — which is where draw() runs."""
|
|
76
|
+
if RegionScreenshot.cursor_set == on:
|
|
77
|
+
return
|
|
78
|
+
window = glfw.get_current_context()
|
|
79
|
+
if window is None:
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
if on and RegionScreenshot.cursor is None:
|
|
83
|
+
img, hot = _build_cursor_image()
|
|
84
|
+
RegionScreenshot.cursor = glfw.create_cursor(img, hot, hot)
|
|
85
|
+
glfw.set_cursor(window, RegionScreenshot.cursor if on else None)
|
|
86
|
+
RegionScreenshot.cursor_set = on
|
|
87
|
+
# Keep the per-frame shape push (mouse_cursor.apply) from our image.
|
|
88
|
+
mouse_cursor.note_external_cursor(on)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"region_screenshot: cursor swap failed: {e}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def arm():
|
|
94
|
+
"""Enter capture mode (idempotent)."""
|
|
95
|
+
RegionScreenshot.armed = True
|
|
96
|
+
RegionScreenshot.start = None
|
|
97
|
+
request_render()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cancel():
|
|
101
|
+
RegionScreenshot.armed = False
|
|
102
|
+
RegionScreenshot.start = None
|
|
103
|
+
request_render()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def toggle():
|
|
107
|
+
cancel() if RegionScreenshot.armed else arm()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _open_captured(path):
|
|
111
|
+
"""screenshot.py's on_captured: runs on the render thread inside
|
|
112
|
+
post_frame — defer the editor work to between frames, like any other
|
|
113
|
+
external window mutation. The shot becomes an editor TAB without
|
|
114
|
+
stealing the selection (OpenFiles.open_file only — not open_in_editor,
|
|
115
|
+
whose jump_to_path the editor adopts as its active tab): the user is
|
|
116
|
+
mid-work in whatever they were screenshotting. The tab bars repaint so
|
|
117
|
+
the new tab shows. The saved file's PATH also goes on the clipboard
|
|
118
|
+
(text, via GLFW's clipboard — the studio is the focused Wayland client,
|
|
119
|
+
so this is the one clipboard write that always lands)."""
|
|
120
|
+
from meltygui.core.runtime.extensions import source_window as editor_window_draw_state
|
|
121
|
+
|
|
122
|
+
def _land():
|
|
123
|
+
open_files = getattr(getattr(Melty.vis, "root", None), "open_files", None)
|
|
124
|
+
if open_files is not None:
|
|
125
|
+
open_files.open_file(path)
|
|
126
|
+
# The tab list changed under the editors' bodies: force both
|
|
127
|
+
# instances through their blit cache so the new tab appears.
|
|
128
|
+
for inst in (0, 1):
|
|
129
|
+
win = editor_window_draw_state(inst)
|
|
130
|
+
if win is not None and Melty.cache is not None and win._tile_id is not None:
|
|
131
|
+
Melty.cache.invalidate_up(win._tile_id, force=True, max_depth=4)
|
|
132
|
+
imgui.set_clipboard_text(str(path))
|
|
133
|
+
request_render()
|
|
134
|
+
Melty.post_to_render(_land)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _finish(x0, y0, x1, y1):
|
|
138
|
+
"""Release: queue the framebuffer read of the box (settled a couple of
|
|
139
|
+
frames so this frame's overlay — crosshair, box — has cleared)."""
|
|
140
|
+
from meltygui.core.graphics.screenshot import request_region_capture
|
|
141
|
+
RegionScreenshot.armed = False
|
|
142
|
+
RegionScreenshot.start = None
|
|
143
|
+
left, top = min(x0, x1), min(y0, y1)
|
|
144
|
+
w, h = abs(x1 - x0), abs(y1 - y0)
|
|
145
|
+
if w < _MIN_BOX_PX or h < _MIN_BOX_PX:
|
|
146
|
+
# A click, not a box: stay armed so the user can try again.
|
|
147
|
+
RegionScreenshot.armed = True
|
|
148
|
+
request_render()
|
|
149
|
+
return
|
|
150
|
+
request_region_capture(left, top, w, h, Melty.frame_count,
|
|
151
|
+
name="screenshot", on_captured=_open_captured)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def box_label_lines(x0, y0, x1, y1):
|
|
155
|
+
"""The readout for a normalized box (x0 <= x1, y0 <= y1), in WINDOW
|
|
156
|
+
POINTS — the coordinate system every draw_state rect and the capture
|
|
157
|
+
itself use, so a measurement read off the label maps straight onto
|
|
158
|
+
`draw_state.abs_left` / `width` values. Line 1 = top-left corner,
|
|
159
|
+
line 2 = size."""
|
|
160
|
+
x, y = int(round(x0)), int(round(y0))
|
|
161
|
+
w, h = int(round(x1 - x0)), int(round(y1 - y0))
|
|
162
|
+
return (f"{x}, {y}", f"{w} × {h}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def label_rect(x0, y0, x1, y1, label_w, label_h, display_w, display_h, gap):
|
|
166
|
+
"""Where the readout pill goes: below the box, right-aligned to its
|
|
167
|
+
right edge (outside, so the measured content stays visible). Off the
|
|
168
|
+
bottom of the display → inside the box's bottom-right corner; past the
|
|
169
|
+
right edge → slid left to fit; a box that fills the screen → inside,
|
|
170
|
+
clamped. Returns (left, top)."""
|
|
171
|
+
left = x1 - label_w
|
|
172
|
+
top = y1 + gap
|
|
173
|
+
if top + label_h > display_h:
|
|
174
|
+
top = y1 - gap - label_h
|
|
175
|
+
left = x1 - gap - label_w
|
|
176
|
+
left = max(0.0, min(left, display_w - label_w))
|
|
177
|
+
top = max(0.0, top)
|
|
178
|
+
return left, top
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _draw_box_label(overlay, x0, y0, x1, y1, display_w, display_h):
|
|
182
|
+
"""Paint the coordinate/size readout next to the drag box (the "measure
|
|
183
|
+
tool" half of the feature): a dark pill, box-coloured text."""
|
|
184
|
+
# [tint=(0.35, 0.75, 1.0)]
|
|
185
|
+
padding = 5
|
|
186
|
+
# [tint=(0.95, 0.61, 0.07)]
|
|
187
|
+
gap = 6
|
|
188
|
+
# [tint=(0.36, 0.68, 0.89)]
|
|
189
|
+
label_background = (0.05, 0.05, 0.08, 0.85)
|
|
190
|
+
lines = box_label_lines(x0, y0, x1, y1)
|
|
191
|
+
sizes = [imgui.calc_text_size(line) for line in lines]
|
|
192
|
+
text_w = max(size.x for size in sizes)
|
|
193
|
+
line_h = sizes[0].y
|
|
194
|
+
label_w = text_w + padding * 2
|
|
195
|
+
label_h = line_h * len(lines) + padding * 2
|
|
196
|
+
left, top = label_rect(x0, y0, x1, y1, label_w, label_h, display_w, display_h, gap)
|
|
197
|
+
overlay.add_rect_filled(left, top, left + label_w, top + label_h,
|
|
198
|
+
pack_color(*label_background), rounding=4.0)
|
|
199
|
+
overlay.add_rect(left, top, left + label_w, top + label_h,
|
|
200
|
+
pack_color(*_BOX_COLOR), rounding=4.0, thickness=1.0)
|
|
201
|
+
text_color = pack_color(*_BOX_COLOR)
|
|
202
|
+
for index, line in enumerate(lines):
|
|
203
|
+
overlay.add_text(left + padding, top + padding + line_h * index, text_color, line)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def draw(draw_state):
|
|
207
|
+
"""Per-frame body (from draw_main): claim the mouse, track the box, paint
|
|
208
|
+
the crosshair/box + its coordinate/size readout on the overlay, and fire
|
|
209
|
+
the capture on release."""
|
|
210
|
+
if not RegionScreenshot.armed:
|
|
211
|
+
_set_tool_cursor(False) # back to the default after cancel / esc
|
|
212
|
+
return
|
|
213
|
+
if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
|
|
214
|
+
cancel()
|
|
215
|
+
_set_tool_cursor(False)
|
|
216
|
+
return
|
|
217
|
+
_set_tool_cursor(True)
|
|
218
|
+
|
|
219
|
+
io = imgui.get_io()
|
|
220
|
+
full = (0.0, 0.0, float(io.display_size.x), float(io.display_size.y))
|
|
221
|
+
sub = dict(view_id="region_shot", rect=full, priority_delta=_PRIORITY_DELTA)
|
|
222
|
+
down = draw_state.on_action("left_mouse_down", **sub)
|
|
223
|
+
drag = draw_state.on_action("left_mouse_drag", **sub)
|
|
224
|
+
release = draw_state.on_action("left_mouse_drag_release", **sub)
|
|
225
|
+
draw_state.on_action("left_mouse_up", **sub) # claimed so nothing under us sees the click
|
|
226
|
+
draw_state.on_action("left_mouse_click", **sub)
|
|
227
|
+
|
|
228
|
+
mx, my = imgui.get_mouse_pos()
|
|
229
|
+
if down is not None:
|
|
230
|
+
RegionScreenshot.start = (float(down.x), float(down.y))
|
|
231
|
+
if release is not None and RegionScreenshot.start is not None:
|
|
232
|
+
sx, sy = RegionScreenshot.start
|
|
233
|
+
_finish(sx, sy, float(release.x), float(release.y))
|
|
234
|
+
return # nothing painted this frame - the capture reads a clean frame
|
|
235
|
+
if drag is None and RegionScreenshot.start is not None and not imgui.is_mouse_down(0):
|
|
236
|
+
RegionScreenshot.start = None # button went up without the drag activating
|
|
237
|
+
|
|
238
|
+
if RegionScreenshot.start is not None:
|
|
239
|
+
overlay = imgui.get_overlay_draw_list()
|
|
240
|
+
overlay.channels_set_current(Core.melty.max_layer - 1)
|
|
241
|
+
sx, sy = RegionScreenshot.start
|
|
242
|
+
x0, y0, x1, y1 = min(sx, mx), min(sy, my), max(sx, mx), max(sy, my)
|
|
243
|
+
overlay.add_rect_filled(x0, y0, x1, y1, pack_color(*_FILL_COLOR))
|
|
244
|
+
overlay.add_rect(x0, y0, x1, y1, pack_color(*_BOX_COLOR), 0.0, 0, 1.0)
|
|
245
|
+
_draw_box_label(overlay, x0, y0, x1, y1, full[2], full[3])
|
|
246
|
+
# The box's moving edge tracks the cursor: keep frames coming.
|
|
247
|
+
request_render()
|