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,41 @@
|
|
|
1
|
+
"""Collection model functions and supporting definitions."""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
4
|
+
from types import NoneType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def annotation_item_type(annotation):
|
|
8
|
+
"""Item type a collection annotation implies for new entries:
|
|
9
|
+
Dict[str, Lora] -> Lora, List[X] -> X, Optional[T] -> T. None when the
|
|
10
|
+
annotation carries no usable element type."""
|
|
11
|
+
args = [a for a in getattr(annotation, "__args__", ()) if a is not NoneType]
|
|
12
|
+
if not args:
|
|
13
|
+
return None
|
|
14
|
+
return args[-1]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _collection_match_keys(input_value, keys, excluded, show_excluded):
|
|
18
|
+
"""The (index, lowercased key string) pairs draw_collection renders and
|
|
19
|
+
searches, in key order — the basis for both counting key matches and
|
|
20
|
+
resolving which key holds the current match, without rendering. `index` is
|
|
21
|
+
the position in `keys`, so it lines up with the render loop. Mirrors the
|
|
22
|
+
loop's key-string derivation and skip filters."""
|
|
23
|
+
out = []
|
|
24
|
+
parent_cls_name = input_value.__class__.__name__
|
|
25
|
+
excl_attrs = getattr(type(input_value), "__excluded_attrs__", None)
|
|
26
|
+
for idx, key in enumerate(keys):
|
|
27
|
+
if isinstance(key, (float, Enum, NoneType)):
|
|
28
|
+
key_str = parent_cls_name
|
|
29
|
+
elif isinstance(key, int):
|
|
30
|
+
key_str = f"{key}"
|
|
31
|
+
else:
|
|
32
|
+
key_str = str(key)
|
|
33
|
+
if str(key).split("##")[0] in excluded:
|
|
34
|
+
continue
|
|
35
|
+
if (not show_excluded and excl_attrs is not None
|
|
36
|
+
and not Toggles.show_excluded and str(key) in excl_attrs):
|
|
37
|
+
continue
|
|
38
|
+
if not show_excluded and (key_str.startswith("_") or key_str.endswith("_")):
|
|
39
|
+
continue
|
|
40
|
+
out.append((idx, key_str.lower()))
|
|
41
|
+
return out
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Color model functions and supporting definitions."""
|
|
2
|
+
from meltygui.hdr_color import scale_saturation
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _brightness_clamp(r, g, b, min_b, max_b):
|
|
6
|
+
"""Clamp PERCEIVED brightness (0.299r + 0.587g + 0.114b) — the
|
|
7
|
+
legibility guard for tinted colors. Both directions SCALE the channels,
|
|
8
|
+
which preserves their ratios and therefore saturation — a dark
|
|
9
|
+
high-saturation tint lifts to a dark high-saturation color, it does NOT
|
|
10
|
+
wash toward gray (a uniform add did, which made lowering the value
|
|
11
|
+
factor also lose saturation). Only true near-black — no hue left to
|
|
12
|
+
preserve — falls back to the uniform add."""
|
|
13
|
+
# Inverted clamp (min above max - mid-drag or experimental toggle values)
|
|
14
|
+
# collapses to the floor: without this, dark colors LIFT to min while
|
|
15
|
+
# bright ones CRUSH to max < min, inverting brightness ordering and pinning
|
|
16
|
+
# every wash to near-identical luminance ("tints stopped responding").
|
|
17
|
+
if 0 < max_b < min_b:
|
|
18
|
+
max_b = min_b
|
|
19
|
+
lum = 0.299 * r + 0.587 * g + 0.114 * b
|
|
20
|
+
if lum < min_b:
|
|
21
|
+
if lum > 1e-4:
|
|
22
|
+
k = min_b / lum
|
|
23
|
+
return min(1.0, r * k), min(1.0, g * k), min(1.0, b * k)
|
|
24
|
+
d = min_b - lum
|
|
25
|
+
return min(1.0, r + d), min(1.0, g + d), min(1.0, b + d)
|
|
26
|
+
if lum > max_b > 0 and lum > 0:
|
|
27
|
+
k = max_b / lum
|
|
28
|
+
return r * k, g * k, b * k
|
|
29
|
+
return r, g, b
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clamp_bg_value(color, max_bg_value):
|
|
33
|
+
"""Cap a background color's VALUE (max channel, as in HSV) at `max_bg_value`,
|
|
34
|
+
keeping hue exact and BOOSTING saturation by the same factor the value was
|
|
35
|
+
cut by (s / k, clamped to 1.0). A plain uniform channel scale holds HSV
|
|
36
|
+
saturation constant but still reads as washed out once it's dark, so the
|
|
37
|
+
boost buys the colorfulness back — the color only ever gets darker and
|
|
38
|
+
*more* saturated, never grayer.
|
|
39
|
+
|
|
40
|
+
This is the LAST thing applied to a bg color — it bounds the color actually
|
|
41
|
+
painted, not the depth ramp that fed it, so whatever the depth/tint/bleed
|
|
42
|
+
chain produced, `max_bg_value=0` is black and `0.2` is at most 20% value.
|
|
43
|
+
Deliberately not text_editor's _brightness_clamp: that one is a perceptual
|
|
44
|
+
(luma) guard and no-ops at max_b == 0, which would break the black case.
|
|
45
|
+
|
|
46
|
+
Done in raw channel arithmetic rather than a colorsys round trip: hue is
|
|
47
|
+
just the position of the mid channel in the [min, max] span, so rebuilding
|
|
48
|
+
against the new chroma preserves it without ever naming an angle."""
|
|
49
|
+
if max_bg_value is None or color is None or len(color) < 3:
|
|
50
|
+
return color
|
|
51
|
+
rest = tuple(color[3:])
|
|
52
|
+
red, green, blue = color[0], color[1], color[2]
|
|
53
|
+
value = max(red, green, blue)
|
|
54
|
+
if value <= max_bg_value:
|
|
55
|
+
return color
|
|
56
|
+
if max_bg_value <= 0 or value <= 0:
|
|
57
|
+
return (0.0, 0.0, 0.0) + rest
|
|
58
|
+
|
|
59
|
+
low = min(red, green, blue)
|
|
60
|
+
if low >= value: # achromatic - no hue to preserve, just darken
|
|
61
|
+
return (max_bg_value, max_bg_value, max_bg_value) + rest
|
|
62
|
+
|
|
63
|
+
# k is the cut applied to the value; undo it with saturation.
|
|
64
|
+
k = max_bg_value / value
|
|
65
|
+
saturation = scale_saturation((value - low) / value, 1.0 / k)
|
|
66
|
+
chroma = max_bg_value * saturation
|
|
67
|
+
new_low = max_bg_value - chroma
|
|
68
|
+
span = value - low
|
|
69
|
+
return (new_low + (red - low) / span * chroma,
|
|
70
|
+
new_low + (green - low) / span * chroma,
|
|
71
|
+
new_low + (blue - low) / span * chroma) + rest
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _wide_pick(fx, fy, top_fraction, max_stops):
|
|
75
|
+
"""Square fractions (x right, y down) → (s, v, exposure)."""
|
|
76
|
+
fx = min(max(fx, 0.0), 1.0)
|
|
77
|
+
fy = min(max(fy, 0.0), 1.0)
|
|
78
|
+
if fy < top_fraction:
|
|
79
|
+
return fx, 1.0, 2.0 ** (max_stops * (1.0 - fy / top_fraction))
|
|
80
|
+
v = 1.0 - (fy - top_fraction) / max(1e-6, 1.0 - top_fraction)
|
|
81
|
+
return fx, min(max(v, 0.0), 1.0), 1.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _wide_marker(s, v, exposure, top_fraction, max_stops):
|
|
85
|
+
"""(s, v, exposure) → square fractions; the inverse of _wide_pick."""
|
|
86
|
+
import math
|
|
87
|
+
if exposure > 1.0:
|
|
88
|
+
fy = top_fraction * (1.0 - min(1.0, math.log2(exposure) / max_stops))
|
|
89
|
+
else:
|
|
90
|
+
fy = top_fraction + (1.0 - v) * (1.0 - top_fraction)
|
|
91
|
+
return min(max(s, 0.0), 1.0), min(max(fy, 0.0), 1.0)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _srgb_plus_pick(px, py, square, ext, band, max_stops):
|
|
95
|
+
"""Cursor offset from the SQUARE's top-left, in px (negative y = over
|
|
96
|
+
the exposure band, x past `square` = over the P3 strip) →
|
|
97
|
+
(s, x, v, exposure)."""
|
|
98
|
+
if px <= square:
|
|
99
|
+
s, x = max(px / square, 0.0), 0.0
|
|
100
|
+
else:
|
|
101
|
+
s, x = 1.0, min(max((px - square) / max(1e-6, ext), 0.0), 1.0)
|
|
102
|
+
if py < 0.0:
|
|
103
|
+
fy = min(max(-py / max(1e-6, band), 0.0), 1.0) # 0 at the seam, 1 at the top
|
|
104
|
+
return s, x, 1.0, 2.0 ** (max_stops * fy)
|
|
105
|
+
v = 1.0 - min(max(py / square, 0.0), 1.0)
|
|
106
|
+
return s, x, v, 1.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _srgb_plus_marker(s, x, v, exposure, square, ext, band, max_stops):
|
|
110
|
+
"""(s, x, v, exposure) → marker offset from the square's top-left, in
|
|
111
|
+
px; the inverse of _srgb_plus_pick."""
|
|
112
|
+
import math
|
|
113
|
+
px = s * square if x <= 0.0 else square + x * ext
|
|
114
|
+
if exposure > 1.0:
|
|
115
|
+
py = -band * min(1.0, math.log2(exposure) / max_stops)
|
|
116
|
+
else:
|
|
117
|
+
py = (1.0 - v) * square
|
|
118
|
+
return px, py
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _extension_pick(fx, ext_fraction):
|
|
122
|
+
"""Cursor x as a fraction of the SQUARE's width (past 1 = over the
|
|
123
|
+
extension, whose width is ext_fraction squares) → (s, x): the classic
|
|
124
|
+
saturation inside the square, s = 1 and the P3 depth x past the seam."""
|
|
125
|
+
if fx <= 1.0:
|
|
126
|
+
return max(fx, 0.0), 0.0
|
|
127
|
+
return 1.0, min(max((fx - 1.0) / max(1e-6, ext_fraction), 0.0), 1.0)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""CUDA decoding of tensor primitive dtypes without packing source storage."""
|
|
2
|
+
|
|
3
|
+
DTYPE_CODES = {
|
|
4
|
+
"torch.float32": 0, "torch.float16": 1, "torch.bfloat16": 2,
|
|
5
|
+
"torch.float64": 3, "torch.int8": 4, "torch.uint8": 5, "torch.bool": 5,
|
|
6
|
+
"torch.int16": 6, "torch.int32": 7, "torch.int64": 8,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
CUDA_LOAD_SOURCE = r"""
|
|
10
|
+
#include <cuda_fp16.h>
|
|
11
|
+
|
|
12
|
+
__device__ __forceinline__ float load_at(const unsigned char* __restrict__ d,
|
|
13
|
+
int dtype, long long e) {
|
|
14
|
+
switch (dtype) {
|
|
15
|
+
case 0: return ((const float*)d)[e];
|
|
16
|
+
case 1: return __half2float(((const __half*)d)[e]);
|
|
17
|
+
case 2: { unsigned short u = ((const unsigned short*)d)[e];
|
|
18
|
+
return __uint_as_float(((unsigned)u) << 16); }
|
|
19
|
+
case 3: return (float)((const double*)d)[e];
|
|
20
|
+
case 4: return (float)((const signed char*)d)[e];
|
|
21
|
+
case 5: return (float)d[e];
|
|
22
|
+
case 6: return (float)((const short*)d)[e];
|
|
23
|
+
case 7: return (float)((const int*)d)[e];
|
|
24
|
+
case 8: return (float)((const long long*)d)[e];
|
|
25
|
+
}
|
|
26
|
+
return 0.0f;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def dtype_code(t):
|
|
33
|
+
code = DTYPE_CODES.get(str(t.dtype))
|
|
34
|
+
if code is None:
|
|
35
|
+
raise ValueError(f"unsupported CUDA tensor dtype {t.dtype}")
|
|
36
|
+
return code
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Versioned CUDA tensors exposed as GLTexture values without a CPU upload.
|
|
2
|
+
|
|
3
|
+
The injected GLState owns each texture/PBO/registration as one resource. CUDA
|
|
4
|
+
unregistration must finish before the GL buffer is deleted. The resource holds
|
|
5
|
+
no reference to the source tensor; cache hits do no tensor copies or syncs.
|
|
6
|
+
"""
|
|
7
|
+
import ctypes
|
|
8
|
+
|
|
9
|
+
import OpenGL.GL as gl
|
|
10
|
+
|
|
11
|
+
from meltygui.core.graphics.cuda_context_core import current_device_index
|
|
12
|
+
from meltygui.core.graphics.cuda_interop_core import (
|
|
13
|
+
interop_context, copy_to_buffer, log_once,
|
|
14
|
+
register_buffer, unregister_buffer,
|
|
15
|
+
)
|
|
16
|
+
from meltygui.core.graphics.gl_state import (
|
|
17
|
+
GLTexture, _scalar, current_context, is_gl_thread, texture3d_fit, tight_unpack,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CudaVolume:
|
|
22
|
+
"""Owned allocation behind a texture value; its source tensor stays external."""
|
|
23
|
+
__slots__ = ("texture", "buffer", "registered", "nbytes", "last_version")
|
|
24
|
+
|
|
25
|
+
def __init__(self, texture, buffer, registered, nbytes):
|
|
26
|
+
self.texture = texture
|
|
27
|
+
self.buffer = buffer
|
|
28
|
+
self.registered = registered
|
|
29
|
+
self.nbytes = nbytes
|
|
30
|
+
self.last_version = object() # even version=None must perform its first upload
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _release_volume(volume, context):
|
|
34
|
+
# Each completed step is recorded so a failed deletion can be retried.
|
|
35
|
+
# Keep the implementation at module scope so queued deleters see hot edits.
|
|
36
|
+
if volume.registered is not None:
|
|
37
|
+
unregister_buffer(volume.registered, context)
|
|
38
|
+
volume.registered = None
|
|
39
|
+
if volume.buffer is not None:
|
|
40
|
+
gl.glDeleteBuffers(1, [volume.buffer])
|
|
41
|
+
volume.buffer = None
|
|
42
|
+
if volume.texture is not None:
|
|
43
|
+
gl.glDeleteTextures([volume.texture.texture_id])
|
|
44
|
+
volume.texture = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def tensor_to_texture(gl_state, key, tensor, version):
|
|
48
|
+
"""Return a versioned 3-D float16/32 GLTexture, or None for CPU fallback.
|
|
49
|
+
|
|
50
|
+
Noncontiguous tensors are packed only on version misses. Tensors on another
|
|
51
|
+
GPU are staged by Torch onto the GL device, never copied across devices with
|
|
52
|
+
a raw CUDA pointer copy. Calls require the owner's GL context and a compatible
|
|
53
|
+
recorded CUDA context. Core activates it locally and restores the caller's
|
|
54
|
+
native context, or lazily establishes the display context when none exists.
|
|
55
|
+
An incompatible caller-owned PyCUDA context is left untouched.
|
|
56
|
+
"""
|
|
57
|
+
if not is_gl_thread() or current_context() is None:
|
|
58
|
+
return None
|
|
59
|
+
if gl_state._context != current_context():
|
|
60
|
+
return None
|
|
61
|
+
import torch
|
|
62
|
+
if not (isinstance(tensor, torch.Tensor) and tensor.is_cuda
|
|
63
|
+
and tensor.dim() == 3 and tensor.dtype in (torch.float16, torch.float32)):
|
|
64
|
+
return None
|
|
65
|
+
with interop_context() as context:
|
|
66
|
+
if context is None:
|
|
67
|
+
return None
|
|
68
|
+
return _upload_tensor(gl_state, key, tensor, version, context)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _upload_tensor(gl_state, key, tensor, version, context):
|
|
72
|
+
import torch
|
|
73
|
+
device = current_device_index()
|
|
74
|
+
depth, height, width = (int(size) for size in tensor.shape)
|
|
75
|
+
half = tensor.dtype == torch.float16
|
|
76
|
+
internal = gl.GL_R16F if half else gl.GL_R32F
|
|
77
|
+
gl_type = gl.GL_HALF_FLOAT if half else gl.GL_FLOAT
|
|
78
|
+
nbytes = tensor.nelement() * tensor.element_size()
|
|
79
|
+
|
|
80
|
+
def delete(volume):
|
|
81
|
+
_release_volume(volume, context)
|
|
82
|
+
|
|
83
|
+
def create():
|
|
84
|
+
_, problems = texture3d_fit((depth, height, width), tensor.element_size())
|
|
85
|
+
if problems:
|
|
86
|
+
raise ValueError("tensor_to_texture: " + "; ".join(problems))
|
|
87
|
+
volume = CudaVolume(None, None, None, nbytes)
|
|
88
|
+
previous_buffer = _scalar(gl.glGetIntegerv(gl.GL_ARRAY_BUFFER_BINDING))
|
|
89
|
+
previous_unpack = _scalar(gl.glGetIntegerv(gl.GL_PIXEL_UNPACK_BUFFER_BINDING))
|
|
90
|
+
previous_texture = _scalar(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_3D))
|
|
91
|
+
try:
|
|
92
|
+
volume.buffer = _scalar(gl.glGenBuffers(1))
|
|
93
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, volume.buffer)
|
|
94
|
+
gl.glBufferData(gl.GL_ARRAY_BUFFER, nbytes, None, gl.GL_DYNAMIC_DRAW)
|
|
95
|
+
volume.registered = register_buffer(volume.buffer)
|
|
96
|
+
texture_id = _scalar(gl.glGenTextures(1))
|
|
97
|
+
volume.texture = GLTexture(texture_id, gl.GL_TEXTURE_3D,
|
|
98
|
+
(depth, height, width), internal)
|
|
99
|
+
gl.glBindTexture(gl.GL_TEXTURE_3D, texture_id)
|
|
100
|
+
# NULL must mean no client data, not offset zero in a caller's PBO.
|
|
101
|
+
gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0)
|
|
102
|
+
gl.glTexImage3D(gl.GL_TEXTURE_3D, 0, internal, width, height, depth, 0,
|
|
103
|
+
gl.GL_RED, gl_type, ctypes.c_void_p(0))
|
|
104
|
+
for parameter, value in (
|
|
105
|
+
(gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST),
|
|
106
|
+
(gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST),
|
|
107
|
+
(gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE),
|
|
108
|
+
(gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE),
|
|
109
|
+
(gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_EDGE)):
|
|
110
|
+
gl.glTexParameteri(gl.GL_TEXTURE_3D, parameter, value)
|
|
111
|
+
return volume
|
|
112
|
+
except Exception:
|
|
113
|
+
gl_state.defer_delete(key, volume, delete)
|
|
114
|
+
raise
|
|
115
|
+
finally:
|
|
116
|
+
gl.glBindTexture(gl.GL_TEXTURE_3D, previous_texture)
|
|
117
|
+
gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, previous_unpack)
|
|
118
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, previous_buffer)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
volume = gl_state.get(key, create, delete,
|
|
122
|
+
deps=((depth, height, width), "f16" if half else "f32", context))
|
|
123
|
+
if volume.last_version != version:
|
|
124
|
+
if tensor.device.index != device:
|
|
125
|
+
# Finish the source producer before Torch stages across devices.
|
|
126
|
+
torch.cuda.synchronize(tensor.device)
|
|
127
|
+
tensor = tensor.to(f"cuda:{device}")
|
|
128
|
+
tensor = tensor.contiguous()
|
|
129
|
+
# The producer can be on a nondefault stream. Waiting AFTER the
|
|
130
|
+
# driver copy is too late, and synchronize() without a device can
|
|
131
|
+
# wait on a completely different GPU.
|
|
132
|
+
torch.cuda.synchronize(device)
|
|
133
|
+
copy_to_buffer(volume.registered, context, tensor.data_ptr(), nbytes)
|
|
134
|
+
previous_unpack = _scalar(gl.glGetIntegerv(gl.GL_PIXEL_UNPACK_BUFFER_BINDING))
|
|
135
|
+
previous_texture = _scalar(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_3D))
|
|
136
|
+
try:
|
|
137
|
+
gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, volume.buffer)
|
|
138
|
+
gl.glBindTexture(gl.GL_TEXTURE_3D, volume.texture.texture_id)
|
|
139
|
+
with tight_unpack():
|
|
140
|
+
gl.glTexSubImage3D(gl.GL_TEXTURE_3D, 0, 0, 0, 0, width, height, depth,
|
|
141
|
+
gl.GL_RED, gl_type, None)
|
|
142
|
+
finally:
|
|
143
|
+
gl.glBindTexture(gl.GL_TEXTURE_3D, previous_texture)
|
|
144
|
+
gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, previous_unpack)
|
|
145
|
+
volume.last_version = version
|
|
146
|
+
return volume.texture
|
|
147
|
+
except Exception as error:
|
|
148
|
+
log_once(f"interop upload failed ({error}); falling back to cpu path")
|
|
149
|
+
return None
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Dropdown model functions and supporting definitions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _dd_entries(container):
|
|
6
|
+
"""Normalized (key, value, label, is_branch) rows for one level. Dict rows
|
|
7
|
+
read by their key (small/medium); list/tuple rows by their value
|
|
8
|
+
(left/center/right) since the index isn't meaningful to the user."""
|
|
9
|
+
if isinstance(container, dict):
|
|
10
|
+
items = list(container.items())
|
|
11
|
+
labelled = [(k, v, str(k)) for k, v in items]
|
|
12
|
+
else:
|
|
13
|
+
labelled = [(i, v, str(v)) for i, v in enumerate(container)]
|
|
14
|
+
return [(k, v, lbl, isinstance(v, (dict, list))) for k, v, lbl in labelled]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dd_subtree_matches(value, search):
|
|
18
|
+
"""True if `search` (already lowercased) appears anywhere in this value's
|
|
19
|
+
subtree, so a branch stays visible while searching when a descendant matches."""
|
|
20
|
+
if not search:
|
|
21
|
+
return True
|
|
22
|
+
if isinstance(value, dict):
|
|
23
|
+
return any(search in str(k).lower() or _dd_subtree_matches(v, search)
|
|
24
|
+
for k, v in value.items())
|
|
25
|
+
if isinstance(value, list):
|
|
26
|
+
return any(_dd_subtree_matches(v, search) for v in value)
|
|
27
|
+
return search in str(value).lower()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _dd_visible_entries(container, search=""):
|
|
31
|
+
"""Rows shown for a level under `search`: a leaf whose label matches, or a
|
|
32
|
+
branch matching by label OR holding a matching descendant. Empty `search`
|
|
33
|
+
keeps everything."""
|
|
34
|
+
if not isinstance(container, (dict, list)):
|
|
35
|
+
return []
|
|
36
|
+
rows = _dd_entries(container)
|
|
37
|
+
if not search:
|
|
38
|
+
return rows
|
|
39
|
+
return [(k, v, lbl, br) for (k, v, lbl, br) in rows
|
|
40
|
+
if search in lbl.lower() or (br and _dd_subtree_matches(v, search))]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _dd_walk(collection, path):
|
|
44
|
+
"""Descend `collection` along a key/index `path`, returning the node there or
|
|
45
|
+
None if the path no longer resolves (e.g. after a search prunes it)."""
|
|
46
|
+
node = collection
|
|
47
|
+
for k in path:
|
|
48
|
+
try:
|
|
49
|
+
node = node[k]
|
|
50
|
+
except (KeyError, IndexError, TypeError):
|
|
51
|
+
return None
|
|
52
|
+
return node
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _dd_rows_at(collection, path, search):
|
|
56
|
+
"""Visible rows at `path`, applying the once-a-branch-matches-by-label rule:
|
|
57
|
+
if any ancestor key on `path` matched the search by its own label, that whole
|
|
58
|
+
subtree counts as a match, so deeper levels are shown unfiltered."""
|
|
59
|
+
container = _dd_walk(collection, path)
|
|
60
|
+
ancestor_matched = bool(search) and any(search in str(k).lower() for k in path)
|
|
61
|
+
return _dd_visible_entries(container, "" if ancestor_matched else search)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _dd_first_match_leaf(container, search, prefix=()):
|
|
65
|
+
"""DFS for the path to the first selectable leaf the search reveals, so the
|
|
66
|
+
cursor can jump straight to it (auto-expanding the branches above). A branch
|
|
67
|
+
that matches by its own label contributes its first leaf unfiltered."""
|
|
68
|
+
for key, value, label, is_branch in _dd_visible_entries(container, search):
|
|
69
|
+
path = tuple(prefix) + (key,)
|
|
70
|
+
if not is_branch:
|
|
71
|
+
return path
|
|
72
|
+
sub_search = "" if (search and search in label.lower()) else search
|
|
73
|
+
sub = _dd_first_match_leaf(value, sub_search, path)
|
|
74
|
+
if sub is not None:
|
|
75
|
+
return sub
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _dd_as_tuple(x):
|
|
80
|
+
"""Coerce a stored path-state to a tuple. The states are meant to be key
|
|
81
|
+
tuples, but DropDownState is a DictConversion and its machinery can alias a
|
|
82
|
+
complex stored value (e.g. a Lora) across fields; this keeps the dropdown
|
|
83
|
+
robust to any input type by never iterating a non-sequence."""
|
|
84
|
+
if isinstance(x, tuple):
|
|
85
|
+
return x
|
|
86
|
+
if isinstance(x, list):
|
|
87
|
+
return tuple(x)
|
|
88
|
+
return ()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _dd_path_for_value(collection, value, _depth=0):
|
|
92
|
+
"""The key/index path of the first LEAF in `collection` equal to
|
|
93
|
+
`value` (depth-first through nested dicts / lists), or None when no
|
|
94
|
+
leaf holds it — the inverse of _dd_walk for the trigger label sync."""
|
|
95
|
+
if _depth > 8 or value is None:
|
|
96
|
+
return None
|
|
97
|
+
items = (collection.items() if isinstance(collection, dict)
|
|
98
|
+
else enumerate(collection) if isinstance(collection, (list, tuple))
|
|
99
|
+
else ())
|
|
100
|
+
for key, node in items:
|
|
101
|
+
if isinstance(node, (dict, list, tuple)):
|
|
102
|
+
sub = _dd_path_for_value(node, value, _depth + 1)
|
|
103
|
+
if sub is not None:
|
|
104
|
+
return (key,) + sub
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
same = node == value
|
|
108
|
+
except Exception:
|
|
109
|
+
same = False
|
|
110
|
+
if same is True:
|
|
111
|
+
return (key,)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _dd_label_for_path(collection, path):
|
|
116
|
+
"""Display label for a selected leaf path: the KEY for a dict entry (e.g.
|
|
117
|
+
"red"), the VALUE for a list entry (e.g. "left"). Used for the trigger title
|
|
118
|
+
so it reads as a name, not a raw value (which may be a tuple/number)."""
|
|
119
|
+
if not path:
|
|
120
|
+
return ""
|
|
121
|
+
parent = _dd_walk(collection, tuple(path[:-1]))
|
|
122
|
+
if isinstance(parent, dict):
|
|
123
|
+
return str(path[-1])
|
|
124
|
+
return str(_dd_walk(collection, tuple(path)))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _dd_row_lookup(mapping, value):
|
|
128
|
+
|
|
129
|
+
"""mapping.get(value), tolerant of UNHASHABLE row values — a BRANCH row's
|
|
130
|
+
value is the nested collection dict itself, which raised TypeError from
|
|
131
|
+
every value-keyed style lookup (row_tags/row_tints/...)."""
|
|
132
|
+
if not mapping:
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
return mapping.get(value)
|
|
136
|
+
except TypeError:
|
|
137
|
+
return None
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Operations over supplied per-file dictionaries; no shared-store discovery."""
|
|
2
|
+
from meltygui.models.file_meta import FileMeta
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def set_row_tint(meta, path, value):
|
|
6
|
+
"""Write `value` (an rgb(a) tuple) as the tint of `path` in the shared
|
|
7
|
+
file-meta store, creating the entry only now — the listing never
|
|
8
|
+
setdefault()s entries for the files it merely shows. None (the picker's
|
|
9
|
+
clear) removes the tint, and the entry too when it holds nothing else,
|
|
10
|
+
so an unpainted file leaves no trace in file_meta.pkl."""
|
|
11
|
+
key = str(path)
|
|
12
|
+
entry = meta.get(key)
|
|
13
|
+
if value is None:
|
|
14
|
+
if isinstance(entry, dict) and dict.__contains__(entry, "tint"):
|
|
15
|
+
del entry["tint"]
|
|
16
|
+
if dict.__len__(entry) == 0:
|
|
17
|
+
del meta[key]
|
|
18
|
+
return
|
|
19
|
+
if not isinstance(entry, dict):
|
|
20
|
+
entry = meta[key] = FileMeta()
|
|
21
|
+
entry["tint"] = tuple(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ordered_rows(rows, meta):
|
|
25
|
+
"""`rows` ([(Path, is_dir)], natural order) sorted by the `order` stamps
|
|
26
|
+
in the meta store, the studio's rule (folder_files._apply_meta):
|
|
27
|
+
stamped rows first by their number, unstamped ones after in natural
|
|
28
|
+
order. No stamps at all: `rows` itself."""
|
|
29
|
+
if meta is None:
|
|
30
|
+
return rows
|
|
31
|
+
orders = {}
|
|
32
|
+
for i, (path, _is_dir) in enumerate(rows):
|
|
33
|
+
entry = meta.get(str(path))
|
|
34
|
+
if isinstance(entry, dict):
|
|
35
|
+
order = entry.get("order")
|
|
36
|
+
if isinstance(order, (int, float)):
|
|
37
|
+
orders[i] = order
|
|
38
|
+
if not orders:
|
|
39
|
+
return rows
|
|
40
|
+
indexed = sorted(range(len(rows)), key=lambda i: (orders.get(i, float("inf")), i))
|
|
41
|
+
return [rows[i] for i in indexed]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_row_order(meta, paths):
|
|
45
|
+
"""Stamp `order` = position into the meta entry of every path of a
|
|
46
|
+
directory (created for the rows that have none — a reorder is the
|
|
47
|
+
user's explicit edit of the folder, like painting it)."""
|
|
48
|
+
for i, path in enumerate(paths):
|
|
49
|
+
key = str(path)
|
|
50
|
+
entry = meta.get(key)
|
|
51
|
+
if not isinstance(entry, dict):
|
|
52
|
+
entry = meta[key] = FileMeta()
|
|
53
|
+
if entry.get("order") != i:
|
|
54
|
+
entry["order"] = i
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply_row_drop(rows, drag_keys, first_visible, drop):
|
|
58
|
+
"""The directory's new order after `drop` (a DropEvent from on_drop, or
|
|
59
|
+
None): `drag_keys` are the rows that registered a drag handle this run
|
|
60
|
+
— the visible ones, a contiguous slice of `rows` starting at
|
|
61
|
+
`first_visible` — and the event's indices count in that slice. Returns
|
|
62
|
+
the complete [Path] order to stamp, or None when nothing moved (no
|
|
63
|
+
drop, a drop back in place, a cross-collection kind: rows only reorder
|
|
64
|
+
here)."""
|
|
65
|
+
if drop is None or drop.kind != "reorder" or not drag_keys:
|
|
66
|
+
return None
|
|
67
|
+
keys = list(drag_keys)
|
|
68
|
+
if not drop.apply(keys):
|
|
69
|
+
return None
|
|
70
|
+
paths = [path for path, _is_dir in rows]
|
|
71
|
+
return paths[:first_visible] + keys + paths[first_visible + len(drag_keys):]
|
|
72
|
+
|
|
73
|
+
|