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,984 @@
|
|
|
1
|
+
"""GlfwRenderer variant that lets the foreground/overlay draw list be deferred.
|
|
2
|
+
|
|
3
|
+
Frame lifecycle:
|
|
4
|
+
|
|
5
|
+
imgui.new_frame()
|
|
6
|
+
# ... build UI; emit overlay primitives via get_foreground_draw_list() ...
|
|
7
|
+
end_frame()
|
|
8
|
+
renderer.begin_frame_split()
|
|
9
|
+
imgui.render()
|
|
10
|
+
draw_data = imgui.get_draw_data()
|
|
11
|
+
renderer.render_except_overlay(draw_data)
|
|
12
|
+
# ... custom GL work happens here, on top of windows but below overlay ...
|
|
13
|
+
renderer.render_overlay_only(draw_data)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import absolute_import
|
|
16
|
+
|
|
17
|
+
import ctypes
|
|
18
|
+
|
|
19
|
+
from meltygui.core.styling.style import adjust_text_color
|
|
20
|
+
|
|
21
|
+
import OpenGL.GL as gl
|
|
22
|
+
import meltygui_imgui as imgui
|
|
23
|
+
import meltygui.core.windowing.window_api as glfw
|
|
24
|
+
from meltygui.core.windowing.backends.imgui_renderer import WindowRenderer
|
|
25
|
+
from meltygui_imgui.integrations.opengl import get_common_gl_state, restore_common_gl_state
|
|
26
|
+
from meltygui.hdr_color import GLSL_DECODE as _GLSL_DECODE
|
|
27
|
+
from meltygui.hdr_color import GLSL_UNPREMULTIPLY as _GLSL_UNPREMULTIPLY
|
|
28
|
+
from meltygui.hdr_color import GLSL_TEXT_CLAMP as _GLSL_TEXT_CLAMP
|
|
29
|
+
from meltygui.hdr_color import set_decode_uniforms
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _FakeWindowRect:
|
|
33
|
+
"""Synthetic stand-in for a window draw_state, used to feed a fixed test
|
|
34
|
+
rect through the real masking path (_setup_channel_stencil)."""
|
|
35
|
+
closed = False
|
|
36
|
+
def __init__(self, layer, x, y, w, h):
|
|
37
|
+
self.layer = layer
|
|
38
|
+
self.abs_left = x
|
|
39
|
+
self.abs_top = y
|
|
40
|
+
self.width = w
|
|
41
|
+
self.height = h
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SplitOverlayRenderer(WindowRenderer):
|
|
45
|
+
debug_overlay_mask = False # one-shot diagnostics of window-mask overlay
|
|
46
|
+
# ISOLATION TEST: when set, inject a synthetic high-layer "window" with this
|
|
47
|
+
# fixed-screen rect into window_channels, so it flows through the SAME
|
|
48
|
+
# _setup_channel_stencil path as real windows. If sub-top overlays get cut
|
|
49
|
+
# inside this rect, the per-window masking path itself is correct and the
|
|
50
|
+
# bug is in the real windows' channel/coord data.
|
|
51
|
+
debug_static_mask = False
|
|
52
|
+
debug_static_rect = (400, 400, 400, 400) # screen-space (x, y, w, h)
|
|
53
|
+
|
|
54
|
+
# LCD subpixel text. The font atlas is 3x oversampled horizontally
|
|
55
|
+
# (fonts.py, FontSpec.oversample), so a glyph edge covers exactly three
|
|
56
|
+
# atlas texels per screen pixel and the texel at -1/0/+1 IS the coverage
|
|
57
|
+
# of the R/G/B sub's centre (stb's 3-wide box prefilter = FreeType's
|
|
58
|
+
# 'light' LCD filter). Dual-source blending applies that per channel:
|
|
59
|
+
# dst = src.rgb * cov.rgb + dst * (1 - cov.rgb)
|
|
60
|
+
# Everything else imgui draws keeps exact stock behaviour: solid
|
|
61
|
+
# geometry (rects, fills, AA fringes) uses the atlas white pixel with a
|
|
62
|
+
# CONSTANT uv -> fwidth(uv) == 0 exactly; thin AA lines sample the
|
|
63
|
+
# TexUvLines strip, which only varies in u -> the two-axis test keeps
|
|
64
|
+
# them grayscale; images/tiles are actual textures (Atlas == 0) and get
|
|
65
|
+
# uniform coverage == alpha, which with the same blend func is plain
|
|
66
|
+
# alpha blending. Proven byte-identical for non-glyph UI lists.
|
|
67
|
+
# Vertex colours are Melty's HDR/P3 packing (gl_gui/hdr_color.py): the
|
|
68
|
+
# vertex stage decodes the u32 into LINEAR scRGB and alpha, so the
|
|
69
|
+
# blend func happens in linear light on the fp16 scene target and a
|
|
70
|
+
# colour above 1.0 (white(4)) or outside sRGB (p3(1, 0, 0)) arrives
|
|
71
|
+
# intact. The two curve uniforms are set per draw (_bind_text_mode).
|
|
72
|
+
# The result is PREMULTIPLIED (melty_decode_premultiplied): imgui's AA
|
|
73
|
+
# feather vertices carry a zero alpha byte, which is not an SDR colour,
|
|
74
|
+
# and should not bleed their rgb to the edge.
|
|
75
|
+
VERTEX_SHADER_SRC = """
|
|
76
|
+
#version 330
|
|
77
|
+
|
|
78
|
+
uniform mat4 ProjMtx;
|
|
79
|
+
in vec2 Position;
|
|
80
|
+
in vec2 UV;
|
|
81
|
+
in vec4 Color;
|
|
82
|
+
out vec2 Frag_UV;
|
|
83
|
+
out vec4 Frag_Color;
|
|
84
|
+
""" + _GLSL_DECODE + """
|
|
85
|
+
void main() {
|
|
86
|
+
Frag_UV = UV;
|
|
87
|
+
Frag_Color = melty_decode_premultiplied(Color);
|
|
88
|
+
gl_Position = ProjMtx * vec4(Position.xy, 0, 1);
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
FRAGMENT_SHADER_SRC = """
|
|
93
|
+
#version 330
|
|
94
|
+
|
|
95
|
+
uniform sampler2D Texture;
|
|
96
|
+
uniform vec2 TexelSize; // 1 / font atlas size
|
|
97
|
+
uniform int Atlas; // 1: this command samples the font atlas
|
|
98
|
+
uniform int Lcd; // Toggles.Fonts.lcd_subpixel
|
|
99
|
+
uniform int Bgr; // Toggles.Fonts.lcd_bgr
|
|
100
|
+
uniform float Gamma; // Toggles.Fonts.text_gamma
|
|
101
|
+
in vec2 Frag_UV;
|
|
102
|
+
in vec4 Frag_Color; // premultiplied (see VERTEX_SHADER_SRC)
|
|
103
|
+
layout(location = 0, index = 0) out vec4 Out_Color;
|
|
104
|
+
layout(location = 0, index = 1) out vec4 Out_Cov;
|
|
105
|
+
""" + _GLSL_UNPREMULTIPLY + _GLSL_TEXT_CLAMP + """
|
|
106
|
+
/* dynamic text policy */
|
|
107
|
+
void main() {
|
|
108
|
+
vec4 color = melty_unpremultiply(Frag_Color);
|
|
109
|
+
vec4 t = texture(Texture, Frag_UV.st);
|
|
110
|
+
vec3 cov = vec3(t.a);
|
|
111
|
+
if (Atlas == 1) {
|
|
112
|
+
vec2 fw = fwidth(Frag_UV);
|
|
113
|
+
if (fw.x > 0.0 && fw.y > 0.0) {
|
|
114
|
+
// Glyph pixels only (solid geometry samples the atlas's white
|
|
115
|
+
// texel with a flat UV): cap HDR text at Toggles.HDR.text_max_stops.
|
|
116
|
+
color.rgb = melty_clamp_text(color.rgb);
|
|
117
|
+
if (DynamicStyles == 1) color.rgb = adjust_text_color(color.rgb);
|
|
118
|
+
if (Lcd == 1) {
|
|
119
|
+
float l = texture(Texture, Frag_UV.st - vec2(TexelSize.x, 0.0)).a;
|
|
120
|
+
float r = texture(Texture, Frag_UV.st + vec2(TexelSize.x, 0.0)).a;
|
|
121
|
+
cov = (Bgr == 1) ? vec3(r, t.a, l) : vec3(l, t.a, r);
|
|
122
|
+
}
|
|
123
|
+
cov = pow(cov, vec3(1.0 / Gamma));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
float a = color.a;
|
|
127
|
+
Out_Color = vec4(color.rgb * t.rgb, a * t.a);
|
|
128
|
+
Out_Cov = vec4(cov * a, a * t.a);
|
|
129
|
+
}
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
_STOCK_FRAGMENT_SHADER_SRC = WindowRenderer.FRAGMENT_SHADER_SRC
|
|
133
|
+
|
|
134
|
+
def __init__(self, window, attach_callbacks: bool = True):
|
|
135
|
+
# Set before super().__init__: it builds the device objects (shader)
|
|
136
|
+
# and the font texture, both of which the overrides below stamp onto
|
|
137
|
+
# these slots.
|
|
138
|
+
self._lcd_ok = False
|
|
139
|
+
self._loc_texel = self._loc_atlas = self._loc_lcd = self._loc_bgr = self._loc_gamma = -1
|
|
140
|
+
self._atlas_texel = (0.0, 0.0)
|
|
141
|
+
super().__init__(window, attach_callbacks=attach_callbacks)
|
|
142
|
+
self._has_overlay = False
|
|
143
|
+
self._scaled_this_frame = False
|
|
144
|
+
self._mask_debug_logged = False
|
|
145
|
+
|
|
146
|
+
def process_inputs(self):
|
|
147
|
+
super().process_inputs()
|
|
148
|
+
io = imgui.get_io()
|
|
149
|
+
# The stock backend reports the pointer as (-1, -1) whenever the OS
|
|
150
|
+
# window is not FOCUSED - but mouse hovers an unfocused window just
|
|
151
|
+
# fine, and a pointer at (-1, -1) reads as the top-left corner to
|
|
152
|
+
# titlebar._edge_at (the ↘ resize shape over the whole unfocused
|
|
153
|
+
# studio). While the pointer is over the window (GLFW's HOVERED
|
|
154
|
+
# flag) report its real position, so hover cursors and highlights
|
|
155
|
+
# resolve exactly as they do focused; off the window it stays the
|
|
156
|
+
# stock off-window sentinel.
|
|
157
|
+
if not glfw.get_window_attrib(self.window, glfw.FOCUSED) and \
|
|
158
|
+
glfw.get_window_attrib(self.window, glfw.HOVERED):
|
|
159
|
+
io.mouse_pos = glfw.get_cursor_pos(self.window)
|
|
160
|
+
# Shadow margin (titlebar.window_inset): imgui's display is the
|
|
161
|
+
# CONTENT and the real framebuffer is wider by the inset on every
|
|
162
|
+
# side; the render methods paint into that inset viewport and the
|
|
163
|
+
# pointer shifts back by it. Melty.frame_inset / framebuffer_size
|
|
164
|
+
# carry the two numbers to the masks, tiles, etc.
|
|
165
|
+
from meltygui.core.windowing.titlebar import window_inset
|
|
166
|
+
from meltygui.core.windowing.titlebar import content_origin
|
|
167
|
+
from meltygui.core.melty import Melty
|
|
168
|
+
inset = int(window_inset())
|
|
169
|
+
ox, oy = (int(v) for v in content_origin())
|
|
170
|
+
w, h = io.display_size
|
|
171
|
+
Melty.framebuffer_size = (int(w * io.display_fb_scale[0]), int(h * io.display_fb_scale[1]))
|
|
172
|
+
Melty.frame_inset = inset
|
|
173
|
+
Melty.frame_origin = (ox, oy)
|
|
174
|
+
if inset or ox or oy:
|
|
175
|
+
io.display_size = (max(1.0, w - ox - inset), max(1.0, h - oy - inset))
|
|
176
|
+
mx, my = io.mouse_pos
|
|
177
|
+
if mx > -1e6 and my > -1e6: # -FLT_MAX / (-1, -1) = off-window stays as is
|
|
178
|
+
io.mouse_pos = (mx - ox, my - oy)
|
|
179
|
+
self._cancel_surface_slide(io)
|
|
180
|
+
# A compositor move/resize grab (gl_gui/wayland_move.py) swallowed a
|
|
181
|
+
# button release, so GLFW's level state - what the stock poll above
|
|
182
|
+
# copies into io.mouse_down - stays PRESS until its next real event.
|
|
183
|
+
import meltygui.core.windowing.wayland_move as wayland_move
|
|
184
|
+
masked = wayland_move.masked_buttons()
|
|
185
|
+
if masked:
|
|
186
|
+
io = imgui.get_io()
|
|
187
|
+
for button in masked:
|
|
188
|
+
if 0 <= button < 3:
|
|
189
|
+
io.mouse_down[button] = False
|
|
190
|
+
# Orchestrator record/replay: inject the next slice of a replaying
|
|
191
|
+
# orchestration into the SAME handler the stock backend feeds - HERE,
|
|
192
|
+
# before imgui.new_frame, where stock input enters too (GLFW
|
|
193
|
+
# callbacks → handler, this poll → io). An injected event therefore
|
|
194
|
+
# lands in the handler and, through stamp_io right below, in imgui's
|
|
195
|
+
# io in the same frame, and process_frame dispatches it this frame at
|
|
196
|
+
# exactly a real press's moment. (It used to pump from
|
|
197
|
+
# Melty.begin_frame, inside the imgui frame: the handler saw a whole
|
|
198
|
+
# empty frame before imgui did, and on a behind press that
|
|
199
|
+
# handler-only frame raised the window and started the drag on a
|
|
200
|
+
# move handle before the widget under the press was active - the
|
|
201
|
+
# window dragged instead of the value, 09-01.) Then, while a replay
|
|
202
|
+
# or directed task drives, the VIRTUAL cursor / buttons / buttons /
|
|
203
|
+
# chars override the real ones for imgui (the handler side is handled
|
|
204
|
+
# through the input_tap funnel). No-ops otherwise.
|
|
205
|
+
from meltygui.core.automation.orchestration_core import Orchestrator
|
|
206
|
+
Orchestrator.pump() # engine errors surface if never swallowed here
|
|
207
|
+
try:
|
|
208
|
+
Orchestrator.stamp_io(imgui.get_io())
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
def _create_device_objects(self):
|
|
213
|
+
"""Build the LCD program; if the driver can't link it (no dual-source
|
|
214
|
+
blending), fall back to the stock shader + stock blending so text
|
|
215
|
+
still renders, just grayscale."""
|
|
216
|
+
self._text_style_source = adjust_text_color()
|
|
217
|
+
self.FRAGMENT_SHADER_SRC = type(self).FRAGMENT_SHADER_SRC.replace(
|
|
218
|
+
"/* dynamic text policy */", self._text_style_source)
|
|
219
|
+
try:
|
|
220
|
+
super()._create_device_objects()
|
|
221
|
+
if not gl.glGetProgramiv(self._shader_handle, gl.GL_LINK_STATUS):
|
|
222
|
+
raise RuntimeError(gl.glGetProgramInfoLog(self._shader_handle))
|
|
223
|
+
self._loc_texel = gl.glGetUniformLocation(self._shader_handle, "TexelSize")
|
|
224
|
+
self._loc_atlas = gl.glGetUniformLocation(self._shader_handle, "Atlas")
|
|
225
|
+
self._loc_lcd = gl.glGetUniformLocation(self._shader_handle, "Lcd")
|
|
226
|
+
self._loc_bgr = gl.glGetUniformLocation(self._shader_handle, "Bgr")
|
|
227
|
+
self._loc_gamma = gl.glGetUniformLocation(self._shader_handle, "Gamma")
|
|
228
|
+
self._loc_dynamic_styles = gl.glGetUniformLocation(self._shader_handle, "DynamicStyles")
|
|
229
|
+
self._loc_style_context = gl.glGetUniformLocation(self._shader_handle, "StyleContext")
|
|
230
|
+
self._loc_text_contrast = gl.glGetUniformLocation(self._shader_handle, "TextContrast")
|
|
231
|
+
self._lcd_ok = min(self._loc_texel, self._loc_atlas, self._loc_lcd,
|
|
232
|
+
self._loc_bgr, self._loc_gamma) >= 0
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f"SplitOverlayRenderer: LCD text shader unavailable ({e}); "
|
|
235
|
+
f"falling back to grayscale text")
|
|
236
|
+
self._lcd_ok = False
|
|
237
|
+
if not self._lcd_ok:
|
|
238
|
+
gl.glDeleteProgram(self._shader_handle)
|
|
239
|
+
gl.glDeleteVertexArrays(1, [self._vao_handle])
|
|
240
|
+
gl.glDeleteBuffers(2, [self._vbo_handle, self._elements_handle])
|
|
241
|
+
self.FRAGMENT_SHADER_SRC = self._STOCK_FRAGMENT_SHADER_SRC
|
|
242
|
+
super()._create_device_objects()
|
|
243
|
+
|
|
244
|
+
def refresh_font_texture(self):
|
|
245
|
+
"""Stock upload, with the atlas routed through FontManager.hint_atlas
|
|
246
|
+
(FreeType light-hinted LCD glyphs) when enabled. Same GL state
|
|
247
|
+
handling as the base: save/restore the bound texture, delete the
|
|
248
|
+
previous atlas texture, clear imgui's CPU copy after upload."""
|
|
249
|
+
last_texture = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
|
|
250
|
+
width, height, pixels = self.io.fonts.get_tex_data_as_rgba32()
|
|
251
|
+
pixels = self._hinted_atlas(width, height, pixels) or pixels
|
|
252
|
+
|
|
253
|
+
if self._font_texture is not None:
|
|
254
|
+
gl.glDeleteTextures([self._font_texture])
|
|
255
|
+
self._font_texture = gl.glGenTextures(1)
|
|
256
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, self._font_texture)
|
|
257
|
+
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
|
258
|
+
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
|
259
|
+
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, width, height, 0,
|
|
260
|
+
gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, pixels)
|
|
261
|
+
self.io.fonts.texture_id = self._font_texture
|
|
262
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, last_texture)
|
|
263
|
+
self.io.fonts.clear_tex_data()
|
|
264
|
+
self._atlas_texel = (1.0 / max(1, width), 1.0 / max(1, height))
|
|
265
|
+
|
|
266
|
+
def _hinted_atlas(self, width, height, pixels):
|
|
267
|
+
"""FreeType-hinted texels for the atlas imgui just built, or None to
|
|
268
|
+
upload stb's. Only a FontManager with live handles (it baked the
|
|
269
|
+
current atlas) may probe it. NB: pyimgui returns a fresh wrapper
|
|
270
|
+
from every get_io(), so io identity can't be compared."""
|
|
271
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
272
|
+
if not Toggles.Fonts.freetype_hinting:
|
|
273
|
+
return None
|
|
274
|
+
from meltygui.core.melty import Melty
|
|
275
|
+
fm = getattr(Melty, "font_mgr", None)
|
|
276
|
+
if fm is None or not fm._handles:
|
|
277
|
+
return None
|
|
278
|
+
try:
|
|
279
|
+
return fm.hint_atlas(width, height, pixels)
|
|
280
|
+
except Exception as e: # never let the atlas upload fail over hinting
|
|
281
|
+
print(f"SplitOverlayRenderer: FreeType hinting pass failed ({e!r}); "
|
|
282
|
+
f"uploading stb atlas")
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def _bind_text_mode(self) -> int:
|
|
286
|
+
"""Blend func + per-frame LCD uniforms for the bound program. Returns
|
|
287
|
+
the font texture id the per-command Atlas uniform compares against,
|
|
288
|
+
or -1 when running the stock shader."""
|
|
289
|
+
# Alpha channel: ONE / ONE_MINUS_SRC_ALPHA (coverage union), never
|
|
290
|
+
# SRC_ALPHA / ONE_MINUS_SRC_ALPHA - that gave dst_a = a² + dst_a-(1-a),
|
|
291
|
+
# so every translucent draw (a texture with alpha, an AA edge, glyph
|
|
292
|
+
# coverage) punched alpha OUT of an opaque framebuffer and, on an
|
|
293
|
+
# ARGB buffer (the frame's Wayland surface, an X11 compositor
|
|
294
|
+
# handing out an ARGB buffer), the desktop showed through. Tiles
|
|
295
|
+
# rendered offscreen start from alpha 0, but with the union rule
|
|
296
|
+
# their alpha is the true coverage, so a blitted alpha lands solid.
|
|
297
|
+
set_decode_uniforms(self._shader_handle) # Toggles.HLS curve, live
|
|
298
|
+
if not self._lcd_ok:
|
|
299
|
+
gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
|
300
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
301
|
+
return -1
|
|
302
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
303
|
+
gl.glBlendFuncSeparate(gl.GL_SRC1_COLOR, gl.GL_ONE_MINUS_SRC1_COLOR,
|
|
304
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
305
|
+
# The shared atlas can be resized by another surface's lazy font load.
|
|
306
|
+
# Its live dimensions, just like its live texture ID, are authoritative:
|
|
307
|
+
# stale texel widths move the LCD taps off their subpixel centres.
|
|
308
|
+
self._atlas_texel = (1.0 / max(1, self.io.fonts.texture_width),
|
|
309
|
+
1.0 / max(1, self.io.fonts.texture_height))
|
|
310
|
+
gl.glUniform2f(self._loc_texel, *self._atlas_texel)
|
|
311
|
+
gl.glUniform1i(self._loc_dynamic_styles, int(Toggles.dynamic_styles))
|
|
312
|
+
gl.glUniform1i(self._loc_style_context, 1)
|
|
313
|
+
gl.glUniform1f(self._loc_text_contrast, Toggles.dynamic_text_contrast)
|
|
314
|
+
gl.glUniform1i(self._loc_lcd, 1 if Toggles.Fonts.lcd_subpixel else 0)
|
|
315
|
+
gl.glUniform1i(self._loc_bgr, 1 if Toggles.Fonts.lcd_bgr else 0)
|
|
316
|
+
gamma = float(Toggles.Fonts.text_gamma)
|
|
317
|
+
gl.glUniform1f(self._loc_gamma, gamma if gamma > 0.0 else 1.0)
|
|
318
|
+
# Surfaces share an atlas: any renderer can replace its texture.
|
|
319
|
+
# Commands use the atlas's live ID, not this renderer's last upload.
|
|
320
|
+
texture = self.io.fonts.texture_id
|
|
321
|
+
return int(texture) if texture is not None else -1
|
|
322
|
+
|
|
323
|
+
def begin_frame_split(self) -> None:
|
|
324
|
+
"""Snapshot whether the foreground list has content. Call after the UI
|
|
325
|
+
is fully built and before ``imgui.render()``."""
|
|
326
|
+
self._has_overlay = imgui.get_foreground_draw_list().vtx_buffer_size > 0
|
|
327
|
+
self._scaled_this_frame = False
|
|
328
|
+
|
|
329
|
+
def render_except_overlay(self, draw_data) -> None:
|
|
330
|
+
self._scale_clip_rects_once(draw_data)
|
|
331
|
+
lists = draw_data.commands_lists
|
|
332
|
+
if self._has_overlay and lists:
|
|
333
|
+
lists = lists[:-1]
|
|
334
|
+
self._render_command_lists(draw_data, lists)
|
|
335
|
+
|
|
336
|
+
def render_overlay_only(self, draw_data) -> None:
|
|
337
|
+
if not self._has_overlay:
|
|
338
|
+
return
|
|
339
|
+
self._scale_clip_rects_once(draw_data)
|
|
340
|
+
lists = draw_data.commands_lists
|
|
341
|
+
if not lists:
|
|
342
|
+
self._has_overlay = False
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# If Melty channel-split the foreground list, render each channel
|
|
346
|
+
# with a per-layer stencil mask. Otherwise fall back to a regular draw.
|
|
347
|
+
from meltygui.core.melty import Melty
|
|
348
|
+
ranges = getattr(Melty, "_overlay_channel_ranges", None)
|
|
349
|
+
if ranges:
|
|
350
|
+
self._render_overlay_channels(draw_data, lists[-1], ranges, Melty)
|
|
351
|
+
else:
|
|
352
|
+
self._render_command_lists(draw_data, lists[-1:])
|
|
353
|
+
self._has_overlay = False
|
|
354
|
+
|
|
355
|
+
# Pointer slide correction: (surface pointer, relative-motion total) at
|
|
356
|
+
# mouse press, None if held.
|
|
357
|
+
_slide_base = None
|
|
358
|
+
# Last frame's (surface pointer, relative-motion total), and the per-axis
|
|
359
|
+
# relative motion the cursor did NOT follow (clamped at the screen
|
|
360
|
+
# edge) since the press - excluded from the slide.
|
|
361
|
+
_slide_last = None
|
|
362
|
+
_slide_clamped = (0.0, 0.0)
|
|
363
|
+
_slide_screen_origin = None
|
|
364
|
+
_slide_native_sample = None
|
|
365
|
+
SLIDE_DEADBAND = 1.5
|
|
366
|
+
|
|
367
|
+
def _cancel_surface_slide(self, io):
|
|
368
|
+
"""While a mouse button is held, subtract the SURFACE's own motion
|
|
369
|
+
from the pointer. The compositor slides the studio to keep it on
|
|
370
|
+
screen when its surface grows past the workarea (the OS-edge push,
|
|
371
|
+
os_frame); the surface-relative pointer then jumps by the slide
|
|
372
|
+
while the hand did not move — a drag reading that as motion grows
|
|
373
|
+
the window more and is slid again, a feedback snap. The slide is
|
|
374
|
+
the difference between the surface-relative pointer's travel and
|
|
375
|
+
the relative-pointer (screen-space) travel since the press; below
|
|
376
|
+
SLIDE_DEADBAND it is rounding, not a slide. Resets on release, so
|
|
377
|
+
hover after a gesture reads the true pointer again."""
|
|
378
|
+
import meltygui.core.windowing.wayland_move as wayland_move
|
|
379
|
+
down = any(io.mouse_down[i] for i in range(3))
|
|
380
|
+
if not down or not wayland_move.relative_motion_available():
|
|
381
|
+
self._slide_base = None
|
|
382
|
+
self._slide_last = None
|
|
383
|
+
self._slide_clamped = (0.0, 0.0)
|
|
384
|
+
if not down:
|
|
385
|
+
self._slide_screen_origin = None
|
|
386
|
+
self._slide_native_sample = None
|
|
387
|
+
return
|
|
388
|
+
# Native seats deliver only surface-local motion. The geometry feed
|
|
389
|
+
# still tells us how far the surface moved: subtract that change
|
|
390
|
+
# from the drag, without treating a stationary relative pointer as a
|
|
391
|
+
# stationary hand. Keep this baseline local to the window/gesture.
|
|
392
|
+
window = getattr(self, 'window', None)
|
|
393
|
+
generation = (window.cursor_motion_generation
|
|
394
|
+
if glfw.is_native_window(window) else None)
|
|
395
|
+
sample = self._slide_native_sample
|
|
396
|
+
if generation is not None and sample is not None and sample[0] == generation:
|
|
397
|
+
# Native Wayland retains the last surface-local sample until
|
|
398
|
+
# wl_pointer.motion. A configure/feed update alone must not
|
|
399
|
+
# combine that OLD sample with a NEW surface origin: doing so
|
|
400
|
+
# turns the window's own translation into another phantom slide.
|
|
401
|
+
io.mouse_pos = sample[1]
|
|
402
|
+
return
|
|
403
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
404
|
+
rect = geometry_feed.frame_rect()
|
|
405
|
+
if rect is None:
|
|
406
|
+
self._slide_screen_origin = None
|
|
407
|
+
return
|
|
408
|
+
origin = rect[:2]
|
|
409
|
+
if self._slide_screen_origin is None:
|
|
410
|
+
self._slide_screen_origin = origin
|
|
411
|
+
mx, my = io.mouse_pos
|
|
412
|
+
if mx > -1e6 and my > -1e6:
|
|
413
|
+
ox, oy = self._slide_screen_origin
|
|
414
|
+
io.mouse_pos = (mx + origin[0] - ox, my + origin[1] - oy)
|
|
415
|
+
if generation is not None:
|
|
416
|
+
self._slide_native_sample = (generation, tuple(io.mouse_pos))
|
|
417
|
+
return
|
|
418
|
+
self._slide_screen_origin = None
|
|
419
|
+
self._slide_native_sample = None
|
|
420
|
+
mx, my = io.mouse_pos
|
|
421
|
+
if mx < -1e6 or my < -1e6:
|
|
422
|
+
return
|
|
423
|
+
rel = wayland_move.relative_motion_total()
|
|
424
|
+
if self._slide_base is None:
|
|
425
|
+
self._slide_base = ((mx, my), rel)
|
|
426
|
+
self._slide_last = ((mx, my), rel)
|
|
427
|
+
self._slide_clamped = (0.0, 0.0)
|
|
428
|
+
return
|
|
429
|
+
# The cursor CLAMPED at the screen edge: the hand (relative pointer)
|
|
430
|
+
# keeps moving on an axis while the surface pointer does not move
|
|
431
|
+
# at all on it. That is not the surface sliding — read as one it
|
|
432
|
+
# re-based every root window by the phantom slide each frame (the
|
|
433
|
+
# meltygui windows "slided closed" once the studio filled the
|
|
434
|
+
# display) and pushed the pointer past the edge, so the drag kept
|
|
435
|
+
# going. Excluded from the slide, permanently: the cursor does
|
|
436
|
+
# not owe that motion back.
|
|
437
|
+
if self._slide_last is None: # hotswapped mid-gesture: start the frame diff here
|
|
438
|
+
self._slide_last = ((mx, my), rel)
|
|
439
|
+
(lx, ly), (lrx, lry) = self._slide_last
|
|
440
|
+
d_rel = (rel[0] - lrx, rel[1] - lry)
|
|
441
|
+
cx, cy = self._slide_clamped
|
|
442
|
+
if d_rel[0] and mx == lx:
|
|
443
|
+
cx += d_rel[0]
|
|
444
|
+
if d_rel[1] and my == ly:
|
|
445
|
+
cy += d_rel[1]
|
|
446
|
+
self._slide_clamped = (cx, cy)
|
|
447
|
+
self._slide_last = ((mx, my), rel)
|
|
448
|
+
(bx, by), (rx0, ry0) = self._slide_base
|
|
449
|
+
slide_x = (mx - bx) - (rel[0] - rx0 - cx)
|
|
450
|
+
slide_y = (my - by) - (rel[1] - ry0 - cy)
|
|
451
|
+
if abs(slide_x) < self.SLIDE_DEADBAND:
|
|
452
|
+
slide_x = 0.0
|
|
453
|
+
if abs(slide_y) < self.SLIDE_DEADBAND:
|
|
454
|
+
slide_y = 0.0
|
|
455
|
+
if slide_x or slide_y:
|
|
456
|
+
io.mouse_pos = (mx - slide_x, my - slide_y)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _frame_origin():
|
|
460
|
+
"""Framebuffer origin of imgui's display: (inset, inset) while the
|
|
461
|
+
frameless window carries its shadow margin, else (0, 0). Viewports
|
|
462
|
+
move by it, scissors add it."""
|
|
463
|
+
from meltygui.core.melty import Melty
|
|
464
|
+
ox, oy = getattr(Melty, "frame_origin", None) or (0, 0)
|
|
465
|
+
return int(ox), int(oy)
|
|
466
|
+
|
|
467
|
+
def _render_overlay_channels(self, draw_data, overlay_list, ranges, Melty) -> None:
|
|
468
|
+
"""Render the foreground draw list one channel at a time. Channel index
|
|
469
|
+
equals layer_channel(draw_state.layer); the top channel is the unmasked
|
|
470
|
+
global overlay. Each channel renders with stencil set to exclude any
|
|
471
|
+
window whose layer_channel is greater than this channel — so the overlay
|
|
472
|
+
appears 'underneath' higher windows."""
|
|
473
|
+
io = self.io
|
|
474
|
+
display_width, display_height = io.display_size
|
|
475
|
+
fb_scale_x, fb_scale_y = io.display_fb_scale
|
|
476
|
+
fb_width = int(display_width * fb_scale_x)
|
|
477
|
+
fb_height = int(display_height * fb_scale_y)
|
|
478
|
+
|
|
479
|
+
if fb_width == 0 or fb_height == 0:
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
self._refresh_style_shader()
|
|
483
|
+
common_gl_state_tuple = get_common_gl_state()
|
|
484
|
+
last_program = gl.glGetIntegerv(gl.GL_CURRENT_PROGRAM)
|
|
485
|
+
last_active_texture = gl.glGetIntegerv(gl.GL_ACTIVE_TEXTURE)
|
|
486
|
+
last_array_buffer = gl.glGetIntegerv(gl.GL_ARRAY_BUFFER_BINDING)
|
|
487
|
+
last_element_array_buffer = gl.glGetIntegerv(gl.GL_ELEMENT_ARRAY_BUFFER_BINDING)
|
|
488
|
+
last_vertex_array = gl.glGetIntegerv(gl.GL_VERTEX_ARRAY_BINDING)
|
|
489
|
+
last_stencil_test = gl.glIsEnabled(gl.GL_STENCIL_TEST)
|
|
490
|
+
|
|
491
|
+
gl.glEnable(gl.GL_BLEND)
|
|
492
|
+
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
|
493
|
+
gl.glDisable(gl.GL_CULL_FACE)
|
|
494
|
+
gl.glDisable(gl.GL_DEPTH_TEST)
|
|
495
|
+
gl.glEnable(gl.GL_SCISSOR_TEST)
|
|
496
|
+
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
497
|
+
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
|
|
498
|
+
ox, oy = self._frame_origin()
|
|
499
|
+
gl.glViewport(ox, oy, fb_width, fb_height)
|
|
500
|
+
|
|
501
|
+
ortho_projection = (ctypes.c_float * 16)(
|
|
502
|
+
2.0 / display_width, 0.0, 0.0, 0.0,
|
|
503
|
+
0.0, 2.0 / -display_height, 0.0, 0.0,
|
|
504
|
+
0.0, 0.0, -1.0, 0.0,
|
|
505
|
+
-1.0, 1.0, 0.0, 1.0,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
509
|
+
style_context = 0
|
|
510
|
+
if self._lcd_ok and Toggles.dynamic_styles:
|
|
511
|
+
style_context = self._ensure_style_context(fb_width, fb_height, fb_scale_x, fb_scale_y, ox, oy)
|
|
512
|
+
previous_context = self._bind_style_context(style_context)
|
|
513
|
+
gl.glUseProgram(self._shader_handle)
|
|
514
|
+
gl.glUniform1i(self._attrib_location_tex, 0)
|
|
515
|
+
gl.glUniformMatrix4fv(self._attrib_proj_mtx, 1, gl.GL_FALSE, ortho_projection)
|
|
516
|
+
font_tex = self._bind_text_mode()
|
|
517
|
+
gl.glBindVertexArray(self._vao_handle)
|
|
518
|
+
|
|
519
|
+
# Upload the merged foreground vtx/idx buffers once for all channels.
|
|
520
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._vbo_handle)
|
|
521
|
+
gl.glBufferData(
|
|
522
|
+
gl.GL_ARRAY_BUFFER,
|
|
523
|
+
overlay_list.vtx_buffer_size * imgui.VERTEX_SIZE,
|
|
524
|
+
ctypes.c_void_p(overlay_list.vtx_buffer_data),
|
|
525
|
+
gl.GL_STREAM_DRAW,
|
|
526
|
+
)
|
|
527
|
+
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._elements_handle)
|
|
528
|
+
gl.glBufferData(
|
|
529
|
+
gl.GL_ELEMENT_ARRAY_BUFFER,
|
|
530
|
+
overlay_list.idx_buffer_size * imgui.INDEX_SIZE,
|
|
531
|
+
ctypes.c_void_p(overlay_list.idx_buffer_data),
|
|
532
|
+
gl.GL_STREAM_DRAW,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
gltype = gl.GL_UNSIGNED_SHORT if imgui.INDEX_SIZE == 2 else gl.GL_UNSIGNED_INT
|
|
536
|
+
|
|
537
|
+
# Each command's (idx_lo, idx_hi) span in the merged index buffer. The
|
|
538
|
+
# channel ranges are also in index space, so we render the intersection
|
|
539
|
+
# of each command with each channel - splitting fused commands at
|
|
540
|
+
# channel boundaries (a command's texture/clip are uniform over its span,
|
|
541
|
+
# so any sub-range is valid to draw).
|
|
542
|
+
commands = list(overlay_list.commands)
|
|
543
|
+
cmd_spans = []
|
|
544
|
+
running = 0
|
|
545
|
+
for cmd in commands:
|
|
546
|
+
cmd_spans.append((running, running + cmd.elem_count, cmd))
|
|
547
|
+
running += cmd.elem_count
|
|
548
|
+
|
|
549
|
+
# (channel, draw_state) for every registered window with a laid-out
|
|
550
|
+
# rect - used to punch holes for windows below each channel. Channels
|
|
551
|
+
# go through Melty.overlay_window_channel - the SAME dense rank map the
|
|
552
|
+
# views routed their overlays with - so the "higher than" comparison
|
|
553
|
+
# below stays exact instead of collapsing at the max_layer clamp.
|
|
554
|
+
top_channel = Melty.max_layer - 1 # global overlay is above all windows
|
|
555
|
+
window_channels = []
|
|
556
|
+
for w in Melty.registered_windows.values():
|
|
557
|
+
ds = getattr(w, "draw_state", None)
|
|
558
|
+
if ds is None or ds.closed:
|
|
559
|
+
continue
|
|
560
|
+
if ds.abs_left is None or ds.abs_top is None or ds.width is None or ds.height is None:
|
|
561
|
+
continue
|
|
562
|
+
window_channels.append((Melty.overlay_channel_for(ds), ds))
|
|
563
|
+
|
|
564
|
+
for r in Melty.root_draw_states.values():
|
|
565
|
+
for ds in r:
|
|
566
|
+
if ds is None or ds.closed:
|
|
567
|
+
continue
|
|
568
|
+
# A window hidden because its spawner scrolled out of view
|
|
569
|
+
# isn't dispatched (no pixels this frame), so its stale rect
|
|
570
|
+
# does not punch stencil holes in lower channels.
|
|
571
|
+
if getattr(ds, "_hidden_offscreen", False):
|
|
572
|
+
continue
|
|
573
|
+
if ds.abs_left is None or ds.abs_top is None or ds.width is None or ds.height is None:
|
|
574
|
+
continue
|
|
575
|
+
window_channels.append((Melty.overlay_channel_for(ds), ds))
|
|
576
|
+
|
|
577
|
+
if self.debug_static_mask:
|
|
578
|
+
sx, sy, sw, sh = self.debug_static_rect
|
|
579
|
+
# Inject at a non-TOP channel so it can mask the controlled debug
|
|
580
|
+
# content drawn on channel 10 (see Melty.end_frame() rect).
|
|
581
|
+
fake_channel = 20
|
|
582
|
+
fake = _FakeWindowRect(fake_channel, sx, sy, sw, sh)
|
|
583
|
+
window_channels.append((fake_channel, fake))
|
|
584
|
+
|
|
585
|
+
if self.debug_overlay_mask and not self._mask_debug_logged:
|
|
586
|
+
self._mask_debug_logged = False
|
|
587
|
+
fbo = gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING)
|
|
588
|
+
# GL_STENCIL_BITS is removed in coreGL; query the bound
|
|
589
|
+
# framebuffer's stencil attachment size instead.
|
|
590
|
+
attachment = gl.GL_STENCIL if fbo == 0 else gl.GL_STENCIL_ATTACHMENT
|
|
591
|
+
try:
|
|
592
|
+
stencil_bits = gl.glGetFramebufferAttachmentParameteriv(
|
|
593
|
+
gl.GL_DRAW_FRAMEBUFFER, attachment,
|
|
594
|
+
gl.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
stencil_bits = f"query-failed({e})"
|
|
597
|
+
non_empty = [(c, s, e) for (c, s, e) in ranges if e > s]
|
|
598
|
+
|
|
599
|
+
for channel_idx, idx_lo, idx_hi in ranges:
|
|
600
|
+
if idx_hi <= idx_lo:
|
|
601
|
+
continue
|
|
602
|
+
self._setup_channel_stencil(channel_idx, top_channel, window_channels,
|
|
603
|
+
fb_height, fb_scale_x, fb_scale_y, ox, oy)
|
|
604
|
+
|
|
605
|
+
for c_lo, c_hi, cmd in cmd_spans:
|
|
606
|
+
seg_lo = max(idx_lo, c_lo)
|
|
607
|
+
seg_hi = min(idx_hi, c_hi)
|
|
608
|
+
if seg_hi <= seg_lo:
|
|
609
|
+
continue
|
|
610
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, cmd.texture_id)
|
|
611
|
+
if font_tex >= 0:
|
|
612
|
+
gl.glUniform1i(self._loc_atlas, 1 if int(cmd.texture_id) == font_tex else 0)
|
|
613
|
+
x, y, z, w = cmd.clip_rect
|
|
614
|
+
gl.glScissor(int(x) + ox, int(fb_height - w) + oy, int(z - x), int(w - y))
|
|
615
|
+
self._draw_style_elements(overlay_list, cmd.texture_id, seg_lo, seg_hi, gltype)
|
|
616
|
+
|
|
617
|
+
if self.debug_overlay_mask:
|
|
618
|
+
self._debug_draw_window_rects(window_channels, fb_height,
|
|
619
|
+
fb_scale_x, fb_scale_y)
|
|
620
|
+
|
|
621
|
+
if last_stencil_test:
|
|
622
|
+
gl.glEnable(gl.GL_STENCIL_TEST)
|
|
623
|
+
else:
|
|
624
|
+
gl.glDisable(gl.GL_STENCIL_TEST)
|
|
625
|
+
|
|
626
|
+
self._unbind_style_context(previous_context)
|
|
627
|
+
restore_common_gl_state(common_gl_state_tuple)
|
|
628
|
+
gl.glUseProgram(last_program)
|
|
629
|
+
gl.glActiveTexture(last_active_texture)
|
|
630
|
+
gl.glBindVertexArray(last_vertex_array)
|
|
631
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, last_array_buffer)
|
|
632
|
+
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer)
|
|
633
|
+
|
|
634
|
+
def _debug_draw_window_rects(self, window_channels, fb_height,
|
|
635
|
+
fb_scale_x, fb_scale_y) -> None:
|
|
636
|
+
"""Visual debug: draw a bright border around each window rect using the
|
|
637
|
+
EXACT transform _setup_channel_stencil uses to punch stencil holes. If
|
|
638
|
+
these borders line up with the on-screen windows, the stencil holes are
|
|
639
|
+
positioned correctly. Magenta border; thickness scales with channel."""
|
|
640
|
+
gl.glDisable(gl.GL_STENCIL_TEST)
|
|
641
|
+
gl.glEnable(gl.GL_SCISSOR_TEST)
|
|
642
|
+
t = 2
|
|
643
|
+
for win_channel, ds in window_channels:
|
|
644
|
+
x = int(ds.abs_left * fb_scale_x)
|
|
645
|
+
y = int(fb_height - (ds.abs_top + ds.height) * fb_scale_y)
|
|
646
|
+
sw = int(ds.width * fb_scale_x)
|
|
647
|
+
sh = int(ds.height * fb_scale_y)
|
|
648
|
+
if sw <= 0 or sh <= 0:
|
|
649
|
+
continue
|
|
650
|
+
# Color-code by channel so overlapping windows are distinguishable.
|
|
651
|
+
r = 0.2 + 0.8 * ((win_channel * 37) % 100) / 100.0
|
|
652
|
+
g = 0.2 + 0.8 * ((win_channel * 71) % 100) / 100.0
|
|
653
|
+
b = 0.2 + 0.8 * ((win_channel * 13) % 100) / 100.0
|
|
654
|
+
gl.glClearColor(r, g, b, 1.0)
|
|
655
|
+
for bx, by, bw, bh in (
|
|
656
|
+
(x, y + sh - t, sw, t), # top
|
|
657
|
+
(x, y, sw, t), # bottom
|
|
658
|
+
(x, y, t, sh), # left
|
|
659
|
+
(x + sw - t, y, t, sh), # right
|
|
660
|
+
):
|
|
661
|
+
gl.glScissor(bx, by, bw, bh)
|
|
662
|
+
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
663
|
+
|
|
664
|
+
def _setup_channel_stencil(self, channel_idx, top_channel, window_channels,
|
|
665
|
+
fb_height, fb_scale_x, fb_scale_y, ox=0, oy=0) -> None:
|
|
666
|
+
"""Top channel: no stencil (global overlay, never masked). Otherwise
|
|
667
|
+
stencil=1 everywhere, then punched to 0 inside the rect of every window
|
|
668
|
+
whose layer_channel is greater than this channel. The stencil test then
|
|
669
|
+
accepts only non-zero pixels, hiding the overlay where a higher window
|
|
670
|
+
covers it."""
|
|
671
|
+
higher = [(c, ds) for (c, ds) in window_channels if c > channel_idx]
|
|
672
|
+
if channel_idx >= top_channel or not higher:
|
|
673
|
+
gl.glDisable(gl.GL_STENCIL_TEST)
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
gl.glEnable(gl.GL_STENCIL_TEST)
|
|
677
|
+
gl.glStencilMask(0xFF)
|
|
678
|
+
|
|
679
|
+
gl.glDisable(gl.GL_SCISSOR_TEST)
|
|
680
|
+
gl.glClearStencil(1)
|
|
681
|
+
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
|
682
|
+
|
|
683
|
+
gl.glEnable(gl.GL_SCISSOR_TEST)
|
|
684
|
+
gl.glClearStencil(0)
|
|
685
|
+
for win_channel, ds in higher:
|
|
686
|
+
x = int(ds.abs_left * fb_scale_x) + ox
|
|
687
|
+
y = int(fb_height - (ds.abs_top + ds.height) * fb_scale_y) + oy
|
|
688
|
+
sw = int(ds.width * fb_scale_x)
|
|
689
|
+
sh = int(ds.height * fb_scale_y)
|
|
690
|
+
if sw <= 0 or sh <= 0:
|
|
691
|
+
continue
|
|
692
|
+
gl.glScissor(x, y, sw, sh)
|
|
693
|
+
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
|
694
|
+
|
|
695
|
+
gl.glStencilFunc(gl.GL_NOTEQUAL, 0, 0xFF)
|
|
696
|
+
gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, gl.GL_KEEP)
|
|
697
|
+
|
|
698
|
+
def render(self, draw_data) -> None:
|
|
699
|
+
"""Drop-in replacement matching the upstream signature; renders all
|
|
700
|
+
lists in a single pass (no split)."""
|
|
701
|
+
self._scale_clip_rects_once(draw_data)
|
|
702
|
+
self._render_command_lists(draw_data, draw_data.commands_lists)
|
|
703
|
+
self._has_overlay = False
|
|
704
|
+
|
|
705
|
+
def _scale_clip_rects_once(self, draw_data) -> None:
|
|
706
|
+
if self._scaled_this_frame:
|
|
707
|
+
return
|
|
708
|
+
io = self.io
|
|
709
|
+
draw_data.scale_clip_rects(*io.display_fb_scale)
|
|
710
|
+
self._scaled_this_frame = True
|
|
711
|
+
|
|
712
|
+
def _refresh_style_shader(self):
|
|
713
|
+
"""A hotswapped text policy replaces the program, retaining the atlas."""
|
|
714
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
715
|
+
initialized = hasattr(self, '_text_style_source')
|
|
716
|
+
if not self._lcd_ok or (initialized and not Toggles.dynamic_styles):
|
|
717
|
+
return
|
|
718
|
+
source = adjust_text_color()
|
|
719
|
+
if (initialized and source == self._text_style_source
|
|
720
|
+
or source == getattr(self, "_failed_text_style_source", None)):
|
|
721
|
+
return
|
|
722
|
+
previous = self.__dict__.copy()
|
|
723
|
+
self._create_device_objects()
|
|
724
|
+
if self._lcd_ok:
|
|
725
|
+
retired = previous
|
|
726
|
+
self._failed_text_style_source = None
|
|
727
|
+
else:
|
|
728
|
+
retired = self.__dict__.copy()
|
|
729
|
+
self.__dict__.update(previous)
|
|
730
|
+
# Keep the last good program, retry only after another source edit.
|
|
731
|
+
self._failed_text_style_source = source
|
|
732
|
+
gl.glDeleteProgram(retired['_shader_handle'])
|
|
733
|
+
gl.glDeleteVertexArrays(1, [retired['_vao_handle']])
|
|
734
|
+
gl.glDeleteBuffers(2, [retired['_vbo_handle'], retired['_elements_handle']])
|
|
735
|
+
|
|
736
|
+
def shutdown(self):
|
|
737
|
+
if hasattr(self, '_style_gl'):
|
|
738
|
+
self._style_gl.release()
|
|
739
|
+
super().shutdown()
|
|
740
|
+
|
|
741
|
+
_STYLE_CONTEXT_VERT = """
|
|
742
|
+
#version 330 core
|
|
743
|
+
layout(location = 0) in vec4 rect; // framebuffer px: x0, y0, x1, y1
|
|
744
|
+
layout(location = 1) in vec4 clip; // framebuffer px: x0, y0, x1, y1
|
|
745
|
+
layout(location = 2) in vec2 extra; // corner radius px, palette index
|
|
746
|
+
uniform vec2 size;
|
|
747
|
+
out vec4 v_rect; out vec4 v_clip; out vec2 v_extra;
|
|
748
|
+
const vec2 corners[6] = vec2[6](vec2(0, 0), vec2(1, 0), vec2(0, 1),
|
|
749
|
+
vec2(1, 0), vec2(1, 1), vec2(0, 1));
|
|
750
|
+
void main() {
|
|
751
|
+
vec2 p = mix(rect.xy, rect.zw, corners[gl_VertexID]);
|
|
752
|
+
gl_Position = vec4(p / size * 2.0 - 1.0, 0.0, 1.0);
|
|
753
|
+
v_rect = rect; v_clip = clip; v_extra = extra;
|
|
754
|
+
}
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
_STYLE_CONTEXT_FRAG = """
|
|
758
|
+
#version 330 core
|
|
759
|
+
in vec4 v_rect; in vec4 v_clip; in vec2 v_extra;
|
|
760
|
+
uniform sampler2D palette;
|
|
761
|
+
out vec4 frag;
|
|
762
|
+
void main() {
|
|
763
|
+
vec2 p = gl_FragCoord.xy;
|
|
764
|
+
if (p.x < v_clip.x || p.y < v_clip.y || p.x > v_clip.z || p.y > v_clip.w) discard;
|
|
765
|
+
vec2 half_size = (v_rect.zw - v_rect.xy) * 0.5;
|
|
766
|
+
float r = min(v_extra.x, min(half_size.x, half_size.y));
|
|
767
|
+
vec2 q = abs(p - (v_rect.xy + v_rect.zw) * 0.5) - (half_size - vec2(r));
|
|
768
|
+
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
|
769
|
+
float coverage = clamp(0.5 - d, 0.0, 1.0);
|
|
770
|
+
if (coverage <= 0.0) discard;
|
|
771
|
+
frag = vec4(texelFetch(palette, ivec2(int(v_extra.y + 0.5), 0), 0).rgb, coverage);
|
|
772
|
+
}
|
|
773
|
+
"""
|
|
774
|
+
|
|
775
|
+
def _ensure_style_context(self, fb_width, fb_height, fb_scale_x, fb_scale_y, ox, oy):
|
|
776
|
+
"""The text-context layer: every dynamic background this frame,
|
|
777
|
+
painted from the resolved palette (Melty.draw_backgrounds) in
|
|
778
|
+
submission order over the root colour, rounded and clipped as it
|
|
779
|
+
was submitted. Built ONCE per frame (Melty.background_gen); the
|
|
780
|
+
text shader reads it at gl_FragCoord, so it is sized like the
|
|
781
|
+
framebuffer including the viewport origin. Replaces the per-glyph-run
|
|
782
|
+
framebuffer copy (5 GL queries + glCopyTexSubImage2D per line of
|
|
783
|
+
text: 20 ms on a LoRA tree, 09-13). Returns the texture id, 0 when
|
|
784
|
+
there is nothing to read."""
|
|
785
|
+
import numpy as np
|
|
786
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
787
|
+
from meltygui.core.graphics.gl_state import gl_limits
|
|
788
|
+
from meltygui.core.melty import Melty
|
|
789
|
+
gen = Melty.background_gen
|
|
790
|
+
if Melty.dynamic_style_gl is None or not Melty.backgrounds:
|
|
791
|
+
self._style_context_gen = gen
|
|
792
|
+
return 0
|
|
793
|
+
if not hasattr(self, '_style_gl'):
|
|
794
|
+
self._style_gl = GLState()
|
|
795
|
+
width, height = int(ox + fb_width), int(oy + fb_height)
|
|
796
|
+
context = self._style_gl.fbo('text_context', width, height)
|
|
797
|
+
if getattr(self, '_style_context_gen', None) == gen and \
|
|
798
|
+
getattr(self, '_style_context_size', None) == (width, height):
|
|
799
|
+
return context.texture_id
|
|
800
|
+
self._style_context_gen, self._style_context_size = gen, (width, height)
|
|
801
|
+
|
|
802
|
+
# Instance rows, framebuffer coordinates (y up, viewport origin in).
|
|
803
|
+
rows = []
|
|
804
|
+
for index, entry in enumerate(Melty.background_rects):
|
|
805
|
+
if entry is None:
|
|
806
|
+
continue
|
|
807
|
+
left, top, w, h, (cl, ct, cr, cb), radius = entry
|
|
808
|
+
rows.append((left * fb_scale_x + ox, fb_height - (top + h) * fb_scale_y + oy,
|
|
809
|
+
(left + w) * fb_scale_x + ox, fb_height - top * fb_scale_y + oy,
|
|
810
|
+
cl * fb_scale_x + ox, fb_height - cb * fb_scale_y + oy,
|
|
811
|
+
cr * fb_scale_x + ox, fb_height - ct * fb_scale_y + oy,
|
|
812
|
+
radius * fb_scale_x, float(index)))
|
|
813
|
+
instances = np.asarray(rows, dtype=np.float32).reshape(-1, 10)
|
|
814
|
+
|
|
815
|
+
def build_program():
|
|
816
|
+
from OpenGL.GL import shaders
|
|
817
|
+
return shaders.compileProgram(
|
|
818
|
+
shaders.compileShader(self._STYLE_CONTEXT_VERT, gl.GL_VERTEX_SHADER),
|
|
819
|
+
shaders.compileShader(self._STYLE_CONTEXT_FRAG, gl.GL_FRAGMENT_SHADER))
|
|
820
|
+
program = self._style_gl.get('style_context_program', build_program, gl.glDeleteProgram)
|
|
821
|
+
|
|
822
|
+
def build_vao():
|
|
823
|
+
vbo = int(gl.glGenBuffers(1))
|
|
824
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
|
|
825
|
+
stride = 10 * 4
|
|
826
|
+
for location, count, offset in ((0, 4, 0), (1, 4, 16), (2, 2, 32)):
|
|
827
|
+
gl.glEnableVertexAttribArray(location)
|
|
828
|
+
gl.glVertexAttribPointer(location, count, gl.GL_FLOAT, gl.GL_FALSE, stride,
|
|
829
|
+
ctypes.c_void_p(offset))
|
|
830
|
+
gl.glVertexAttribDivisor(location, 1)
|
|
831
|
+
self._style_context_vbo = vbo
|
|
832
|
+
return (vbo,)
|
|
833
|
+
vao = self._style_gl.vao('style_context_vao', build_vao)
|
|
834
|
+
palette = Melty.dynamic_style_gl.fbo('background_palette', gl_limits()['max_2d'], 1)
|
|
835
|
+
|
|
836
|
+
last_program = gl.glGetIntegerv(gl.GL_CURRENT_PROGRAM)
|
|
837
|
+
last_vao = gl.glGetIntegerv(gl.GL_VERTEX_ARRAY_BINDING)
|
|
838
|
+
last_array_buffer = gl.glGetIntegerv(gl.GL_ARRAY_BUFFER_BINDING)
|
|
839
|
+
last_texture = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
|
|
840
|
+
last_scissor = gl.glIsEnabled(gl.GL_SCISSOR_TEST)
|
|
841
|
+
last_blend = gl.glIsEnabled(gl.GL_BLEND)
|
|
842
|
+
with context: # binds the FBO + viewport, restores both on exit
|
|
843
|
+
gl.glDisable(gl.GL_SCISSOR_TEST)
|
|
844
|
+
gl.glClearColor(*Melty.style_context_root)
|
|
845
|
+
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
|
846
|
+
if len(instances):
|
|
847
|
+
gl.glEnable(gl.GL_BLEND)
|
|
848
|
+
gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
|
849
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
850
|
+
gl.glUseProgram(program)
|
|
851
|
+
gl.glUniform2f(gl.glGetUniformLocation(program, 'size'), float(width), float(height))
|
|
852
|
+
gl.glUniform1i(gl.glGetUniformLocation(program, 'palette'), 0)
|
|
853
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, palette.texture_id)
|
|
854
|
+
gl.glBindVertexArray(vao)
|
|
855
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._style_context_vbo)
|
|
856
|
+
gl.glBufferData(gl.GL_ARRAY_BUFFER, instances.nbytes, instances, gl.GL_STREAM_DRAW)
|
|
857
|
+
gl.glDrawArraysInstanced(gl.GL_TRIANGLES, 0, 6, len(instances))
|
|
858
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, last_array_buffer)
|
|
859
|
+
gl.glBindVertexArray(last_vao)
|
|
860
|
+
gl.glUseProgram(last_program)
|
|
861
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, last_texture)
|
|
862
|
+
(gl.glEnable if last_scissor else gl.glDisable)(gl.GL_SCISSOR_TEST)
|
|
863
|
+
(gl.glEnable if last_blend else gl.glDisable)(gl.GL_BLEND)
|
|
864
|
+
return context.texture_id
|
|
865
|
+
|
|
866
|
+
def _bind_style_context(self, texture_id):
|
|
867
|
+
"""Texture unit 1 = StyleContext for the whole command render."""
|
|
868
|
+
gl.glActiveTexture(gl.GL_TEXTURE1)
|
|
869
|
+
previous = int(gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D))
|
|
870
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, int(texture_id))
|
|
871
|
+
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
872
|
+
return previous
|
|
873
|
+
|
|
874
|
+
@staticmethod
|
|
875
|
+
def _unbind_style_context(previous):
|
|
876
|
+
gl.glActiveTexture(gl.GL_TEXTURE1)
|
|
877
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, previous)
|
|
878
|
+
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
879
|
+
|
|
880
|
+
def _draw_style_elements(self, draw_list, texture_id, start, end, index_type):
|
|
881
|
+
"""One command, one draw. The text shader samples the context layer
|
|
882
|
+
(_ensure_style_context) at gl_FragCoord for what is behind a glyph."""
|
|
883
|
+
gl.glDrawElements(gl.GL_TRIANGLES, end - start, index_type,
|
|
884
|
+
ctypes.c_void_p(start * imgui.INDEX_SIZE))
|
|
885
|
+
|
|
886
|
+
def _render_command_lists(self, draw_data, command_lists) -> None:
|
|
887
|
+
"""Body of ProgrammablePipelineRenderer.render, parameterized over
|
|
888
|
+
which command lists to iterate. GL state save/restore is self-contained
|
|
889
|
+
so this is safe to call multiple times per frame."""
|
|
890
|
+
io = self.io
|
|
891
|
+
|
|
892
|
+
display_width, display_height = io.display_size
|
|
893
|
+
fb_scale_x, fb_scale_y = io.display_fb_scale
|
|
894
|
+
fb_width = int(display_width * fb_scale_x)
|
|
895
|
+
fb_height = int(display_height * fb_scale_y)
|
|
896
|
+
|
|
897
|
+
if fb_width == 0 or fb_height == 0:
|
|
898
|
+
return
|
|
899
|
+
if not command_lists:
|
|
900
|
+
return
|
|
901
|
+
|
|
902
|
+
self._refresh_style_shader()
|
|
903
|
+
common_gl_state_tuple = get_common_gl_state()
|
|
904
|
+
last_program = gl.glGetIntegerv(gl.GL_CURRENT_PROGRAM)
|
|
905
|
+
last_active_texture = gl.glGetIntegerv(gl.GL_ACTIVE_TEXTURE)
|
|
906
|
+
last_array_buffer = gl.glGetIntegerv(gl.GL_ARRAY_BUFFER_BINDING)
|
|
907
|
+
last_element_array_buffer = gl.glGetIntegerv(gl.GL_ELEMENT_ARRAY_BUFFER_BINDING)
|
|
908
|
+
last_vertex_array = gl.glGetIntegerv(gl.GL_VERTEX_ARRAY_BINDING)
|
|
909
|
+
|
|
910
|
+
gl.glEnable(gl.GL_BLEND)
|
|
911
|
+
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
|
912
|
+
gl.glDisable(gl.GL_CULL_FACE)
|
|
913
|
+
gl.glDisable(gl.GL_DEPTH_TEST)
|
|
914
|
+
gl.glEnable(gl.GL_SCISSOR_TEST)
|
|
915
|
+
gl.glActiveTexture(gl.GL_TEXTURE0)
|
|
916
|
+
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
|
|
917
|
+
|
|
918
|
+
ox, oy = self._frame_origin()
|
|
919
|
+
gl.glViewport(ox, oy, fb_width, fb_height)
|
|
920
|
+
|
|
921
|
+
ortho_projection = (ctypes.c_float * 16)(
|
|
922
|
+
2.0 / display_width, 0.0, 0.0, 0.0,
|
|
923
|
+
0.0, 2.0 / -display_height, 0.0, 0.0,
|
|
924
|
+
0.0, 0.0, -1.0, 0.0,
|
|
925
|
+
-1.0, 1.0, 0.0, 1.0,
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
929
|
+
style_context = 0
|
|
930
|
+
if self._lcd_ok and Toggles.dynamic_styles:
|
|
931
|
+
style_context = self._ensure_style_context(fb_width, fb_height, fb_scale_x, fb_scale_y, ox, oy)
|
|
932
|
+
previous_context = self._bind_style_context(style_context)
|
|
933
|
+
gl.glUseProgram(self._shader_handle)
|
|
934
|
+
gl.glUniform1i(self._attrib_location_tex, 0)
|
|
935
|
+
gl.glUniformMatrix4fv(self._attrib_proj_mtx, 1, gl.GL_FALSE, ortho_projection)
|
|
936
|
+
font_tex = self._bind_text_mode()
|
|
937
|
+
gl.glBindVertexArray(self._vao_handle)
|
|
938
|
+
|
|
939
|
+
for commands in command_lists:
|
|
940
|
+
idx_buffer_offset = 0
|
|
941
|
+
|
|
942
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._vbo_handle)
|
|
943
|
+
gl.glBufferData(
|
|
944
|
+
gl.GL_ARRAY_BUFFER,
|
|
945
|
+
commands.vtx_buffer_size * imgui.VERTEX_SIZE,
|
|
946
|
+
ctypes.c_void_p(commands.vtx_buffer_data),
|
|
947
|
+
gl.GL_STREAM_DRAW,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._elements_handle)
|
|
951
|
+
gl.glBufferData(
|
|
952
|
+
gl.GL_ELEMENT_ARRAY_BUFFER,
|
|
953
|
+
commands.idx_buffer_size * imgui.INDEX_SIZE,
|
|
954
|
+
ctypes.c_void_p(commands.idx_buffer_data),
|
|
955
|
+
gl.GL_STREAM_DRAW,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
for command in commands.commands:
|
|
959
|
+
gl.glBindTexture(gl.GL_TEXTURE_2D, command.texture_id)
|
|
960
|
+
if font_tex >= 0:
|
|
961
|
+
gl.glUniform1i(self._loc_atlas, 1 if int(command.texture_id) == font_tex else 0)
|
|
962
|
+
|
|
963
|
+
x, y, z, w = command.clip_rect
|
|
964
|
+
gl.glScissor(int(x) + ox, int(fb_height - w) + oy, int(z - x), int(w - y))
|
|
965
|
+
|
|
966
|
+
if imgui.INDEX_SIZE == 2:
|
|
967
|
+
gltype = gl.GL_UNSIGNED_SHORT
|
|
968
|
+
else:
|
|
969
|
+
gltype = gl.GL_UNSIGNED_INT
|
|
970
|
+
|
|
971
|
+
self._draw_style_elements(commands, command.texture_id,
|
|
972
|
+
idx_buffer_offset // imgui.INDEX_SIZE,
|
|
973
|
+
idx_buffer_offset // imgui.INDEX_SIZE + command.elem_count, gltype)
|
|
974
|
+
|
|
975
|
+
idx_buffer_offset += command.elem_count * imgui.INDEX_SIZE
|
|
976
|
+
|
|
977
|
+
self._unbind_style_context(previous_context)
|
|
978
|
+
restore_common_gl_state(common_gl_state_tuple)
|
|
979
|
+
|
|
980
|
+
gl.glUseProgram(last_program)
|
|
981
|
+
gl.glActiveTexture(last_active_texture)
|
|
982
|
+
gl.glBindVertexArray(last_vertex_array)
|
|
983
|
+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, last_array_buffer)
|
|
984
|
+
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer)
|