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,706 @@
|
|
|
1
|
+
import functools as _functools
|
|
2
|
+
import gc as _gc
|
|
3
|
+
import sys as _sys
|
|
4
|
+
import locale
|
|
5
|
+
import threading as _threading
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque, defaultdict
|
|
8
|
+
from contextlib import contextmanager as _contextmanager
|
|
9
|
+
|
|
10
|
+
import meltygui.core.windowing.window_api as glfw
|
|
11
|
+
import meltygui_imgui as imgui
|
|
12
|
+
from meltygui.hdr_color import pack_color
|
|
13
|
+
|
|
14
|
+
from meltygui.core.styling.fonts import Font
|
|
15
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
16
|
+
|
|
17
|
+
# Use the system's locale time format (e.g. 12-hour AM/PM if configured)
|
|
18
|
+
# for %X instead of the default "C" locale 24-hour clock.
|
|
19
|
+
try:
|
|
20
|
+
locale.setlocale(locale.LC_TIME, "")
|
|
21
|
+
except locale.Error:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _timestamp():
|
|
26
|
+
"""Locale-aware time string (AM/PM on systems configured for 12-hour)."""
|
|
27
|
+
return time.strftime("%X", time.localtime())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fade_opacity(created_at):
|
|
31
|
+
"""Opacity that decays with age: 100% fresh, 50% at ~10s, floored at 30%."""
|
|
32
|
+
elapsed = time.time() - created_at
|
|
33
|
+
return max(0.3, 1.0 - 0.05 * elapsed)
|
|
34
|
+
|
|
35
|
+
class NotificationCenter:
|
|
36
|
+
max_notifications = 20
|
|
37
|
+
notifications = deque(maxlen=max_notifications)
|
|
38
|
+
tagged_notifications = defaultdict(lambda: deque(maxlen=NotificationCenter.max_notifications))
|
|
39
|
+
ignore_tags = ["host"]
|
|
40
|
+
# Single live value per tag (overwritten on each display() call), shown in
|
|
41
|
+
# the dedicated "Live" column. Insertion order = row order.
|
|
42
|
+
live_values = {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Notification:
|
|
46
|
+
"""One toast. `stack` is an optional list of (filename, lineno, funcname)
|
|
47
|
+
tuples, OUTERMOST FIRST (the capture_stack() shape); `jump` is the
|
|
48
|
+
(path, line) the entry opens in the code editor when clicked — by default
|
|
49
|
+
the innermost stack frame. `count` is how many times the same (tag, text,
|
|
50
|
+
stack) was notified while this entry was in the column."""
|
|
51
|
+
__slots__ = ("text", "tint", "time_label", "created_at", "stack", "jump", "count")
|
|
52
|
+
|
|
53
|
+
def __init__(self, text, tint, time_label, created_at, stack=None, jump=None):
|
|
54
|
+
self.text = text
|
|
55
|
+
self.tint = tint
|
|
56
|
+
self.time_label = time_label
|
|
57
|
+
self.created_at = created_at
|
|
58
|
+
self.stack = stack
|
|
59
|
+
self.jump = jump
|
|
60
|
+
self.count = 1
|
|
61
|
+
|
|
62
|
+
# Legacy 4-tuple unpacking (text, tint, time_label, created_at).
|
|
63
|
+
def __iter__(self):
|
|
64
|
+
return iter((self.text, self.tint, self.time_label, self.created_at))
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def key(self):
|
|
68
|
+
return (self.text, self.jump, tuple(self.stack) if self.stack else None)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def label(self):
|
|
72
|
+
"""Text as drawn: the message, a repeat count, and the jump site."""
|
|
73
|
+
out = self.text if self.count == 1 else f"{self.text} ({self.count})"
|
|
74
|
+
if self.jump is not None:
|
|
75
|
+
path, line = self.jump
|
|
76
|
+
out += f"\n{_short_path(path)}:{line}"
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def copy_text(self):
|
|
81
|
+
"""Clipboard form: the label plus the whole captured stack."""
|
|
82
|
+
out = self.label
|
|
83
|
+
if self.stack:
|
|
84
|
+
out += "\n" + "\n".join(f' File "{f}", line {ln}, in {fn}' for f, ln, fn in self.stack)
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _short_path(path):
|
|
89
|
+
path = str(path)
|
|
90
|
+
marker = "/src/"
|
|
91
|
+
i = path.rfind(marker)
|
|
92
|
+
return path[i + 1:] if i >= 0 else path.rsplit("/", 1)[-1]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def capture_stack(skip_files=(), skip_funcs=(), limit=12):
|
|
96
|
+
"""Cheap stack capture for notify(stack=...): walks live frames with
|
|
97
|
+
sys._getframe (no source-line lookup, unlike traceback.extract_stack) and
|
|
98
|
+
returns [(filename, lineno, funcname), ...] OUTERMOST FIRST, at most
|
|
99
|
+
`limit` frames. Frames whose file ends with an entry of `skip_files` or
|
|
100
|
+
whose function name is in `skip_funcs` are dropped from the INNER end
|
|
101
|
+
until the first frame that is neither — so the innermost kept frame is
|
|
102
|
+
the "caller" of the skipped plumbing. Returns [] off the interpreter."""
|
|
103
|
+
f = _sys._getframe(1)
|
|
104
|
+
out = []
|
|
105
|
+
skipping = True
|
|
106
|
+
while f is not None and len(out) < limit:
|
|
107
|
+
code = f.f_code
|
|
108
|
+
fn = code.co_filename
|
|
109
|
+
if skipping and (fn.endswith(tuple(skip_files)) or code.co_name in skip_funcs
|
|
110
|
+
or fn.endswith("notifications.py")):
|
|
111
|
+
f = f.f_back
|
|
112
|
+
continue
|
|
113
|
+
skipping = False
|
|
114
|
+
out.append((fn, f.f_lineno, code.co_name))
|
|
115
|
+
f = f.f_back
|
|
116
|
+
out.reverse()
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def notify(text, tint=(1,1,1,1), tag=None, urgent=False, stack=None, jump=None):
|
|
121
|
+
"""Push a toast into the `tag` column. `stack` (see capture_stack) makes
|
|
122
|
+
the entry clickable: clicking opens `jump` — default the innermost stack
|
|
123
|
+
frame — in the code editor. Repeats of the same (tag, text, stack) while
|
|
124
|
+
the previous entry is still in the column bump its count (drawn as
|
|
125
|
+
"(3)") and refresh its time instead of adding a row."""
|
|
126
|
+
formatted_time = _timestamp()
|
|
127
|
+
created_at = time.time()
|
|
128
|
+
tint = tint if len(tint) == 4 else (*tint, 1) # Ensure color has alpha
|
|
129
|
+
if stack:
|
|
130
|
+
stack = [tuple(fr[:3]) for fr in stack]
|
|
131
|
+
if jump is None:
|
|
132
|
+
jump = (stack[-1][0], stack[-1][1])
|
|
133
|
+
if jump is not None:
|
|
134
|
+
jump = (str(jump[0]), int(jump[1]))
|
|
135
|
+
|
|
136
|
+
column = NotificationCenter.tagged_notifications["General" if tag is None else tag]
|
|
137
|
+
entry = Notification(text, tint, formatted_time, created_at, stack=stack, jump=jump)
|
|
138
|
+
for existing in column:
|
|
139
|
+
if existing.key == entry.key:
|
|
140
|
+
existing.count += 1
|
|
141
|
+
existing.tint = tint
|
|
142
|
+
existing.time_label = formatted_time
|
|
143
|
+
existing.created_at = created_at
|
|
144
|
+
column.remove(existing)
|
|
145
|
+
column.appendleft(existing)
|
|
146
|
+
break
|
|
147
|
+
else:
|
|
148
|
+
column.appendleft(entry)
|
|
149
|
+
|
|
150
|
+
if urgent:
|
|
151
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
152
|
+
request_render()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@_contextmanager
|
|
156
|
+
def lag_span(label, min_ms=50.0):
|
|
157
|
+
"""Notify (tag "lag") when the wrapped block ran slower than `min_ms` —
|
|
158
|
+
near-zero cost when fast, so it can sit on hot paths. Includes the thread
|
|
159
|
+
name: a slow span on a worker still stalls the render thread for its
|
|
160
|
+
GIL-held portion, so every entry here is a frame-drop suspect."""
|
|
161
|
+
# Stack of the SPAN SITE, captured on entry (a few µs - no source
|
|
162
|
+
# lookup); clicking the toast opens the `with lag_span(...)` line. Skips
|
|
163
|
+
# contextlib's generator plumbing and lag_traced's wrapper.
|
|
164
|
+
stack = capture_stack(skip_files=("contextlib.py",), skip_funcs=("wrapper", "lag_span"))
|
|
165
|
+
t0 = time.perf_counter()
|
|
166
|
+
try:
|
|
167
|
+
yield
|
|
168
|
+
finally:
|
|
169
|
+
ms = (time.perf_counter() - t0) * 1000.0
|
|
170
|
+
if ms >= min_ms:
|
|
171
|
+
tint = (1.0, 0.25, 0.2) if ms >= 300 else (1.0, 0.65, 0.2)
|
|
172
|
+
notify(f"{label} {ms:.0f}ms [{_threading.current_thread().name}]",
|
|
173
|
+
tint=tint, tag="lag", stack=stack)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def lag_traced(label, min_ms=50.0):
|
|
177
|
+
"""Decorator form of lag_span — stamp on any function suspected of
|
|
178
|
+
GIL-held stalls; it reports only when a call actually ran slow."""
|
|
179
|
+
def deco(fn):
|
|
180
|
+
@_functools.wraps(fn)
|
|
181
|
+
def wrapper(*args, **kwargs):
|
|
182
|
+
with lag_span(label, min_ms=min_ms):
|
|
183
|
+
return fn(*args, **kwargs)
|
|
184
|
+
return wrapper
|
|
185
|
+
return deco
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# GC watch: a generation-2 collection walks every live object with the GIL
|
|
189
|
+
# held - with several large cst dicts resident that's an intermittent multi-
|
|
190
|
+
# hundred-ms stall attributed to whichever thread happened to allocate. The
|
|
191
|
+
# lazy install keeps a hotswap re-exec (which reuses module globals) from
|
|
192
|
+
# stacking callbacks.
|
|
193
|
+
_gc_start = {}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _gc_watch(phase, info):
|
|
197
|
+
gen = info.get("generation")
|
|
198
|
+
if phase == "start":
|
|
199
|
+
_gc_start[gen] = time.perf_counter()
|
|
200
|
+
return
|
|
201
|
+
t0 = _gc_start.pop(gen, None)
|
|
202
|
+
if t0 is not None:
|
|
203
|
+
ms = (time.perf_counter() - t0) * 1000.0
|
|
204
|
+
if ms >= 20.0:
|
|
205
|
+
# urgent=False: a gc callback can fire at ANY allocation point,
|
|
206
|
+
# including mid-operation at boot - never pull request_render (and
|
|
207
|
+
# its lazy glfw import) from here; the next UI frame shows it.
|
|
208
|
+
try:
|
|
209
|
+
# The stack is whatever allocation the collector interrupted -
|
|
210
|
+
# still the best "who triggered it" there is.
|
|
211
|
+
notify(f"gc gen{gen} {ms:.0f}ms collected={info.get('collected')}"
|
|
212
|
+
f" [{_threading.current_thread().name}]",
|
|
213
|
+
tint=(1.0, 0.25, 0.2) if ms >= 300 else (1.0, 0.65, 0.2),
|
|
214
|
+
tag="lag", urgent=False, stack=capture_stack(skip_funcs=("_gc_watch",)))
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# One installed callback per PROCESS, tracked on sys: a module-global guard
|
|
220
|
+
# resets on the in-place restart's re-import, and each stacked callback pinned
|
|
221
|
+
# its session's module dict (and fired on every GC). Replace, don't append.
|
|
222
|
+
# (Sweeping by name also sheds the stack left by sessions predating this.)
|
|
223
|
+
_gc.callbacks[:] = [cb for cb in _gc.callbacks
|
|
224
|
+
if getattr(cb, "__name__", None) != "_gc_watch"]
|
|
225
|
+
_gc.callbacks.append(_gc_watch)
|
|
226
|
+
_sys._lsd_gc_watch = _gc_watch
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _format_value(value):
|
|
230
|
+
"""Render a variety of value types into a compact display string."""
|
|
231
|
+
# scalar tensors / numpy scalars into a plain python number
|
|
232
|
+
item = getattr(value, "item", None)
|
|
233
|
+
if callable(item):
|
|
234
|
+
try:
|
|
235
|
+
value = value.item()
|
|
236
|
+
except Exception:
|
|
237
|
+
return str(value)
|
|
238
|
+
if isinstance(value, bool):
|
|
239
|
+
return str(value)
|
|
240
|
+
if isinstance(value, float):
|
|
241
|
+
return f"{value:.4g}"
|
|
242
|
+
return str(value)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def display(value, tint=(1, 1, 1, 1), tag=None, urgent=True):
|
|
246
|
+
"""Show a single live value in the "Live" column, keyed by `tag`.
|
|
247
|
+
|
|
248
|
+
Unlike notify(), which keeps a running list per tag, display() overwrites the
|
|
249
|
+
value for a tag each call so it updates in place over time."""
|
|
250
|
+
formatted_time = _timestamp()
|
|
251
|
+
created_at = time.time()
|
|
252
|
+
tint = tint if len(tint) == 4 else (*tint, 1) # Ensure color has alpha
|
|
253
|
+
key = tag if tag is not None else "value"
|
|
254
|
+
NotificationCenter.live_values[key] = (_format_value(value), tint, formatted_time, created_at)
|
|
255
|
+
|
|
256
|
+
# if urgent:
|
|
257
|
+
# from meltygui.core.windowing.glfw_utils import request_render
|
|
258
|
+
# request_render()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _wrap_text(text, max_width):
|
|
262
|
+
"""Greedy word-wrap `text` into lines no wider than `max_width` px.
|
|
263
|
+
Preserves explicit newlines. A single word wider than the limit is left on
|
|
264
|
+
its own line (it overflows rather than being split mid-word)."""
|
|
265
|
+
if max_width <= 0:
|
|
266
|
+
return [text]
|
|
267
|
+
# Memoized per (text, width): every visible toast re-wraps its text
|
|
268
|
+
# twice a frame (height measure + draw), each a calc_text_size per word.
|
|
269
|
+
memo_key = (text, max_width)
|
|
270
|
+
hit = _WRAP_MEMO.get(memo_key)
|
|
271
|
+
if hit is not None:
|
|
272
|
+
return hit
|
|
273
|
+
lines = []
|
|
274
|
+
for paragraph in text.split("\n"):
|
|
275
|
+
current = ""
|
|
276
|
+
for word in paragraph.split(" "):
|
|
277
|
+
candidate = word if not current else current + " " + word
|
|
278
|
+
if not current or imgui.calc_text_size(candidate).x <= max_width:
|
|
279
|
+
current = candidate
|
|
280
|
+
else:
|
|
281
|
+
lines.append(current)
|
|
282
|
+
current = word
|
|
283
|
+
lines.append(current)
|
|
284
|
+
if len(_WRAP_MEMO) > 1024:
|
|
285
|
+
_WRAP_MEMO.clear()
|
|
286
|
+
_WRAP_MEMO[memo_key] = lines
|
|
287
|
+
return lines
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
_WRAP_MEMO = globals().get("_WRAP_MEMO", {}) # (text, max_width) → lines
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _entry_height(label, content, content_width, line_height, padding):
|
|
294
|
+
"""Vertical space one stacked entry consumes: wrapped block + bg padding +
|
|
295
|
+
the inter-entry gap (matches _draw_column_entry's bg_top - padding step)."""
|
|
296
|
+
label_size = imgui.calc_text_size(label)
|
|
297
|
+
lines = _wrap_text(content, content_width - (label_size.x + padding))
|
|
298
|
+
return max(1, len(lines)) * line_height + padding * 3
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _draw_column_entry(draw_list, column_left, content_width, line_height, padding,
|
|
302
|
+
bg_bottom, label, label_color, content, content_color, opacity=1.0,
|
|
303
|
+
hit_rects=None, copy_text=None, jump=None, clip=None):
|
|
304
|
+
"""Draw one stacked entry: a left-aligned `label` (time / tag name) followed
|
|
305
|
+
by the wrapped, tinted `content`. `label_color`/`content_color` are RGBA
|
|
306
|
+
tuples; `opacity` scales every alpha so the whole toast fades with age.
|
|
307
|
+
When `hit_rects` is given, appends (rect, copy_text, jump) so the caller
|
|
308
|
+
can make the entry click-to-copy (or click-to-jump when `jump` is a
|
|
309
|
+
(path, line)); `clip` (a scroll viewport rect) clamps the hit rect so the
|
|
310
|
+
scrolled-out part of an entry can't swallow clicks. Returns the next
|
|
311
|
+
bg_bottom (above this one)."""
|
|
312
|
+
label_size = imgui.calc_text_size(label)
|
|
313
|
+
content_x = column_left + label_size.x + padding
|
|
314
|
+
|
|
315
|
+
lines = _wrap_text(content, content_width - (label_size.x + padding))
|
|
316
|
+
block_height = max(1, len(lines)) * line_height
|
|
317
|
+
bg_top = bg_bottom - (block_height + padding * 2)
|
|
318
|
+
content_top = bg_top + padding
|
|
319
|
+
|
|
320
|
+
# The entry under the mouse is drawn at full opacity regardless of age.
|
|
321
|
+
mouse = imgui.get_io().mouse_pos
|
|
322
|
+
if (column_left - padding <= mouse.x <= column_left + content_width + padding
|
|
323
|
+
and bg_top <= mouse.y <= bg_bottom):
|
|
324
|
+
opacity = 1.0
|
|
325
|
+
|
|
326
|
+
label_u32 = pack_color(label_color[0], label_color[1],
|
|
327
|
+
label_color[2], label_color[3] * opacity)
|
|
328
|
+
content_u32 = pack_color(content_color[0], content_color[1],
|
|
329
|
+
content_color[2], content_color[3] * opacity)
|
|
330
|
+
|
|
331
|
+
rect = (column_left - padding, bg_top, column_left + content_width + padding, bg_bottom)
|
|
332
|
+
|
|
333
|
+
# background rectangle with some transparency
|
|
334
|
+
draw_list.add_rect_filled(*rect, pack_color(0, 0, 0, opacity), rounding=2)
|
|
335
|
+
|
|
336
|
+
if hit_rects is not None:
|
|
337
|
+
hit_rect = rect if clip is None else (max(rect[0], clip[0]), max(rect[1], clip[1]),
|
|
338
|
+
min(rect[2], clip[2]), min(rect[3], clip[3]))
|
|
339
|
+
if hit_rect[0] < hit_rect[2] and hit_rect[1] < hit_rect[3]:
|
|
340
|
+
hit_rects.append((hit_rect, content if copy_text is None else copy_text, jump))
|
|
341
|
+
|
|
342
|
+
# label on the first line, then the wrapped, left-aligned content
|
|
343
|
+
draw_list.add_text(column_left, content_top, label_u32, label)
|
|
344
|
+
for li, line in enumerate(lines):
|
|
345
|
+
draw_list.add_text(content_x, content_top + li * line_height, content_u32, line)
|
|
346
|
+
|
|
347
|
+
return bg_top - padding
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# Tags whose band is collapsed to its title strip (toggled by the tree arrow
|
|
351
|
+
# beside the title, see _draw_column_title / _handle_arrow_click). Hotswap-safe.
|
|
352
|
+
_collapsed_tags = globals().get("_collapsed_tags") or set()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _draw_column_title(draw_list, x, y, color, title, hit_rects=None, copy_text=None,
|
|
356
|
+
arrow_rects=None, collapsed=False):
|
|
357
|
+
"""Draw a column's tree arrow + title. The arrow (▾ open / ▸ collapsed)
|
|
358
|
+
is a click target that toggles the band (registered on `arrow_rects` as
|
|
359
|
+
(rect, tag)); the title text is a click target that copies `copy_text`
|
|
360
|
+
(the whole column). Returns the title's right edge."""
|
|
361
|
+
# [tint=(0.95, 0.61, 0.07)]
|
|
362
|
+
arrow_size = 8
|
|
363
|
+
# [tint=(0.36, 0.68, 0.89)]
|
|
364
|
+
arrow_gap = 6
|
|
365
|
+
|
|
366
|
+
line_height = imgui.get_text_line_height()
|
|
367
|
+
cx = x + arrow_size / 2
|
|
368
|
+
cy = y + line_height / 2
|
|
369
|
+
half = arrow_size / 2
|
|
370
|
+
if collapsed: # ▸
|
|
371
|
+
draw_list.add_triangle_filled(cx - half / 2, cy - half, cx - half / 2, cy + half,
|
|
372
|
+
cx + half, cy, color)
|
|
373
|
+
else: # ▾
|
|
374
|
+
draw_list.add_triangle_filled(cx - half, cy - half / 2, cx + half, cy - half / 2,
|
|
375
|
+
cx, cy + half, color)
|
|
376
|
+
if arrow_rects is not None:
|
|
377
|
+
arrow_rects.append(((x - 2, y, x + arrow_size + arrow_gap, y + line_height), title))
|
|
378
|
+
|
|
379
|
+
text_x = x + arrow_size + arrow_gap
|
|
380
|
+
draw_list.add_text(text_x, y, color, title)
|
|
381
|
+
size = imgui.calc_text_size(title)
|
|
382
|
+
if hit_rects is not None and copy_text:
|
|
383
|
+
hit_rects.append(((text_x, y, text_x + size.x, y + size.y), copy_text, None))
|
|
384
|
+
return text_x + size.x
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _handle_arrow_click(arrow_rects):
|
|
388
|
+
"""A click on a band's tree arrow toggles that band between its full
|
|
389
|
+
height and just its title strip. Returns True when it consumed the click."""
|
|
390
|
+
if not arrow_rects or not imgui.is_mouse_clicked(0):
|
|
391
|
+
return False
|
|
392
|
+
mouse = imgui.get_io().mouse_pos
|
|
393
|
+
for (x0, y0, x1, y1), tag in arrow_rects:
|
|
394
|
+
if x0 <= mouse.x <= x1 and y0 <= mouse.y <= y1:
|
|
395
|
+
_collapsed_tags.symmetric_difference_update({tag})
|
|
396
|
+
return True
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _draw_scrolled_column(draw_list, io, tag, rows, column_left, content_width,
|
|
401
|
+
line_height, padding, viewport_top, viewport_bottom,
|
|
402
|
+
hit_rects, badge_rects, show_badge=True):
|
|
403
|
+
"""Draw one category's rows inside a fixed-height scrolling viewport.
|
|
404
|
+
|
|
405
|
+
`rows` is newest-first: (label, label_color, content, content_color,
|
|
406
|
+
created_at, copy_text, jump). Newest sits at the viewport bottom. Scroll
|
|
407
|
+
is per-`tag` and STICKY at the newest end: offset 0 follows new rows as
|
|
408
|
+
they arrive; a scrolled-back column instead holds its place (the offset
|
|
409
|
+
grows with the content) and shows a clickable "N new" badge (see
|
|
410
|
+
_handle_badge_click) counting rows newer than the last pinned view."""
|
|
411
|
+
# [tint=(0.95, 0.61, 0.07)]
|
|
412
|
+
wheel_lines_per_tick = 3
|
|
413
|
+
# [tint=(0.36, 0.68, 0.89)]
|
|
414
|
+
thumb_width = 3
|
|
415
|
+
# [tint=(0.72, 0.53, 0.94)]
|
|
416
|
+
thumb_min_height = 24.0
|
|
417
|
+
|
|
418
|
+
viewport = (column_left - padding, viewport_top,
|
|
419
|
+
column_left + content_width + padding, viewport_bottom)
|
|
420
|
+
viewport_height = viewport_bottom - viewport_top
|
|
421
|
+
heights = [_entry_height(label, content, content_width, line_height, padding)
|
|
422
|
+
for label, _lc, content, _cc, _t, _cp, _j in rows]
|
|
423
|
+
content_height = sum(heights)
|
|
424
|
+
max_offset = max(0.0, content_height - viewport_height)
|
|
425
|
+
|
|
426
|
+
state = _scroll_state.get(tag)
|
|
427
|
+
if state is None:
|
|
428
|
+
state = _scroll_state[tag] = {"offset": 0.0, "height": 0.0, "seen_time": 0.0}
|
|
429
|
+
|
|
430
|
+
# A scrolled-back view holds still while new rows grow the column's content.
|
|
431
|
+
growth = content_height - state["height"]
|
|
432
|
+
if state["offset"] > 0 and growth > 0:
|
|
433
|
+
state["offset"] += growth
|
|
434
|
+
state["height"] = content_height
|
|
435
|
+
|
|
436
|
+
hovered = (viewport[0] <= io.mouse_pos.x <= viewport[2]
|
|
437
|
+
and viewport[1] <= io.mouse_pos.y <= viewport[3])
|
|
438
|
+
if hovered and io.mouse_wheel:
|
|
439
|
+
# wheel-up = back toward older rows
|
|
440
|
+
state["offset"] += io.mouse_wheel * line_height * wheel_lines_per_tick
|
|
441
|
+
state["offset"] = min(max(state["offset"], 0.0), max_offset)
|
|
442
|
+
offset = state["offset"]
|
|
443
|
+
|
|
444
|
+
newest_time = max((created_at for _l, _lc, _c, _cc, created_at, _cp, _j in rows),
|
|
445
|
+
default=0.0)
|
|
446
|
+
if offset <= 0:
|
|
447
|
+
state["seen_time"] = newest_time
|
|
448
|
+
unseen = (sum(1 for _l, _lc, _c, _cc, created_at, _cp, _j in rows
|
|
449
|
+
if created_at > state["seen_time"]) if offset > 0 else 0)
|
|
450
|
+
|
|
451
|
+
# Stack rows upward from the bottom, shifted DOWN by the scroll offset so
|
|
452
|
+
# older rows come into view; the clip rect swallows any overhang.
|
|
453
|
+
draw_list.push_clip_rect(viewport[0], viewport[1], viewport[2], viewport[3], True)
|
|
454
|
+
bg_bottom = viewport_bottom + offset
|
|
455
|
+
for (label, label_color, content, content_color, created_at,
|
|
456
|
+
copy_text, jump), height in zip(rows, heights):
|
|
457
|
+
entry_top = bg_bottom - (height - padding)
|
|
458
|
+
if entry_top > viewport_bottom: # newest rows scrolled out below
|
|
459
|
+
bg_bottom -= height
|
|
460
|
+
continue
|
|
461
|
+
if bg_bottom < viewport_top: # everything further up is out too
|
|
462
|
+
break
|
|
463
|
+
opacity = _fade_opacity(created_at)
|
|
464
|
+
bg_bottom = _draw_column_entry(draw_list, column_left, content_width,
|
|
465
|
+
line_height, padding, bg_bottom,
|
|
466
|
+
label, label_color, content, content_color, opacity,
|
|
467
|
+
hit_rects=hit_rects, copy_text=copy_text,
|
|
468
|
+
jump=jump, clip=viewport)
|
|
469
|
+
draw_list.pop_clip_rect()
|
|
470
|
+
|
|
471
|
+
# Slim scroll thumb on the column's right side while it overflows.
|
|
472
|
+
if max_offset > 0 and (hovered or offset > 0):
|
|
473
|
+
thumb_height = max(thumb_min_height,
|
|
474
|
+
viewport_height * viewport_height / content_height)
|
|
475
|
+
travel = viewport_height - thumb_height
|
|
476
|
+
thumb_bottom = viewport_bottom - (offset / max_offset) * travel
|
|
477
|
+
draw_list.add_rect_filled(viewport[2] - thumb_width, thumb_bottom - thumb_height,
|
|
478
|
+
viewport[2], thumb_bottom,
|
|
479
|
+
pack_color(1, 1, 1, 0.25), rounding=1)
|
|
480
|
+
|
|
481
|
+
if show_badge and unseen:
|
|
482
|
+
_draw_new_badge(draw_list, tag, unseen, column_left, content_width,
|
|
483
|
+
line_height, padding, viewport_bottom, badge_rects)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _draw_new_badge(draw_list, tag, unseen, column_left, content_width,
|
|
487
|
+
line_height, padding, viewport_bottom, badge_rects):
|
|
488
|
+
"""Small "N new" pill at the bottom of a scrolled-back column; clicking it
|
|
489
|
+
(see _handle_badge_click) jumps the column back to the sticky end."""
|
|
490
|
+
# [tint=(0.95, 0.61, 0.07)]
|
|
491
|
+
arrow_width = 6
|
|
492
|
+
# [tint=(0.36, 0.68, 0.89)]
|
|
493
|
+
pill_extra_width = 6
|
|
494
|
+
|
|
495
|
+
text = f"{unseen} new"
|
|
496
|
+
text_size = imgui.calc_text_size(text)
|
|
497
|
+
pill_width = text_size.x + arrow_width + padding * 4 + pill_extra_width
|
|
498
|
+
pill_height = line_height + padding * 2
|
|
499
|
+
x0 = column_left + (content_width - pill_width) / 2
|
|
500
|
+
y1 = viewport_bottom - padding * 2
|
|
501
|
+
y0 = y1 - pill_height
|
|
502
|
+
yellow = pack_color(1, 1, 0, 1)
|
|
503
|
+
draw_list.add_rect_filled(x0, y0, x0 + pill_width, y1,
|
|
504
|
+
pack_color(0, 0, 0, 0.9),
|
|
505
|
+
rounding=pill_height / 2)
|
|
506
|
+
draw_list.add_rect(x0, y0, x0 + pill_width, y1, yellow,
|
|
507
|
+
rounding=pill_height / 2, thickness=1.0)
|
|
508
|
+
text_x = x0 + padding * 2 + 2
|
|
509
|
+
draw_list.add_text(text_x, y0 + padding, yellow, text)
|
|
510
|
+
# down-pointing triangle after the text ("new toasts are below")
|
|
511
|
+
arrow_x = text_x + text_size.x + 4
|
|
512
|
+
arrow_y = y0 + pill_height / 2
|
|
513
|
+
draw_list.add_triangle_filled(arrow_x, arrow_y - 3, arrow_x + arrow_width, arrow_y - 3,
|
|
514
|
+
arrow_x + arrow_width / 2, arrow_y + 3, yellow)
|
|
515
|
+
badge_rects.append(((x0, y0, x0 + pill_width, y1), tag))
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# Per-tag scroll state, keyed by column title. "offset" is pixels scrolled
|
|
519
|
+
# back from the newest end - 0 means pinned to the end, so new toasts
|
|
520
|
+
# auto-scroll into view (sticky). "height" is last frame's content height,
|
|
521
|
+
# used to hold a scrolled-back view still while new toasts grow the column.
|
|
522
|
+
# "seen_time" is the newest created_at that was on screen while pinned;
|
|
523
|
+
# anything newer feeds the "N new" badge. Hotswap-safe.
|
|
524
|
+
_scroll_state = globals().get("_scroll_state") or {}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _file_tint(path):
|
|
528
|
+
"""The tint the user painted on `path` in the code editor (FileMeta), as
|
|
529
|
+
RGBA, or None. Same store the editor tabs / folder tree read."""
|
|
530
|
+
try:
|
|
531
|
+
from meltygui.editor.source_ui import _file_meta_tint
|
|
532
|
+
rgb = _file_meta_tint(path)
|
|
533
|
+
except Exception:
|
|
534
|
+
return None
|
|
535
|
+
return (*rgb, 1) if rgb else None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# Rect of the most recently copied entry and when it was copied, so the click gets
|
|
539
|
+
# a brief visual confirmation. Guarded so a hotswap re-exec keeps the value.
|
|
540
|
+
_copy_flash = globals().get("_copy_flash")
|
|
541
|
+
_COPY_FLASH_SECONDS = 0.6
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _handle_badge_click(badge_rects):
|
|
545
|
+
"""A click on a column's "N new" badge jumps that column back to the
|
|
546
|
+
sticky end (offset 0), where the seen-marker resets on the next frame.
|
|
547
|
+
Returns True when a badge consumed the click."""
|
|
548
|
+
if not badge_rects or not imgui.is_mouse_clicked(0):
|
|
549
|
+
return False
|
|
550
|
+
mouse = imgui.get_io().mouse_pos
|
|
551
|
+
for (x0, y0, x1, y1), tag in badge_rects:
|
|
552
|
+
if x0 <= mouse.x <= x1 and y0 <= mouse.y <= y1:
|
|
553
|
+
state = _scroll_state.get(tag)
|
|
554
|
+
if state is not None:
|
|
555
|
+
state["offset"] = 0.0
|
|
556
|
+
return True
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _handle_entry_click(hit_rects):
|
|
561
|
+
"""Click handling: a click inside an entry with a `jump` opens that
|
|
562
|
+
(path, line) in the code editor (global search's jump path); otherwise —
|
|
563
|
+
and always with ctrl held — the entry's text goes on the clipboard. Either
|
|
564
|
+
way the entry flashes."""
|
|
565
|
+
global _copy_flash
|
|
566
|
+
if not hit_rects or not imgui.is_mouse_clicked(0):
|
|
567
|
+
return
|
|
568
|
+
io = imgui.get_io()
|
|
569
|
+
mx, my = io.mouse_pos.x, io.mouse_pos.y
|
|
570
|
+
# Later entries are drawn above earlier ones and never overlap, so the first
|
|
571
|
+
# containing rect is the hit.
|
|
572
|
+
for (x0, y0, x1, y1), text, jump in hit_rects:
|
|
573
|
+
if x0 <= mx <= x1 and y0 <= my <= y1:
|
|
574
|
+
if jump is not None and not io.key_ctrl:
|
|
575
|
+
from meltygui.core.runtime.extensions import open_source as open_in_editor
|
|
576
|
+
open_in_editor(jump[0], jump[1])
|
|
577
|
+
else:
|
|
578
|
+
imgui.set_clipboard_text(text)
|
|
579
|
+
_copy_flash = ((x0, y0, x1, y1), time.time())
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _draw_copy_flash(draw_list):
|
|
584
|
+
"""Outline the entry that was just copied, fading out over ~0.6s."""
|
|
585
|
+
if _copy_flash is None:
|
|
586
|
+
return
|
|
587
|
+
(x0, y0, x1, y1), copied_at = _copy_flash
|
|
588
|
+
remaining = _COPY_FLASH_SECONDS - (time.time() - copied_at)
|
|
589
|
+
if remaining <= 0:
|
|
590
|
+
return
|
|
591
|
+
alpha = remaining / _COPY_FLASH_SECONDS
|
|
592
|
+
draw_list.add_rect(x0, y0, x1, y1,
|
|
593
|
+
pack_color(1, 1, 0, alpha), rounding=2, thickness=1.5)
|
|
594
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
595
|
+
request_render()
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def draw_notifications():
|
|
599
|
+
io = imgui.get_io()
|
|
600
|
+
display_size = io.display_size
|
|
601
|
+
draw_list = imgui.get_overlay_draw_list()
|
|
602
|
+
|
|
603
|
+
from meltygui.core.melty import Melty
|
|
604
|
+
font_handle = Melty.font_mgr.get(Font.JETBRAINS_MONO_14) if Melty.font_mgr else None
|
|
605
|
+
if font_handle is not None:
|
|
606
|
+
imgui.push_font(font_handle)
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# [tint=(0.36, 0.68, 0.89)]
|
|
610
|
+
padding = 2
|
|
611
|
+
# [tint=(0.95, 0.61, 0.07)]
|
|
612
|
+
column_width = 300 # also the toast max width
|
|
613
|
+
# Bottom strip of each band that its title text sits in.
|
|
614
|
+
# [tint=(0.72, 0.53, 0.94)]
|
|
615
|
+
title_strip = 30
|
|
616
|
+
# Gap between the bands and the window's right edge.
|
|
617
|
+
# [tint=(0.42, 0.79, 0.42)]
|
|
618
|
+
edge_margin = 10
|
|
619
|
+
# Per-category band height - a Toggles knob, applied live.
|
|
620
|
+
category_height = Toggles.Notifications.category_height
|
|
621
|
+
|
|
622
|
+
content_width = column_width - padding * 2 # left-aligned content box
|
|
623
|
+
line_height = imgui.get_text_line_height()
|
|
624
|
+
title_color = pack_color(1, 1, 0, 1)
|
|
625
|
+
|
|
626
|
+
tagged_columns = [(tag, notifications) for tag, notifications
|
|
627
|
+
in NotificationCenter.tagged_notifications.items()
|
|
628
|
+
if tag not in NotificationCenter.ignore_tags]
|
|
629
|
+
live_entries = [(tag + " ", value_str)
|
|
630
|
+
for tag, (value_str, _c, _t, _a) in NotificationCenter.live_values.items()]
|
|
631
|
+
|
|
632
|
+
n_columns = len(tagged_columns) + (1 if live_entries else 0)
|
|
633
|
+
if n_columns == 0:
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
# Categories stack VERTICALLY along the right edge: the first tag's
|
|
637
|
+
# history in the bottom-right corner, each further category in its own
|
|
638
|
+
# fixed-height band above it, "Live" topmost. Within each band the title
|
|
639
|
+
# is at the bottom with the scrolling viewport above it.
|
|
640
|
+
column_left = display_size.x - column_width - edge_margin
|
|
641
|
+
hit_rects = []
|
|
642
|
+
badge_rects = []
|
|
643
|
+
arrow_rects = []
|
|
644
|
+
# Bottom of the NEXT band to lay out; bands stack upward, a collapsed
|
|
645
|
+
# band taking only its title strip so the ones above it pack down.
|
|
646
|
+
next_band_bottom = [display_size.y]
|
|
647
|
+
|
|
648
|
+
def band_rect(tag):
|
|
649
|
+
"""(title_top, viewport_top, viewport_bottom, collapsed) of `tag`'s
|
|
650
|
+
band, stacked above the previously laid-out one."""
|
|
651
|
+
collapsed = tag in _collapsed_tags
|
|
652
|
+
band_bottom = next_band_bottom[0]
|
|
653
|
+
title_top = band_bottom - title_strip
|
|
654
|
+
height = title_strip if collapsed else category_height
|
|
655
|
+
next_band_bottom[0] = band_bottom - height
|
|
656
|
+
return title_top, band_bottom - height, title_top - padding, collapsed
|
|
657
|
+
|
|
658
|
+
for tag, notifications in tagged_columns:
|
|
659
|
+
title_top, viewport_top, viewport_bottom, collapsed = band_rect(tag)
|
|
660
|
+
|
|
661
|
+
# tag title pinned at the bottom of the band; clicking it copies
|
|
662
|
+
# the band's whole history, not just the entries on screen.
|
|
663
|
+
_draw_column_title(draw_list, column_left, title_top, title_color, tag,
|
|
664
|
+
hit_rects, "\n".join(n.copy_text for n in notifications),
|
|
665
|
+
arrow_rects=arrow_rects, collapsed=collapsed)
|
|
666
|
+
if collapsed:
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
rows = []
|
|
670
|
+
for n in notifications:
|
|
671
|
+
# Entries that jump to a file take that file's editor tint.
|
|
672
|
+
tint = (_file_tint(n.jump[0]) if n.jump is not None else None) or n.tint
|
|
673
|
+
rows.append((n.time_label, tint, n.label, tint, n.created_at,
|
|
674
|
+
n.copy_text, n.jump))
|
|
675
|
+
_draw_scrolled_column(draw_list, io, tag, rows, column_left, content_width,
|
|
676
|
+
line_height, padding, viewport_top, viewport_bottom,
|
|
677
|
+
hit_rects, badge_rects)
|
|
678
|
+
|
|
679
|
+
# dedicated "Live" band above the tagged notification bands - each row
|
|
680
|
+
# is one tag's current value, tinted, updated in place over time - so
|
|
681
|
+
# it scrolls like the others but skips the "new" badge (in-place
|
|
682
|
+
# updates would keep it lit permanently).
|
|
683
|
+
if live_entries:
|
|
684
|
+
title_top, viewport_top, viewport_bottom, collapsed = band_rect("Live")
|
|
685
|
+
label_color = (0.6, 0.6, 0.6, 1)
|
|
686
|
+
|
|
687
|
+
_draw_column_title(draw_list, column_left, title_top, title_color, "Live",
|
|
688
|
+
hit_rects, "\n".join(f"{label}{value_str}"
|
|
689
|
+
for label, value_str in live_entries),
|
|
690
|
+
arrow_rects=arrow_rects, collapsed=collapsed)
|
|
691
|
+
|
|
692
|
+
if live_entries and not collapsed:
|
|
693
|
+
rows = [(live_tag + " ", label_color, value_str, color, created_at, None, None)
|
|
694
|
+
for live_tag, (value_str, color, _time_label, created_at)
|
|
695
|
+
in NotificationCenter.live_values.items()]
|
|
696
|
+
_draw_scrolled_column(draw_list, io, "Live", rows, column_left, content_width,
|
|
697
|
+
line_height, padding, viewport_top, viewport_bottom,
|
|
698
|
+
hit_rects, badge_rects, show_badge=False)
|
|
699
|
+
|
|
700
|
+
if not _handle_arrow_click(arrow_rects) and not _handle_badge_click(badge_rects):
|
|
701
|
+
_handle_entry_click(hit_rects)
|
|
702
|
+
_draw_copy_flash(draw_list)
|
|
703
|
+
finally:
|
|
704
|
+
if font_handle is not None:
|
|
705
|
+
imgui.pop_font()
|
|
706
|
+
|