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,1054 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform userspace input backend using pynput.
|
|
3
|
+
|
|
4
|
+
Works on Linux (X11/Wayland), macOS, and Windows without root/admin.
|
|
5
|
+
|
|
6
|
+
Requirements:
|
|
7
|
+
pip install pynput
|
|
8
|
+
|
|
9
|
+
Note: pynput uses OS-level hooks:
|
|
10
|
+
- Linux: Xlib (X11) or uinput
|
|
11
|
+
- macOS: Quartz event taps
|
|
12
|
+
- Windows: Win32 hooks
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
import threading
|
|
17
|
+
from collections import deque
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import meltygui.core.windowing.window_api as glfw
|
|
22
|
+
|
|
23
|
+
from meltygui.core.windowing.glfw_utils import print_stack_trace
|
|
24
|
+
|
|
25
|
+
# Bare modifiers never count as "typing" for Melody._keys_down / typing_hold.
|
|
26
|
+
_MODIFIER_KEYS = {glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT,
|
|
27
|
+
glfw.KEY_LEFT_CONTROL, glfw.KEY_RIGHT_CONTROL,
|
|
28
|
+
glfw.KEY_LEFT_ALT, glfw.KEY_RIGHT_ALT,
|
|
29
|
+
glfw.KEY_LEFT_SUPER, glfw.KEY_RIGHT_SUPER,
|
|
30
|
+
glfw.KEY_CAPS_LOCK, glfw.KEY_NUM_LOCK}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from pynput import mouse, keyboard
|
|
34
|
+
from pynput.keyboard import Key, KeyCode
|
|
35
|
+
from pynput.mouse import Button
|
|
36
|
+
HAS_PYNPUT = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
HAS_PYNPUT = False
|
|
39
|
+
mouse = keyboard = Key = KeyCode = Button = None
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from meltygui.core.input.input_handler import InputHandler
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# Key Mappings
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
def _key_to_id(key) -> str:
|
|
50
|
+
"""Convert pynput key to input_id string."""
|
|
51
|
+
if not HAS_PYNPUT:
|
|
52
|
+
return str(key)
|
|
53
|
+
|
|
54
|
+
# Regular character keys
|
|
55
|
+
if isinstance(key, KeyCode):
|
|
56
|
+
if key.char:
|
|
57
|
+
return key.char.lower()
|
|
58
|
+
# Virtual key code fallback
|
|
59
|
+
if key.vk:
|
|
60
|
+
return f"vk_{key.vk}"
|
|
61
|
+
|
|
62
|
+
print_stack_trace()
|
|
63
|
+
return "unknown"
|
|
64
|
+
|
|
65
|
+
# Special keys
|
|
66
|
+
if isinstance(key, Key):
|
|
67
|
+
mapping = {
|
|
68
|
+
Key.space: "space",
|
|
69
|
+
Key.enter: "enter",
|
|
70
|
+
Key.tab: "tab",
|
|
71
|
+
Key.backspace: "backspace",
|
|
72
|
+
Key.esc: "escape",
|
|
73
|
+
Key.delete: "delete",
|
|
74
|
+
Key.insert: "insert",
|
|
75
|
+
Key.home: "home",
|
|
76
|
+
Key.end: "end",
|
|
77
|
+
Key.page_up: "page_up",
|
|
78
|
+
Key.page_down: "page_down",
|
|
79
|
+
Key.up: "up",
|
|
80
|
+
Key.down: "down",
|
|
81
|
+
Key.left: "left",
|
|
82
|
+
Key.right: "right",
|
|
83
|
+
Key.shift: "left_shift",
|
|
84
|
+
Key.shift_r: "right_shift",
|
|
85
|
+
Key.ctrl: "left_ctrl",
|
|
86
|
+
Key.ctrl_r: "right_ctrl",
|
|
87
|
+
Key.alt: "left_alt",
|
|
88
|
+
Key.alt_r: "right_alt",
|
|
89
|
+
Key.cmd: "left_meta",
|
|
90
|
+
Key.cmd_r: "right_meta",
|
|
91
|
+
Key.caps_lock: "caps_lock",
|
|
92
|
+
Key.num_lock: "num_lock",
|
|
93
|
+
Key.scroll_lock: "scroll_lock",
|
|
94
|
+
Key.print_screen: "print_screen",
|
|
95
|
+
Key.pause: "pause",
|
|
96
|
+
Key.menu: "menu",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if key in mapping:
|
|
100
|
+
return mapping[key]
|
|
101
|
+
|
|
102
|
+
# Function keys
|
|
103
|
+
name = key.name if hasattr(key, 'name') else str(key)
|
|
104
|
+
if name.startswith("f") and name[1:].isdigit():
|
|
105
|
+
return name
|
|
106
|
+
|
|
107
|
+
return name.lower().replace("key.", "")
|
|
108
|
+
|
|
109
|
+
return str(key).lower()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
Dear ImGui input backend.
|
|
114
|
+
|
|
115
|
+
Works with pyimgui or the imgui bundle (imgui[glfw], imgui[sdl2], etc.)
|
|
116
|
+
|
|
117
|
+
Requirements:
|
|
118
|
+
pip install imgui[glfw] # or imgui[sdl2], pyimgui, etc.
|
|
119
|
+
|
|
120
|
+
Note: ImGui receives input from a windowing backend (GLFW, SDL2, etc.)
|
|
121
|
+
This backend reads ImGui's IO state and converts it to InputHandler events.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
from typing import TYPE_CHECKING
|
|
125
|
+
import time
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
import meltygui_imgui as imgui
|
|
129
|
+
|
|
130
|
+
HAS_IMGUI = True
|
|
131
|
+
except ImportError:
|
|
132
|
+
HAS_IMGUI = False
|
|
133
|
+
imgui = None
|
|
134
|
+
|
|
135
|
+
if TYPE_CHECKING:
|
|
136
|
+
from meltygui.core.input.input_handler import InputHandler
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _button_to_id(button) -> str:
|
|
142
|
+
"""Convert pynput mouse button to input_id string."""
|
|
143
|
+
if not HAS_PYNPUT:
|
|
144
|
+
return str(button)
|
|
145
|
+
|
|
146
|
+
mapping = {
|
|
147
|
+
Button.left: "left_mouse",
|
|
148
|
+
Button.right: "right_mouse",
|
|
149
|
+
Button.middle: "middle_mouse",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if button in mapping:
|
|
153
|
+
return mapping[button]
|
|
154
|
+
|
|
155
|
+
# Extra buttons (Button.x1, Button.x2, etc.)
|
|
156
|
+
name = button.name if hasattr(button, 'name') else str(button)
|
|
157
|
+
return f"mouse_{name}"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Raw Event Container
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
class RawEvent:
|
|
165
|
+
__slots__ = ('type', 'input_id', 'value', 'x', 'y', 'dx', 'dy', 't')
|
|
166
|
+
|
|
167
|
+
DOWN = 0
|
|
168
|
+
UP = 1
|
|
169
|
+
MOVE = 2
|
|
170
|
+
SCROLL = 3
|
|
171
|
+
|
|
172
|
+
def __init__(self, type_: int, input_id: str, value: float,
|
|
173
|
+
x: float, y: float, dx: float, dy: float, t: float):
|
|
174
|
+
self.type = type_
|
|
175
|
+
self.input_id = input_id
|
|
176
|
+
self.value = value
|
|
177
|
+
self.x = x
|
|
178
|
+
self.y = y
|
|
179
|
+
self.dx = dx
|
|
180
|
+
self.dy = dy
|
|
181
|
+
self.t = t
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# ImGui Backend (State-Based)
|
|
186
|
+
# =============================================================================
|
|
187
|
+
|
|
188
|
+
class ImGuiBackend:
|
|
189
|
+
"""
|
|
190
|
+
Backend that reads Dear ImGui's IO state.
|
|
191
|
+
|
|
192
|
+
ImGui maintains input state rather than emitting events, so this backend
|
|
193
|
+
tracks state changes between frames and generates corresponding events.
|
|
194
|
+
|
|
195
|
+
Usage:
|
|
196
|
+
handler = InputHandler()
|
|
197
|
+
backend = ImGuiBackend(handler)
|
|
198
|
+
|
|
199
|
+
while running:
|
|
200
|
+
imgui.new_frame() # ImGui updates IO state here
|
|
201
|
+
|
|
202
|
+
handler.begin_frame()
|
|
203
|
+
handler.register_hovered(...)
|
|
204
|
+
|
|
205
|
+
backend.pump() # Read IO state, generate events
|
|
206
|
+
|
|
207
|
+
events = handler.process_frame()
|
|
208
|
+
|
|
209
|
+
# ... render imgui ...
|
|
210
|
+
imgui.render()
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# ImGui mouse button indices
|
|
214
|
+
MOUSE_BUTTONS = {
|
|
215
|
+
0: "left_mouse",
|
|
216
|
+
1: "right_mouse",
|
|
217
|
+
2: "middle_mouse",
|
|
218
|
+
3: "mouse_4",
|
|
219
|
+
4: "mouse_5",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# ImGui key indices (imgui.KEY_*)
|
|
223
|
+
# This mapping covers common keys; extend as needed
|
|
224
|
+
KEY_NAMES = {
|
|
225
|
+
glfw.KEY_TAB: "tab",
|
|
226
|
+
glfw.KEY_LEFT: "left",
|
|
227
|
+
glfw.KEY_RIGHT: "right",
|
|
228
|
+
glfw.KEY_UP: "up",
|
|
229
|
+
glfw.KEY_DOWN: "down",
|
|
230
|
+
glfw.KEY_PAGE_UP: "page_up",
|
|
231
|
+
glfw.KEY_PAGE_DOWN: "page_down",
|
|
232
|
+
glfw.KEY_HOME: "home",
|
|
233
|
+
glfw.KEY_END: "end",
|
|
234
|
+
glfw.KEY_INSERT: "insert",
|
|
235
|
+
glfw.KEY_DELETE: "delete",
|
|
236
|
+
glfw.KEY_BACKSPACE: "backspace",
|
|
237
|
+
glfw.KEY_SPACE: "space",
|
|
238
|
+
glfw.KEY_ENTER: "enter",
|
|
239
|
+
glfw.KEY_ESCAPE: "escape",
|
|
240
|
+
glfw.KEY_A: "a",
|
|
241
|
+
glfw.KEY_B: "b",
|
|
242
|
+
glfw.KEY_C: "c",
|
|
243
|
+
glfw.KEY_V: "v",
|
|
244
|
+
glfw.KEY_F: "f",
|
|
245
|
+
glfw.KEY_X: "x",
|
|
246
|
+
glfw.KEY_Y: "y",
|
|
247
|
+
glfw.KEY_S: "s",
|
|
248
|
+
glfw.KEY_Z: "z",
|
|
249
|
+
glfw.KEY_SLASH: "slash",
|
|
250
|
+
# -/= - draw_text's scope-fold shortcuts (Ctrl[+Shift]+Minus/Equal)
|
|
251
|
+
# subscribe to these; Ctrl+Equal doubles as "Ctrl+Plus" (plus IS
|
|
252
|
+
# shift+equal, so the shifted variant arrives as ctrl_shift_equal).
|
|
253
|
+
glfw.KEY_MINUS: "minus",
|
|
254
|
+
glfw.KEY_EQUAL: "equal",
|
|
255
|
+
# Arrows - navigation undo/redo (Ctrl+Shift+Left/Right) subscribes to
|
|
256
|
+
# these; the text editor keeps reading its arrows from frame_key_events.
|
|
257
|
+
glfw.KEY_LEFT: "left_arrow",
|
|
258
|
+
glfw.KEY_RIGHT: "right_arrow",
|
|
259
|
+
# Main-row digits - Ctrl+Shift+3 (region screenshot) subscribes via
|
|
260
|
+
# on_action("ctrl_shift_3")"); numeric keys feed as "key_<code>".
|
|
261
|
+
glfw.KEY_0: "0", glfw.KEY_1: "1", glfw.KEY_2: "2", glfw.KEY_3: "3",
|
|
262
|
+
glfw.KEY_4: "4", glfw.KEY_5: "5", glfw.KEY_6: "6", glfw.KEY_7: "7",
|
|
263
|
+
glfw.KEY_8: "8", glfw.KEY_9: "9",
|
|
264
|
+
# Numpad - Blender-style viewport hotkeys (voxel views) live on these.
|
|
265
|
+
glfw.KEY_KP_0: "kp_0",
|
|
266
|
+
glfw.KEY_KP_1: "kp_1",
|
|
267
|
+
glfw.KEY_KP_2: "kp_2",
|
|
268
|
+
glfw.KEY_KP_3: "kp_3",
|
|
269
|
+
glfw.KEY_KP_4: "kp_4",
|
|
270
|
+
glfw.KEY_KP_5: "kp_5",
|
|
271
|
+
glfw.KEY_KP_6: "kp_6",
|
|
272
|
+
glfw.KEY_KP_7: "kp_7",
|
|
273
|
+
glfw.KEY_KP_8: "kp_8",
|
|
274
|
+
glfw.KEY_KP_9: "kp_9",
|
|
275
|
+
glfw.KEY_KP_DECIMAL: "kp_decimal",
|
|
276
|
+
glfw.KEY_KP_DIVIDE: "kp_divide",
|
|
277
|
+
glfw.KEY_KP_MULTIPLY: "kp_multiply",
|
|
278
|
+
glfw.KEY_KP_SUBTRACT: "kp_subtract",
|
|
279
|
+
glfw.KEY_KP_ADD: "kp_add",
|
|
280
|
+
glfw.KEY_KP_ENTER: "kp_enter",
|
|
281
|
+
} if HAS_IMGUI else {}
|
|
282
|
+
|
|
283
|
+
def __init__(self, handler: InputHandler):
|
|
284
|
+
if not HAS_IMGUI:
|
|
285
|
+
raise ImportError("pip install imgui[glfw] # or imgui[sdl2], pyimgui")
|
|
286
|
+
|
|
287
|
+
self.handler = handler
|
|
288
|
+
|
|
289
|
+
# Previous frame state for change detection
|
|
290
|
+
self._prev_mouse_pos = (0.0, 0.0)
|
|
291
|
+
self._prev_mouse_down = [False] * 5
|
|
292
|
+
self._prev_keys_down: set[int] = set()
|
|
293
|
+
self._prev_scroll = (0.0, 0.0)
|
|
294
|
+
|
|
295
|
+
def pump(self):
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
Read ImGui IO state and generate events for state changes.
|
|
299
|
+
Call once per frame after imgui.new_frame().
|
|
300
|
+
"""
|
|
301
|
+
io = imgui.get_io()
|
|
302
|
+
t = time.perf_counter()
|
|
303
|
+
|
|
304
|
+
# --- Mouse Position ---
|
|
305
|
+
mx, my = io.mouse_pos
|
|
306
|
+
px, py = self._prev_mouse_pos
|
|
307
|
+
|
|
308
|
+
if (mx, my) != (px, py) and mx >= 0 and my >= 0:
|
|
309
|
+
dx, dy = mx - px, my - py
|
|
310
|
+
self.handler.feed_move(mx, my, dx, dy, t)
|
|
311
|
+
|
|
312
|
+
self._prev_mouse_pos = (mx, my)
|
|
313
|
+
|
|
314
|
+
# --- Mouse Buttons ---
|
|
315
|
+
for i in range(5):
|
|
316
|
+
input_id = self.MOUSE_BUTTONS.get(i, f"mouse_{i}")
|
|
317
|
+
is_down = io.mouse_down[i]
|
|
318
|
+
was_down = self._prev_mouse_down[i]
|
|
319
|
+
|
|
320
|
+
if is_down and not was_down:
|
|
321
|
+
self.handler.feed_down(input_id, mx, my, t)
|
|
322
|
+
elif not is_down and was_down:
|
|
323
|
+
self.handler.feed_up(input_id, mx, my, t)
|
|
324
|
+
|
|
325
|
+
self._prev_mouse_down[i] = is_down
|
|
326
|
+
|
|
327
|
+
# --- Mouse Wheel ---
|
|
328
|
+
wheel_x = io.mouse_wheel_horizontal
|
|
329
|
+
wheel_y = io.mouse_wheel
|
|
330
|
+
|
|
331
|
+
if wheel_y != 0:
|
|
332
|
+
self.handler.feed_change("scroll_y", wheel_y, t)
|
|
333
|
+
if wheel_x != 0:
|
|
334
|
+
self.handler.feed_change("scroll_x", wheel_x, t)
|
|
335
|
+
|
|
336
|
+
# --- Keyboard ---
|
|
337
|
+
current_keys: set[int] = set()
|
|
338
|
+
|
|
339
|
+
for key_index, key_name in self.KEY_NAMES.items():
|
|
340
|
+
if io.keys_down[key_index]:
|
|
341
|
+
current_keys.add(key_index)
|
|
342
|
+
|
|
343
|
+
# Keys pressed this frame
|
|
344
|
+
for key_index in current_keys - self._prev_keys_down:
|
|
345
|
+
key_name = self.KEY_NAMES.get(key_index, f"key_{key_index}")
|
|
346
|
+
self.handler.feed_down(key_name, mx, my, t)
|
|
347
|
+
|
|
348
|
+
# Keys released this frame
|
|
349
|
+
for key_index in self._prev_keys_down - current_keys:
|
|
350
|
+
key_name = self.KEY_NAMES.get(key_index, f"key_{key_index}")
|
|
351
|
+
self.handler.feed_up(key_name, mx, my, t)
|
|
352
|
+
|
|
353
|
+
self._prev_keys_down = current_keys
|
|
354
|
+
|
|
355
|
+
# --- Modifiers ---
|
|
356
|
+
self.handler.set_modifiers(
|
|
357
|
+
shift=io.key_shift,
|
|
358
|
+
ctrl=io.key_ctrl,
|
|
359
|
+
alt=io.key_alt,
|
|
360
|
+
meta=io.key_super,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def allow_hovering(self) -> bool:
|
|
365
|
+
"""True if ImGui allows hovering (not blocking input)."""
|
|
366
|
+
return not imgui.is_window_hovered()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# =============================================================================
|
|
370
|
+
# GLFW Callback Backend (event-queued, frame-rate independent)
|
|
371
|
+
# =============================================================================
|
|
372
|
+
|
|
373
|
+
# input_id -> GLFW button index (ImGuiBackend.MOUSE_BUTTONS inverted).
|
|
374
|
+
_BUTTON_INDEX = {name: i for i, name in ImGuiBackend.MOUSE_BUTTONS.items()}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class GlfwQueueBackend:
|
|
378
|
+
"""Feeds the InputHandler from GLFW callbacks instead of sampling state once
|
|
379
|
+
per frame.
|
|
380
|
+
|
|
381
|
+
GLFW callbacks fire for *every* event during glfw.poll_events/wait_events,
|
|
382
|
+
even when rendering is slow, so nothing is dropped between frames — fixing
|
|
383
|
+
missed keystrokes, hard-to-click buttons, and skipped drags under load.
|
|
384
|
+
|
|
385
|
+
Callbacks are chained: any previously-registered callback (the imgui
|
|
386
|
+
GlfwRenderer's) is preserved and still called, so imgui keeps working.
|
|
387
|
+
|
|
388
|
+
Keyboard down/up and mouse buttons/scroll are fed to the handler queue
|
|
389
|
+
(consumed via on_action). Raw key presses/repeats are also recorded on
|
|
390
|
+
Melty.frame_key_events so the text editor can drain them in order rather
|
|
391
|
+
than polling imgui.is_key_pressed (which only sees the current frame).
|
|
392
|
+
|
|
393
|
+
Cursor position and modifiers stay sampled in pump() — they're level state,
|
|
394
|
+
so per-frame sampling loses nothing and avoids queue churn from raw motion.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def __init__(self, handler, window):
|
|
398
|
+
if not HAS_IMGUI:
|
|
399
|
+
raise ImportError("pip install imgui[glfw]")
|
|
400
|
+
self.handler = handler
|
|
401
|
+
self.window = window
|
|
402
|
+
self._prev_mouse_pos = (0.0, 0.0)
|
|
403
|
+
# Register our callbacks, chaining whatever was registered pre
|
|
404
|
+
# (imgui's GlfwRenderer). glfw.set_*_callback returns the prior callback.
|
|
405
|
+
self._prev_key = glfw.set_key_callback(window, self._on_key)
|
|
406
|
+
self._prev_char = glfw.set_char_callback(window, self._on_char)
|
|
407
|
+
self._prev_button = glfw.set_mouse_button_callback(window, self._on_button)
|
|
408
|
+
self._prev_scroll = glfw.set_scroll_callback(window, self._on_scroll)
|
|
409
|
+
# Mouse motion (for hover/drag) - only used to defer the background parse
|
|
410
|
+
# while the mouse is busy; chains to whatever imgui registered (if any).
|
|
411
|
+
self._prev_cursor = glfw.set_cursor_pos_callback(window, self._on_move)
|
|
412
|
+
# Pointer inside / outside our window - the GC backend's "is anybody
|
|
413
|
+
# reading?" bit (an unfocused window with the pointer over it is being
|
|
414
|
+
# read, not left).
|
|
415
|
+
self._prev_enter = glfw.set_cursor_enter_callback(window, self._on_enter)
|
|
416
|
+
# The handler asks this before emitting HELD/DRAGGED each frame.
|
|
417
|
+
from meltygui.core.input.input_handler import set_button_probe
|
|
418
|
+
set_button_probe(self.button_really_down)
|
|
419
|
+
|
|
420
|
+
def button_really_down(self, input_id):
|
|
421
|
+
"""The REAL level state of a mouse button, or None for anything that
|
|
422
|
+
isn't one. Three sources, any of which saying UP wins — after a
|
|
423
|
+
freeze every single one can be stale on its own:
|
|
424
|
+
- glfw.get_mouse_button: fed by the same event stream as our
|
|
425
|
+
callbacks, so it misses the same lost release;
|
|
426
|
+
- wayland_move.button_masked: a compositor grab (xdg move/resize)
|
|
427
|
+
swallowed the release and GLFW still reads PRESS;
|
|
428
|
+
- wayland_move's own wl_pointer `held` set: cleared by the release
|
|
429
|
+
AND by a pointer `leave` while buttons are held (the compositor
|
|
430
|
+
broke the implicit grab — the release goes elsewhere, the freeze
|
|
431
|
+
case), which GLFW never reflects in its button state."""
|
|
432
|
+
button = _BUTTON_INDEX.get(input_id)
|
|
433
|
+
if button is None:
|
|
434
|
+
return None
|
|
435
|
+
try:
|
|
436
|
+
if glfw.get_mouse_button(self.window, button) == glfw.RELEASE:
|
|
437
|
+
return False
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
try:
|
|
441
|
+
import meltygui.core.windowing.wayland_move as wayland_move
|
|
442
|
+
if wayland_move.button_masked(button):
|
|
443
|
+
return False
|
|
444
|
+
held = wayland_move.button_held(button)
|
|
445
|
+
if held is False:
|
|
446
|
+
return False
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
@staticmethod
|
|
452
|
+
def _content_cursor(window):
|
|
453
|
+
"""GLFW's cursor in imgui/content coordinates: the frameless
|
|
454
|
+
window's shadow margin (Melty.frame_inset) sits outside imgui's
|
|
455
|
+
display, so the handler's press positions must shift like io.mouse_pos."""
|
|
456
|
+
x, y = glfw.get_cursor_pos(window)
|
|
457
|
+
try:
|
|
458
|
+
from meltygui.core.melty import Melty
|
|
459
|
+
ox, oy = getattr(Melty, "frame_origin", None) or (0, 0)
|
|
460
|
+
except Exception:
|
|
461
|
+
ox, oy = 0, 0
|
|
462
|
+
return x - ox, y - oy
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
def _chain(prev, *args):
|
|
466
|
+
if prev is not None:
|
|
467
|
+
try:
|
|
468
|
+
prev(*args)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _set_mods(handler, mods):
|
|
474
|
+
handler.set_modifiers(
|
|
475
|
+
shift=bool(mods & glfw.MOD_SHIFT), ctrl=bool(mods & glfw.MOD_CONTROL),
|
|
476
|
+
alt=bool(mods & glfw.MOD_ALT), meta=bool(mods & glfw.MOD_SUPER))
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _stamp_input():
|
|
480
|
+
# Recency signal for the cst→dict parse's cooperative UI yield: key (incl.
|
|
481
|
+
# held-key auto-repeat), mouse button, DRAG (held button), or scroll defers
|
|
482
|
+
# the background parse so the render thread stays smooth. Bare hover does
|
|
483
|
+
# NOT (it's cheap and would stall the parse indefinitely - see _on_move).
|
|
484
|
+
try:
|
|
485
|
+
from meltygui.core.melty import Melty
|
|
486
|
+
Melty._last_input_time = time.monotonic()
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
def _on_key(self, window, key, scancode, action, mods):
|
|
491
|
+
try:
|
|
492
|
+
from meltygui.core.melty import Melty
|
|
493
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
494
|
+
self._stamp_input() # key activity (press/repeat/release) defers the parse
|
|
495
|
+
self._set_mods(self.handler, mods)
|
|
496
|
+
x, y = self._content_cursor(window)
|
|
497
|
+
name = ImGuiBackend.KEY_NAMES.get(key, f"key_{key}")
|
|
498
|
+
if action == glfw.PRESS or action == glfw.REPEAT:
|
|
499
|
+
# Orchestrator funnel: records the keystroke for a replayable
|
|
500
|
+
# take, or consumes it while a replay is driving (Esc aborts
|
|
501
|
+
# there). Consumed = imgui never sees it either (chain skipped).
|
|
502
|
+
from meltygui.core.input.input_handler import input_tap
|
|
503
|
+
if input_tap("key", key, mods):
|
|
504
|
+
return
|
|
505
|
+
# Ordered record for the text editor (preserves typed order, and
|
|
506
|
+
# repeats so a held key still inserts/navigates).
|
|
507
|
+
Melty.frame_key_events.append((key, mods))
|
|
508
|
+
# Key-only recency stamp (mouse doesn't touch this) - read by
|
|
509
|
+
# RenderHost.typing_hold to defer host draws while typing.
|
|
510
|
+
Melty._last_key_time = time.monotonic()
|
|
511
|
+
if action == glfw.PRESS:
|
|
512
|
+
# Held-key set for typing_hold: repeats don't arrive
|
|
513
|
+
# reliably (Wayland gaps, OS repeat delay), so the hold
|
|
514
|
+
# rides on this press until RELEASE. Bare modifiers stay out
|
|
515
|
+
# - holding Ctrl while focused isn't typing and would
|
|
516
|
+
# starve host draws indefinitely.
|
|
517
|
+
if key not in _MODIFIER_KEYS:
|
|
518
|
+
Melty._keys_down.add(key)
|
|
519
|
+
self.handler.feed_down(name, x, y)
|
|
520
|
+
request_render()
|
|
521
|
+
elif action == glfw.RELEASE:
|
|
522
|
+
Melty._keys_down.discard(key)
|
|
523
|
+
# The typing-debounce tail starts at release, not the last
|
|
524
|
+
# press - a long hold shouldn't end with an already-expired
|
|
525
|
+
# window.
|
|
526
|
+
Melty._last_key_time = time.monotonic()
|
|
527
|
+
self.handler.feed_up(name, x, y)
|
|
528
|
+
request_render()
|
|
529
|
+
except Exception:
|
|
530
|
+
pass
|
|
531
|
+
self._chain(self._prev_key, window, key, scancode, action, mods)
|
|
532
|
+
|
|
533
|
+
def _on_char(self, window, codepoint):
|
|
534
|
+
# imgui consumes this for its own text editor; the app's text input
|
|
535
|
+
# uses frame_key_events. Just chain so imgui still gets the character.
|
|
536
|
+
# The Orchestrator tap records the char for replay (injected back via
|
|
537
|
+
# io.add_input_character) and mutes real typing while replaying.
|
|
538
|
+
from meltygui.core.input.input_handler import input_tap
|
|
539
|
+
if input_tap("char", codepoint):
|
|
540
|
+
return
|
|
541
|
+
self._chain(self._prev_char, window, codepoint)
|
|
542
|
+
|
|
543
|
+
def _on_button(self, window, button, action, mods):
|
|
544
|
+
try:
|
|
545
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
546
|
+
self._stamp_input() # mouse button press/release defers the parse
|
|
547
|
+
self._set_mods(self.handler, mods)
|
|
548
|
+
x, y = self._content_cursor(window)
|
|
549
|
+
name = ImGuiBackend.MOUSE_BUTTONS.get(button, f"mouse_{button}")
|
|
550
|
+
if action == glfw.PRESS:
|
|
551
|
+
self.handler.feed_down(name, x, y)
|
|
552
|
+
elif action == glfw.RELEASE:
|
|
553
|
+
self.handler.feed_up(name, x, y)
|
|
554
|
+
request_render()
|
|
555
|
+
except Exception:
|
|
556
|
+
pass
|
|
557
|
+
self._chain(self._prev_button, window, button, action, mods)
|
|
558
|
+
|
|
559
|
+
def _on_scroll(self, window, x_offset, y_offset):
|
|
560
|
+
try:
|
|
561
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
562
|
+
self._stamp_input() # scrolling defers the parse
|
|
563
|
+
if y_offset:
|
|
564
|
+
self.handler.feed_change("scroll_y", y_offset)
|
|
565
|
+
if x_offset:
|
|
566
|
+
self.handler.feed_change("scroll_x", x_offset)
|
|
567
|
+
request_render()
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
self._chain(self._prev_scroll, window, x_offset, y_offset)
|
|
571
|
+
|
|
572
|
+
def _on_move(self, window, x, y):
|
|
573
|
+
# Mouse motion. Bare HOVER does NOT defer the cst→dict parse; it's cheap to
|
|
574
|
+
# render, and previously stamping on every move kept the parse from ever
|
|
575
|
+
# resuming (e.g. moving the cursor over a freshly-loaded page stalled it
|
|
576
|
+
# indefinitely, the slow initial load). A DRAG still defers via its held
|
|
577
|
+
# mouse button (pump()'s _any_input_held), so no stamp is needed here.
|
|
578
|
+
# Fires often, so keep it minimal and chain imgui's own cursor callback.
|
|
579
|
+
# It DOES stamp presence for the GC watchdog (gc_manager.tick): a
|
|
580
|
+
# return to the studio starts with the pointer crossing the window.
|
|
581
|
+
# The loop renders only on request (LSDStudio's render loop gates
|
|
582
|
+
# on _needs_render - a wait_events wake alone is no frame), so a
|
|
583
|
+
# new move here triggers the frame that answers it.
|
|
584
|
+
try:
|
|
585
|
+
from meltygui.core.melty import Melty
|
|
586
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
587
|
+
Melty._last_presence_time = time.monotonic()
|
|
588
|
+
request_render()
|
|
589
|
+
except Exception:
|
|
590
|
+
pass
|
|
591
|
+
self._chain(getattr(self, "_prev_cursor", None), window, x, y)
|
|
592
|
+
|
|
593
|
+
def _on_enter(self, window, entered):
|
|
594
|
+
try:
|
|
595
|
+
from meltygui.core.melty import Melty
|
|
596
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
597
|
+
Melty._pointer_inside = bool(entered)
|
|
598
|
+
if entered:
|
|
599
|
+
Melty._last_presence_time = time.monotonic()
|
|
600
|
+
request_render() # hover state changes either way (see _on_move)
|
|
601
|
+
except Exception:
|
|
602
|
+
pass
|
|
603
|
+
self._chain(getattr(self, "_prev_enter", None), window, entered)
|
|
604
|
+
|
|
605
|
+
def pump(self):
|
|
606
|
+
"""Per-frame: feed the latest cursor position (level state), refresh
|
|
607
|
+
modifiers, and refresh the cst→dict cooperative-yield recency signal
|
|
608
|
+
while any input is HELD.
|
|
609
|
+
|
|
610
|
+
The GLFW edge callbacks (PRESS/REPEAT/RELEASE) don't fire every frame —
|
|
611
|
+
GLFW REPEAT is sparse or absent (e.g. on Wayland), and the editor's
|
|
612
|
+
held-key repeat is driven by imgui's own auto-repeat — so stamping only in
|
|
613
|
+
_on_key let _last_input_time go stale mid-hold and the parse stopped
|
|
614
|
+
yielding. imgui's io.keys_down / mouse_down ARE per-frame level state, so
|
|
615
|
+
stamp from them here every frame the input is held. pump() is re-resolved
|
|
616
|
+
each frame, so this path also hotswaps without a restart."""
|
|
617
|
+
io = imgui.get_io()
|
|
618
|
+
mx, my = io.mouse_pos
|
|
619
|
+
px, py = self._prev_mouse_pos
|
|
620
|
+
moved = (mx, my) != (px, py) and mx >= 0 and my >= 0
|
|
621
|
+
if moved:
|
|
622
|
+
self.handler.feed_move(mx, my, mx - px, my - py)
|
|
623
|
+
self._prev_mouse_pos = (mx, my)
|
|
624
|
+
# Missed-release guard: the handler's drag capture latches on is_down,
|
|
625
|
+
# and is_down only clears via feed_up. See button_really_down for the
|
|
626
|
+
# sources of truth; the handler's own process_frame runs the same
|
|
627
|
+
# probe (button_probe) right before it emits HELD/DRAGGED, so a
|
|
628
|
+
# phantom drag can't outlive the frame the button is seen up.
|
|
629
|
+
for i, name in ImGuiBackend.MOUSE_BUTTONS.items():
|
|
630
|
+
if self.handler.is_down(name) and self.button_really_down(name) is False:
|
|
631
|
+
# io.mouse_pos is -FLT_MAX with the cursor off-window; fall
|
|
632
|
+
# back to the handler's last known position.
|
|
633
|
+
ux, uy = (mx, my) if mx >= 0 and my >= 0 else self.handler.cursor()
|
|
634
|
+
self.handler.feed_up(name, ux, uy)
|
|
635
|
+
self.handler.set_modifiers(
|
|
636
|
+
shift=io.key_shift, ctrl=io.key_ctrl, alt=io.key_alt, meta=io.key_super)
|
|
637
|
+
# Only a HELD key/button (typing or a drag) defers the parse; NOT bare
|
|
638
|
+
# hover (`moved` alone), which would stall the parse continuously while the
|
|
639
|
+
# cursor wanders (the slow initial load). Scroll/click/keypress edges still
|
|
640
|
+
# stamp via their own callbacks.
|
|
641
|
+
if self._any_input_held(io):
|
|
642
|
+
self._stamp_input()
|
|
643
|
+
|
|
644
|
+
@staticmethod
|
|
645
|
+
def _any_input_held(io):
|
|
646
|
+
"""True if any key or mouse button is currently down (imgui per-frame
|
|
647
|
+
level state) — so a held key/button keeps deferring the parse each frame,
|
|
648
|
+
regardless of how often GLFW delivers REPEAT edges."""
|
|
649
|
+
if io.key_ctrl or io.key_shift or io.key_alt or io.key_super:
|
|
650
|
+
return True
|
|
651
|
+
kd = io.keys_down
|
|
652
|
+
for i in range(len(kd)):
|
|
653
|
+
if kd[i]:
|
|
654
|
+
return True
|
|
655
|
+
for i in range(5):
|
|
656
|
+
if imgui.is_mouse_down(i):
|
|
657
|
+
return True
|
|
658
|
+
return False
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def allow_hovering(self) -> bool:
|
|
662
|
+
return not imgui.is_window_hovered()
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# =============================================================================
|
|
666
|
+
# Pynput Backend
|
|
667
|
+
# =============================================================================
|
|
668
|
+
|
|
669
|
+
class PynputBackend:
|
|
670
|
+
"""
|
|
671
|
+
Cross-platform userspace input backend.
|
|
672
|
+
|
|
673
|
+
Usage:
|
|
674
|
+
handler = InputHandler()
|
|
675
|
+
backend = PynputBackend(handler)
|
|
676
|
+
backend.start()
|
|
677
|
+
|
|
678
|
+
while running:
|
|
679
|
+
handler.begin_frame()
|
|
680
|
+
handler.register_hovered(...)
|
|
681
|
+
backend.pump()
|
|
682
|
+
events = handler.process_frame()
|
|
683
|
+
|
|
684
|
+
backend.stop()
|
|
685
|
+
"""
|
|
686
|
+
|
|
687
|
+
def __init__(self, handler: InputHandler):
|
|
688
|
+
if not HAS_PYNPUT:
|
|
689
|
+
raise ImportError("pip install pynput")
|
|
690
|
+
|
|
691
|
+
self.handler = handler
|
|
692
|
+
self._queue: deque[RawEvent] = deque(maxlen=1024)
|
|
693
|
+
self._lock = threading.Lock()
|
|
694
|
+
|
|
695
|
+
self._cursor_x = 0.0
|
|
696
|
+
self._cursor_y = 0.0
|
|
697
|
+
|
|
698
|
+
self._shift = False
|
|
699
|
+
self._ctrl = False
|
|
700
|
+
self._alt = False
|
|
701
|
+
self._meta = False
|
|
702
|
+
|
|
703
|
+
self._mouse_listener: mouse.Listener | None = None
|
|
704
|
+
self._keyboard_listener: keyboard.Listener | None = None
|
|
705
|
+
|
|
706
|
+
def start(self):
|
|
707
|
+
"""Start listening for input events."""
|
|
708
|
+
self._mouse_listener = mouse.Listener(
|
|
709
|
+
on_move=self._on_move,
|
|
710
|
+
on_click=self._on_click,
|
|
711
|
+
on_scroll=self._on_scroll,
|
|
712
|
+
)
|
|
713
|
+
self._keyboard_listener = keyboard.Listener(
|
|
714
|
+
on_press=self._on_press,
|
|
715
|
+
on_release=self._on_release,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
self._mouse_listener.start()
|
|
719
|
+
self._keyboard_listener.start()
|
|
720
|
+
|
|
721
|
+
def stop(self):
|
|
722
|
+
"""Stop listening."""
|
|
723
|
+
if self._mouse_listener:
|
|
724
|
+
self._mouse_listener.stop()
|
|
725
|
+
self._mouse_listener = None
|
|
726
|
+
if self._keyboard_listener:
|
|
727
|
+
self._keyboard_listener.stop()
|
|
728
|
+
self._keyboard_listener = None
|
|
729
|
+
|
|
730
|
+
def pump(self):
|
|
731
|
+
"""Transfer accumulated events to handler. Call once per frame."""
|
|
732
|
+
with self._lock:
|
|
733
|
+
events = list(self._queue)
|
|
734
|
+
self._queue.clear()
|
|
735
|
+
self.handler.set_modifiers(self._shift, self._ctrl, self._alt, self._meta)
|
|
736
|
+
|
|
737
|
+
# Process events in order to preserve drag semantics
|
|
738
|
+
for e in events:
|
|
739
|
+
if e.type == RawEvent.DOWN:
|
|
740
|
+
self.handler.feed_down(e.input_id, e.x, e.y, e.t)
|
|
741
|
+
elif e.type == RawEvent.UP:
|
|
742
|
+
self.handler.feed_up(e.input_id, e.x, e.y, e.t)
|
|
743
|
+
elif e.type == RawEvent.MOVE:
|
|
744
|
+
self.handler.feed_move(e.x, e.y, e.dx, e.dy, e.t)
|
|
745
|
+
elif e.type == RawEvent.SCROLL:
|
|
746
|
+
self.handler.feed_change(e.input_id, e.value, e.t)
|
|
747
|
+
|
|
748
|
+
# -------------------------------------------------------------------------
|
|
749
|
+
# Mouse callbacks (run in pynput's thread)
|
|
750
|
+
# -------------------------------------------------------------------------
|
|
751
|
+
|
|
752
|
+
def _on_move(self, x: int, y: int):
|
|
753
|
+
t = time.perf_counter()
|
|
754
|
+
with self._lock:
|
|
755
|
+
dx = x - self._cursor_x
|
|
756
|
+
dy = y - self._cursor_y
|
|
757
|
+
self._cursor_x = float(x)
|
|
758
|
+
self._cursor_y = float(y)
|
|
759
|
+
self._queue.append(RawEvent(
|
|
760
|
+
RawEvent.MOVE, "cursor", 0.0, x, y, dx, dy, t
|
|
761
|
+
))
|
|
762
|
+
|
|
763
|
+
def _on_click(self, x: int, y: int, button, pressed: bool):
|
|
764
|
+
t = time.perf_counter()
|
|
765
|
+
input_id = _button_to_id(button)
|
|
766
|
+
with self._lock:
|
|
767
|
+
self._cursor_x = float(x)
|
|
768
|
+
self._cursor_y = float(y)
|
|
769
|
+
|
|
770
|
+
if pressed:
|
|
771
|
+
self._queue.append(RawEvent(
|
|
772
|
+
RawEvent.DOWN, input_id, 1.0, x, y, 0, 0, t
|
|
773
|
+
))
|
|
774
|
+
else:
|
|
775
|
+
self._queue.append(RawEvent(
|
|
776
|
+
RawEvent.UP, input_id, 0.0, x, y, 0, 0, t
|
|
777
|
+
))
|
|
778
|
+
|
|
779
|
+
def _on_scroll(self, x: int, y: int, dx: int, dy: int):
|
|
780
|
+
t = time.perf_counter()
|
|
781
|
+
with self._lock:
|
|
782
|
+
self._cursor_x = float(x)
|
|
783
|
+
self._cursor_y = float(y)
|
|
784
|
+
|
|
785
|
+
if dy != 0:
|
|
786
|
+
self._queue.append(RawEvent(
|
|
787
|
+
RawEvent.SCROLL, "scroll_y", float(dy), x, y, 0, 0, t
|
|
788
|
+
))
|
|
789
|
+
if dx != 0:
|
|
790
|
+
self._queue.append(RawEvent(
|
|
791
|
+
RawEvent.SCROLL, "scroll_x", float(dx), x, y, 0, 0, t
|
|
792
|
+
))
|
|
793
|
+
|
|
794
|
+
# -------------------------------------------------------------------------
|
|
795
|
+
# Keyboard callbacks (run in pynput's thread)
|
|
796
|
+
# -------------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
def _on_press(self, key):
|
|
799
|
+
t = time.perf_counter()
|
|
800
|
+
input_id = _key_to_id(key)
|
|
801
|
+
self._update_modifiers(input_id, True)
|
|
802
|
+
|
|
803
|
+
with self._lock:
|
|
804
|
+
self._queue.append(RawEvent(
|
|
805
|
+
RawEvent.DOWN, input_id, 1.0,
|
|
806
|
+
self._cursor_x, self._cursor_y, 0, 0, t
|
|
807
|
+
))
|
|
808
|
+
|
|
809
|
+
def _on_release(self, key):
|
|
810
|
+
t = time.perf_counter()
|
|
811
|
+
input_id = _key_to_id(key)
|
|
812
|
+
self._update_modifiers(input_id, False)
|
|
813
|
+
|
|
814
|
+
with self._lock:
|
|
815
|
+
self._queue.append(RawEvent(
|
|
816
|
+
RawEvent.UP, input_id, 0.0,
|
|
817
|
+
self._cursor_x, self._cursor_y, 0, 0, t
|
|
818
|
+
))
|
|
819
|
+
|
|
820
|
+
def _update_modifiers(self, input_id: str, pressed: bool):
|
|
821
|
+
if "shift" in input_id:
|
|
822
|
+
self._shift = pressed
|
|
823
|
+
elif "ctrl" in input_id:
|
|
824
|
+
self._ctrl = pressed
|
|
825
|
+
elif "alt" in input_id:
|
|
826
|
+
self._alt = pressed
|
|
827
|
+
elif "meta" in input_id:
|
|
828
|
+
self._meta = pressed
|
|
829
|
+
|
|
830
|
+
def set_cursor(self, x: float, y: float):
|
|
831
|
+
"""Manually set cursor position if needed."""
|
|
832
|
+
with self._lock:
|
|
833
|
+
self._cursor_x = x
|
|
834
|
+
self._cursor_y = y
|
|
835
|
+
|
|
836
|
+
@property
|
|
837
|
+
def allow_hovering(self) -> bool:
|
|
838
|
+
"""True if ImGui allows hovering (not blocking input)."""
|
|
839
|
+
return True
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
# =============================================================================
|
|
843
|
+
# Window-Integrated Backend (for toolkit integration)
|
|
844
|
+
# =============================================================================
|
|
845
|
+
|
|
846
|
+
class JsonBackend:
|
|
847
|
+
"""
|
|
848
|
+
Backend that receives events as JSON/dict.
|
|
849
|
+
|
|
850
|
+
Use this to integrate with any windowing toolkit (pygame, glfw, sdl, tkinter, etc.)
|
|
851
|
+
by converting their events to a simple dict format.
|
|
852
|
+
|
|
853
|
+
Usage:
|
|
854
|
+
handler = InputHandler()
|
|
855
|
+
backend = JsonBackend(handler)
|
|
856
|
+
|
|
857
|
+
# In your toolkit's event loop:
|
|
858
|
+
for event in toolkit_events:
|
|
859
|
+
backend.push({
|
|
860
|
+
"type": "down", # or "up", "move", "scroll"
|
|
861
|
+
"input": "left_mouse", # or key name
|
|
862
|
+
"x": event.x,
|
|
863
|
+
"y": event.y,
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
backend.pump()
|
|
867
|
+
events = handler.process_frame()
|
|
868
|
+
"""
|
|
869
|
+
|
|
870
|
+
def __init__(self, handler: InputHandler):
|
|
871
|
+
self.handler = handler
|
|
872
|
+
self._queue: deque[dict] = deque(maxlen=1024)
|
|
873
|
+
self._cursor_x = 0.0
|
|
874
|
+
self._cursor_y = 0.0
|
|
875
|
+
|
|
876
|
+
def push(self, event: dict):
|
|
877
|
+
"""
|
|
878
|
+
Push an event dict:
|
|
879
|
+
{"type": "down"|"up"|"move"|"scroll", "input": str, "x": float, "y": float, "value": float}
|
|
880
|
+
"""
|
|
881
|
+
self._queue.append(event)
|
|
882
|
+
|
|
883
|
+
def push_down(self, input_id: str, x: float = None, y: float = None):
|
|
884
|
+
self._queue.append({"type": "down", "input": input_id,
|
|
885
|
+
"x": x if x is not None else self._cursor_x,
|
|
886
|
+
"y": y if y is not None else self._cursor_y})
|
|
887
|
+
|
|
888
|
+
def push_up(self, input_id: str, x: float = None, y: float = None):
|
|
889
|
+
self._queue.append({"type": "up", "input": input_id,
|
|
890
|
+
"x": x if x is not None else self._cursor_x,
|
|
891
|
+
"y": y if y is not None else self._cursor_y})
|
|
892
|
+
|
|
893
|
+
def push_move(self, x: float, y: float, dx: float = None, dy: float = None):
|
|
894
|
+
if dx is None:
|
|
895
|
+
dx = x - self._cursor_x
|
|
896
|
+
if dy is None:
|
|
897
|
+
dy = y - self._cursor_y
|
|
898
|
+
self._cursor_x = x
|
|
899
|
+
self._cursor_y = y
|
|
900
|
+
# Queue move events to preserve ordering with button events
|
|
901
|
+
self._queue.append({"type": "move", "x": x, "y": y, "dx": dx, "dy": dy})
|
|
902
|
+
|
|
903
|
+
def push_scroll(self, dx: float = 0, dy: float = 0):
|
|
904
|
+
if dy:
|
|
905
|
+
self._queue.append({"type": "scroll", "input": "scroll_y", "value": dy})
|
|
906
|
+
if dx:
|
|
907
|
+
self._queue.append({"type": "scroll", "input": "scroll_x", "value": dx})
|
|
908
|
+
|
|
909
|
+
def pump(self):
|
|
910
|
+
"""Transfer events to handler in order."""
|
|
911
|
+
while self._queue:
|
|
912
|
+
e = self._queue.popleft()
|
|
913
|
+
t = e.get("t") or e.get("timestamp")
|
|
914
|
+
|
|
915
|
+
if e["type"] == "down":
|
|
916
|
+
self.handler.feed_down(e["input"], e.get("x", self._cursor_x),
|
|
917
|
+
e.get("y", self._cursor_y), t)
|
|
918
|
+
elif e["type"] == "up":
|
|
919
|
+
self.handler.feed_up(e["input"], e.get("x", self._cursor_x),
|
|
920
|
+
e.get("y", self._cursor_y), t)
|
|
921
|
+
elif e["type"] == "move":
|
|
922
|
+
x, y = e["x"], e["y"]
|
|
923
|
+
dx, dy = e.get("dx", 0), e.get("dy", 0)
|
|
924
|
+
self._cursor_x, self._cursor_y = x, y
|
|
925
|
+
self.handler.feed_move(x, y, dx, dy, t)
|
|
926
|
+
elif e["type"] == "scroll":
|
|
927
|
+
self.handler.feed_change(e["input"], e.get("value", 0), t)
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def allow_hovering(self) -> bool:
|
|
931
|
+
"""True if ImGui allows hovering (not blocking input)."""
|
|
932
|
+
return True
|
|
933
|
+
|
|
934
|
+
#
|
|
935
|
+
# # =============================================================================
|
|
936
|
+
# # Pygame Integration Example
|
|
937
|
+
# # =============================================================================
|
|
938
|
+
#
|
|
939
|
+
# class PygameBackend:
|
|
940
|
+
# """
|
|
941
|
+
# Backend for pygame.
|
|
942
|
+
#
|
|
943
|
+
# Usage:
|
|
944
|
+
# handler = InputHandler()
|
|
945
|
+
# backend = PygameBackend(handler)
|
|
946
|
+
#
|
|
947
|
+
# while running:
|
|
948
|
+
# handler.begin_frame()
|
|
949
|
+
# handler.register_hovered(...)
|
|
950
|
+
#
|
|
951
|
+
# for event in pygame.event.get():
|
|
952
|
+
# backend.handle(event)
|
|
953
|
+
#
|
|
954
|
+
# backend.pump()
|
|
955
|
+
# events = handler.process_frame()
|
|
956
|
+
# """
|
|
957
|
+
#
|
|
958
|
+
# def __init__(self, handler: InputHandler):
|
|
959
|
+
# self.handler = handler
|
|
960
|
+
# self._json = JsonBackend(handler)
|
|
961
|
+
#
|
|
962
|
+
# # Pygame button mapping
|
|
963
|
+
# self._buttons = {
|
|
964
|
+
# 1: "left_mouse",
|
|
965
|
+
# 2: "middle_mouse",
|
|
966
|
+
# 3: "right_mouse",
|
|
967
|
+
# 4: "mouse_4",
|
|
968
|
+
# 5: "mouse_5",
|
|
969
|
+
# }
|
|
970
|
+
#
|
|
971
|
+
# def handle(self, event):
|
|
972
|
+
# """Handle a pygame event."""
|
|
973
|
+
# try:
|
|
974
|
+
# import pygame
|
|
975
|
+
# except ImportError:
|
|
976
|
+
# return
|
|
977
|
+
#
|
|
978
|
+
# if event.type == pygame.MOUSEMOTION:
|
|
979
|
+
# self._json.push_move(event.pos[0], event.pos[1],
|
|
980
|
+
# event.rel[0] if hasattr(event, 'rel') else None,
|
|
981
|
+
# event.rel[1] if hasattr(event, 'rel') else None)
|
|
982
|
+
#
|
|
983
|
+
# elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
984
|
+
# input_id = self._buttons.get(event.button, f"mouse_{event.button}")
|
|
985
|
+
# self._json.push_down(input_id, event.pos[0], event.pos[1])
|
|
986
|
+
#
|
|
987
|
+
# elif event.type == pygame.MOUSEBUTTONUP:
|
|
988
|
+
# input_id = self._buttons.get(event.button, f"mouse_{event.button}")
|
|
989
|
+
# self._json.push_up(input_id, event.pos[0], event.pos[1])
|
|
990
|
+
#
|
|
991
|
+
# elif event.type == pygame.MOUSEWHEEL:
|
|
992
|
+
# self._json.push_scroll(event.x, event.y)
|
|
993
|
+
#
|
|
994
|
+
# elif event.type == pygame.KEYDOWN:
|
|
995
|
+
# input_id = self._key_name(event)
|
|
996
|
+
# self._json.push_down(input_id)
|
|
997
|
+
# self._update_mods(event)
|
|
998
|
+
#
|
|
999
|
+
# elif event.type == pygame.KEYUP:
|
|
1000
|
+
# input_id = self._key_name(event)
|
|
1001
|
+
# self._json.push_up(input_id)
|
|
1002
|
+
# self._update_mods(event)
|
|
1003
|
+
#
|
|
1004
|
+
# def _key_name(self, event) -> str:
|
|
1005
|
+
#
|
|
1006
|
+
# # Check for pygame
|
|
1007
|
+
# import pygame
|
|
1008
|
+
# name = pygame.key.name(event.key)
|
|
1009
|
+
#
|
|
1010
|
+
# # Single letters
|
|
1011
|
+
# if len(name) == 1:
|
|
1012
|
+
# return name.lower()
|
|
1013
|
+
#
|
|
1014
|
+
# # Normalize common names
|
|
1015
|
+
# mapping = {
|
|
1016
|
+
# "space": "space",
|
|
1017
|
+
# "return": "enter",
|
|
1018
|
+
# "escape": "escape",
|
|
1019
|
+
# "tab": "tab",
|
|
1020
|
+
# "backspace": "backspace",
|
|
1021
|
+
# "delete": "delete",
|
|
1022
|
+
# "left shift": "left_shift",
|
|
1023
|
+
# "right shift": "right_shift",
|
|
1024
|
+
# "left ctrl": "left_ctrl",
|
|
1025
|
+
# "right ctrl": "right_ctrl",
|
|
1026
|
+
# "left alt": "left_alt",
|
|
1027
|
+
# "right alt": "right_alt",
|
|
1028
|
+
# "left meta": "left_meta",
|
|
1029
|
+
# "right meta": "right_meta",
|
|
1030
|
+
# }
|
|
1031
|
+
#
|
|
1032
|
+
# return mapping.get(name, name.lower().replace(" ", "_"))
|
|
1033
|
+
#
|
|
1034
|
+
# def _update_mods(self, event):
|
|
1035
|
+
# import pygame
|
|
1036
|
+
# mods = pygame.key.get_mods()
|
|
1037
|
+
# self.handler.set_modifiers(
|
|
1038
|
+
# shift=bool(mods & pygame.KMOD_SHIFT),
|
|
1039
|
+
# ctrl=bool(mods & pygame.KMOD_CTRL),
|
|
1040
|
+
# alt=bool(mods & pygame.KMOD_ALT),
|
|
1041
|
+
# meta=bool(mods & pygame.KMOD_META),
|
|
1042
|
+
# )
|
|
1043
|
+
#
|
|
1044
|
+
# def pump(self):
|
|
1045
|
+
# """Transfer events to handler."""
|
|
1046
|
+
# self._json.pump()
|
|
1047
|
+
#
|
|
1048
|
+
# @property
|
|
1049
|
+
# def allow_hovering(self) -> bool:
|
|
1050
|
+
# """True if ImGui allows hovering (not blocking input)."""
|
|
1051
|
+
# return True
|
|
1052
|
+
#
|
|
1053
|
+
|
|
1054
|
+
|