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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,323 @@
1
+ """The desktop's window-gesture toggle, mirrored in meltygui's own title bar.
2
+
3
+ Lukas's patched Hyprland (~/.local/opt/hyprland-hdr, `general:left_drag_move`)
4
+ moves a floating window by a plain left drag on its empty space, and its
5
+ hyprbars title bars carry a stateful button (`state = "left_drag_move"`)
6
+ that shows whether that applies to the window's app and toggles it — the
7
+ app's class goes in / out of `general:left_drag_move_exclude`, live and
8
+ persisted through the desktop's Settings overrides, by
9
+ `desktop/left-drag-toggle`. meltygui's frameless windows draw their own
10
+ controls (titlebar.py), so on that desktop they get the same button:
11
+
12
+ available() the compositor has the option (a stock Hyprland answers
13
+ "no such option"; GNOME / KDE have no socket at all)
14
+ enabled() left-drag move applies to THIS window's class right now
15
+ toggle() runs the desktop's own script for the class — one source
16
+ of truth, one notification, the hyprbars refresh included
17
+
18
+ The button owns BOTH compositor gestures for this app's class (09-13):
19
+ plain left-drag MOVE (general:left_drag_move) and right-drag RESIZE
20
+ (general:right_drag_resize). Lit = the compositor drives them; faded = the
21
+ class is excluded from both and the meltygui app does them itself — its
22
+ drag-anywhere xdg_toplevel.move (titlebar.drag_anywhere_enabled) and its
23
+ right-drag resize through the edge physics (os_frame), which is also what
24
+ lets nested meltygui windows keep their own right-drag (the compositor's
25
+ resize swallows the press otherwise). The studio is excluded statically in
26
+ hyprland.lua; apps flip through this button. `toggle()` runs the script
27
+ with `--gestures`, which moves the class through both exclude lists; a
28
+ probe that finds the right list out of step with the left one (a hyprbars
29
+ button or the Settings app edited only the left) runs `--gestures --sync`
30
+ once to align it (`resize_synced`).
31
+
32
+ Both reads are `getoption` requests on Hyprland's socket
33
+ (geometry_feed.hypr_request, ~0.05 ms) made on a daemon thread, the same
34
+ shape as titlebar_buttons: `state()` answers "unknown" until the first
35
+ probe lands, a change requests a frame, `refresh_if_stale` re-probes at
36
+ Toggles.Melty.titlebar_button_refresh_s (the desktop's Settings app can
37
+ flip the option too), `refresh()` after a focus gain. The toggle script
38
+ is spawned through posix_spawn (absolute path, close_fds=False, no cwd —
39
+ the studio never forks, CLAUDE.md) and a re-probe follows it.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import os
45
+ import re
46
+ import shutil
47
+ import subprocess
48
+ import threading
49
+ import time
50
+
51
+ OPTION = "general:left_drag_move"
52
+ EXCLUDE_OPTION = "general:left_drag_move_exclude"
53
+ RESIZE_OPTION = "general:right_drag_resize"
54
+ RESIZE_EXCLUDE_OPTION = "general:right_drag_resize_exclude"
55
+
56
+ # The desktop's toggle script: on PATH if the session exports it, else at
57
+ # the desktop's install root (config/hyprland.lua's `root`).
58
+ _DESKTOP_ROOT = os.path.expanduser("~/.local/opt/hyprland-hdr")
59
+ TOGGLE_SCRIPT = shutil.which("left-drag-toggle") \
60
+ or os.path.join(_DESKTOP_ROOT, "desktop", "left-drag-toggle")
61
+
62
+ # Survives hotswap re-exec (module globals are reused).
63
+ _state = globals().get("_state") or {
64
+ "available": None, # None = not probed yet
65
+ "enabled": False,
66
+ "wm_class": "",
67
+ "probed_at": 0.0,
68
+ "thread": None,
69
+ "toggling": False,
70
+ "resize_synced": True, # right_drag_resize_exclude list matches the move list
71
+ "synced_for": None, # (wm_class, enabled) the last --sync ran for: no loop on a failing script
72
+ }
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Reads
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def window_class():
80
+ """The class Hyprland files this process's window under: what the feed
81
+ saw (`j/clients` → wm_class), else the BOOTED meltygui app's id
82
+ (app.boot's app_id — the GLFW app_id / X11 class hint; known before
83
+ any Surface exists, which is when the first probe runs), else the
84
+ app's declared Surface.app_id, else the studio's own
85
+ (geometry_feed.WM_CLASS). The studio fallback is LAST and only for a
86
+ process that never booted as an app: a probe that read an app as
87
+ "lsd-studio" toggled and synced the wrong class (09-13)."""
88
+ import meltygui.core.windowing.geometry_feed as geometry_feed
89
+ frame = geometry_feed._STATE.get("frame") if isinstance(geometry_feed._STATE, dict) else None
90
+ if frame and frame.get("wm_class"):
91
+ return frame["wm_class"]
92
+ try:
93
+ import meltygui.core.runtime.app as app
94
+ if app._state.get("booted") and app._state.get("app_id"):
95
+ return app._state["app_id"]
96
+ except Exception:
97
+ pass
98
+ try:
99
+ from meltygui.core.windowing.surface import Surface
100
+ if Surface.all:
101
+ return Surface.app_id
102
+ except Exception:
103
+ pass
104
+ return geometry_feed.WM_CLASS
105
+
106
+
107
+ def class_is_certain():
108
+ """True when window_class() comes from something that names THIS
109
+ process's window: the feed's frame or the booted app's id. The
110
+ Surface.app_id default and the studio fallback are guesses — good
111
+ enough to paint the button, never to WRITE the compositor's lists
112
+ (an app probed before its Surface existed read itself as the studio
113
+ and synced the studio's entry away, 09-13)."""
114
+ import meltygui.core.windowing.geometry_feed as geometry_feed
115
+ frame = geometry_feed._STATE.get("frame") if isinstance(geometry_feed._STATE, dict) else None
116
+ if frame and frame.get("wm_class"):
117
+ return True
118
+ try:
119
+ import meltygui.core.runtime.app as app
120
+ return bool(app._state.get("booted") and app._state.get("app_id"))
121
+ except Exception:
122
+ return False
123
+
124
+
125
+ def _getoption(name, request=None):
126
+ """(kind, value) of a Hyprland option through the socket, or
127
+ (None, None) when the option does not exist / no socket."""
128
+ import meltygui.core.windowing.geometry_feed as geometry_feed
129
+ request = request or geometry_feed.hypr_request
130
+ try:
131
+ reply = request(f"j/getoption {name}")
132
+ except Exception:
133
+ return None, None
134
+ reply = reply.strip()
135
+ if not reply.startswith("{"):
136
+ return None, None
137
+ try:
138
+ data = json.loads(reply)
139
+ except ValueError:
140
+ # hyprctl-style replies don't escape quotes inside str values.
141
+ match = re.search(r'"str":\s*"(.*)",\s*"set"', reply)
142
+ return ("str", match.group(1)) if match else (None, None)
143
+ for kind in ("bool", "int", "float", "str"):
144
+ if kind in data:
145
+ return kind, data[kind]
146
+ return None, None
147
+
148
+
149
+ def class_excluded(exclude_text, wm_class):
150
+ """True when `wm_class` fully matches one of the comma-separated
151
+ regexes in the exclude option — the compositor's own rule, and
152
+ desktop/left-drag-toggle's."""
153
+ for entry in (exclude_text or "").split(","):
154
+ entry = entry.strip()
155
+ if not entry:
156
+ continue
157
+ try:
158
+ if re.fullmatch(entry, wm_class):
159
+ return True
160
+ except re.error:
161
+ continue
162
+ return False
163
+
164
+
165
+ def detect(request=None):
166
+ """(available, enabled, wm_class) read from the compositor now."""
167
+ return detect_gestures(request)[:3]
168
+
169
+
170
+ def detect_gestures(request=None):
171
+ """(available, enabled, wm_class, resize_synced): the left-drag read plus
172
+ whether the class's membership in right_drag_resize_exclude matches
173
+ its left-list membership (True when the compositor has no
174
+ right_drag_resize, or the feature is off — nothing to align)."""
175
+ import meltygui.core.windowing.geometry_feed as geometry_feed
176
+ if geometry_feed.backend() != "hyprland":
177
+ return False, False, "", True
178
+ kind, value = _getoption(OPTION, request)
179
+ if kind is None:
180
+ return False, False, "", True
181
+ wm_class = window_class()
182
+ if not value:
183
+ return True, False, wm_class, True # the feature itself is off
184
+ _kind, exclude = _getoption(EXCLUDE_OPTION, request)
185
+ enabled = not class_excluded(str(exclude or ""), wm_class)
186
+ resize_kind, resize_on = _getoption(RESIZE_OPTION, request)
187
+ if resize_kind is None or not resize_on:
188
+ return True, enabled, wm_class, True
189
+ _kind, resize_exclude = _getoption(RESIZE_EXCLUDE_OPTION, request)
190
+ resize_excluded = class_excluded(str(resize_exclude or ""), wm_class)
191
+ return True, enabled, wm_class, resize_excluded == (not enabled)
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Probe thread
196
+ # ---------------------------------------------------------------------------
197
+
198
+ def _probe(run=None):
199
+ try:
200
+ available, enabled, wm_class, resize_synced = detect_gestures()
201
+ except Exception:
202
+ available, enabled, wm_class, resize_synced = False, False, "", True
203
+ changed = (available, enabled) != (_state["available"], _state["enabled"])
204
+ _state.update(available=available, enabled=enabled, wm_class=wm_class,
205
+ resize_synced=resize_synced, probed_at=time.monotonic())
206
+ if changed:
207
+ try:
208
+ from meltygui.core.windowing.glfw_utils import request_render
209
+ request_render()
210
+ except Exception:
211
+ pass
212
+ if available and not resize_synced and class_is_certain():
213
+ _sync_resize(wm_class, enabled, run)
214
+
215
+
216
+ def _sync_resize(wm_class, enabled, run=None):
217
+ """Align right_drag_resize_exclude with the left list for this class
218
+ (the script's `--gestures --sync`), once per (class, state) — a script
219
+ that fails must not be re-run every probe. Re-probes after."""
220
+ if _state["synced_for"] == (wm_class, enabled) or not os.path.isfile(TOGGLE_SCRIPT):
221
+ return
222
+ _state["synced_for"] = (wm_class, enabled)
223
+ run = run or subprocess.run
224
+ try:
225
+ run([TOGGLE_SCRIPT, "--gestures", "--sync", wm_class], close_fds=False,
226
+ capture_output=True, timeout=10.0)
227
+ except Exception as ex:
228
+ print(f"[hypr_left_drag] {TOGGLE_SCRIPT} --gestures --sync {wm_class!r}: {ex}", flush=True)
229
+ return
230
+ try:
231
+ _, _, _, resize_synced = detect_gestures()
232
+ except Exception:
233
+ return
234
+ _state["resize_synced"] = resize_synced
235
+
236
+
237
+ def start_probe(force=False):
238
+ """Read on a daemon thread (idempotent while one runs; a known answer
239
+ is kept unless ``force``). Returns the thread, or None."""
240
+ thread = _state["thread"]
241
+ if thread is not None and thread.is_alive():
242
+ return thread
243
+ if _state["available"] is not None and not force:
244
+ return None
245
+ thread = threading.Thread(target=_probe, name="hypr-left-drag", daemon=True)
246
+ thread.start()
247
+ _state["thread"] = thread
248
+ return thread
249
+
250
+
251
+ def available():
252
+ """True on the patched desktop; False until the first probe lands
253
+ (kicked here if nobody started it)."""
254
+ if _state["available"] is None:
255
+ start_probe()
256
+ return False
257
+ return bool(_state["available"])
258
+
259
+
260
+ def enabled():
261
+ """The compositor's gestures (left-drag move, right-drag resize) apply
262
+ to this window's class (as of the last probe). False = the meltygui app
263
+ moves and resizes itself."""
264
+ return bool(_state["enabled"])
265
+
266
+
267
+ def state():
268
+ """"unknown" before the first probe, "off" without the feature,
269
+ "on" / "excluded" with it — for the info tab / tests."""
270
+ if _state["available"] is None:
271
+ return "unknown"
272
+ if not _state["available"]:
273
+ return "off"
274
+ return "on" if _state["enabled"] else "excluded"
275
+
276
+
277
+ def refresh(min_interval=2.0):
278
+ """Re-read (a focus gain), at most once per ``min_interval`` seconds."""
279
+ if time.monotonic() - _state["probed_at"] >= min_interval:
280
+ start_probe(force=True)
281
+
282
+
283
+ def refresh_if_stale(interval):
284
+ """Per-frame: re-read once the last probe is ``interval`` seconds old."""
285
+ if interval and interval > 0:
286
+ refresh(min_interval=float(interval))
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # The toggle
291
+ # ---------------------------------------------------------------------------
292
+
293
+ def _run_toggle(wm_class, run=None):
294
+ run = run or subprocess.run
295
+ try:
296
+ # posix_spawn: absolute executable, close_fds=False, no env / preexec_fn.
297
+ run([TOGGLE_SCRIPT, "--gestures", wm_class], close_fds=False, capture_output=True, timeout=10.0)
298
+ except Exception as ex:
299
+ print(f"[hypr_left_drag] {TOGGLE_SCRIPT} --gestures {wm_class!r}: {ex}", flush=True)
300
+ finally:
301
+ _state["toggling"] = False
302
+ _probe(run)
303
+
304
+
305
+ def toggle(run=None):
306
+ """Flip the compositor's window gestures (left-drag move AND right-drag
307
+ resize) for this window's class through the desktop's own script
308
+ (`--gestures`: it edits both exclude lists, persists them, refreshes
309
+ hyprbars and notifies). The button flips at once (the probe after the script
310
+ confirms); a second click while one runs is ignored. Returns the
311
+ worker thread, or None when nothing was started."""
312
+ if not available() or _state["toggling"]:
313
+ return None
314
+ if not os.path.isfile(TOGGLE_SCRIPT):
315
+ print(f"[hypr_left_drag] toggle script missing: {TOGGLE_SCRIPT}", flush=True)
316
+ return None
317
+ wm_class = _state["wm_class"] or window_class()
318
+ _state["toggling"] = True
319
+ _state["enabled"] = not _state["enabled"]
320
+ thread = threading.Thread(target=_run_toggle, args=(wm_class, run),
321
+ name="hypr-left-drag-toggle", daemon=True)
322
+ thread.start()
323
+ return thread
@@ -0,0 +1,245 @@
1
+ """
2
+ draw_space_mouse — a 3-D rendering of the SpaceMouse with its live reading.
3
+
4
+ The puck on screen does what the real one does: the cap slides with the
5
+ three translation axes and leans / twists with the three rotation axes,
6
+ straight off the reader's level state (events/space_mouse.py), so the
7
+ window is the place to check axis signs, dead zone and per-axis
8
+ sensitivity (Toggles.SpaceMouse) by eye. Under the image: connection
9
+ status and one row per axis — raw device units, the normalized reading
10
+ the views receive, and a centered bar.
11
+
12
+ Same shape as draw_voxels / draw_line_graph: a pass into a gl_state FBO
13
+ that imgui.image shows, every knob a parameter on the signature (auto
14
+ draw_state params — the camera orbit and the exaggeration of the cap's
15
+ travel persist per view). The moving piece is drawn with gl_gui/pbr.py —
16
+ the immediate-mode PBR renderer: begin_scene, draw_mesh calls over the
17
+ cap's lathe profiles (`_cap_meshes`: the waisted rubber body, the glossy
18
+ inset disc, the blue ring) with materials and transforms as kwargs,
19
+ end_scene (render_puck). Orbit with the middle mouse, wheel to zoom.
20
+
21
+ Not hover-routed: a space_mouse_changed event only reaches the hovered
22
+ view, and this window should animate wherever the pointer is. It reads the
23
+ reader's level state directly and registers as a WATCHER
24
+ (space_mouse.watch): the pump invalidates it on every active frame and
25
+ once more on release, so the cached tile follows the cap.
26
+ """
27
+
28
+ from __future__ import annotations
29
+ import math
30
+
31
+ import meltygui_imgui as imgui
32
+ from meltygui.hdr_color import pack_color
33
+ import numpy as np
34
+
35
+ import meltygui.core.input.space_mouse as space_mouse
36
+ from meltygui.core.graphics.gl_state import GLState
37
+ from meltygui.core.melty import Melty
38
+ from meltygui.pbr import begin_scene
39
+ from meltygui.pbr import end_scene
40
+ from meltygui.pbr import draw_cylinder
41
+ from meltygui.pbr import draw_mesh
42
+ from meltygui.pbr import draw_plane
43
+ from meltygui.pbr import draw_prism
44
+ from meltygui.pbr import environment
45
+ from meltygui.pbr import load_model
46
+ from meltygui.pbr import orbit_camera
47
+ from meltygui.pbr import Light
48
+ from meltygui.pbr import model_matrix
49
+ from meltygui.pbr import rotation_matrix
50
+ from meltygui.core.runtime.toggles import Toggles
51
+ from meltygui.code.new_converters import code_hosts_for
52
+ from meltygui.core.layout.column_core import ColumnLayout
53
+ from meltygui.core.core_render import render_func
54
+ from meltygui.core.rendering.window_decoration import window
55
+
56
+
57
+ def _lathe_profile(fn, y0, y1, steps, r_end=None):
58
+ """Profile rings for pbr.lathe_mesh from a radius function r(y) over
59
+ [y0, y1]: normals from the numeric tangent ((dy, -dr) in the (r, y)
60
+ plane, pointing outward)."""
61
+ ys = np.linspace(y0, y1, steps)
62
+ rs = np.array([fn(y) for y in ys], np.float64)
63
+ dr = np.gradient(rs, ys)
64
+ profile = []
65
+ for r, y, d in zip(rs, ys, dr):
66
+ n = np.array([1.0, -d]); n /= np.linalg.norm(n)
67
+ profile.append((float(r), float(y), float(n[0]), float(n[1])))
68
+ return profile
69
+
70
+
71
+ def _cap_meshes():
72
+ """The SpaceMouse Wireless as lathes. MOVING: the black rubber cap — a
73
+ waisted body that is narrowest near the TOP under an overhanging rim
74
+ and flares out to its widest at the FOOT (the photo), rolling over the
75
+ rim into a lip with the piano-black disc set into it, and a marker
76
+ dimple on the lip's front. STATIC (the base): the black collar the cap
77
+ sits in, the blue ring on the collar's inner edge, and the brushed
78
+ steel skirt widening to the desk. Unit: the cap's foot radius 1; cap
79
+ height CAP_HEIGHT. Built once (module cache), uploaded per GLState by
80
+ draw_mesh."""
81
+ from meltygui.pbr import lathe_mesh
82
+ h = CAP_HEIGHT
83
+ # [tint=(0.9, 0.6, 0.2)]
84
+ r_rim, r_waist, y_waist = 0.86, 0.78, 0.66 * h # the rim and the narrowest ring
85
+ # [tint=(0.9, 0.6, 0.2)]
86
+ lip_radius, inset_depth, inset_r = 0.06, 0.03, 0.74
87
+
88
+ def body_radius(y):
89
+ # a concave arc either side of the waist: gentle near the waist,
90
+ # steep into the overhanging rim above and the wide foot below
91
+ if y < y_waist:
92
+ t = (y_waist - y) / y_waist
93
+ return r_waist + (1.0 - r_waist) * t ** 1.7
94
+ t = (y - y_waist) / (h - lip_radius - y_waist)
95
+ t = max(0.0, min(1.0, t))
96
+ return r_waist + (r_rim - r_waist) * t ** 1.7
97
+
98
+ # [tint=(0.9, 0.6, 0.2)]
99
+ foot_bevel = 0.05
100
+ side = _lathe_profile(body_radius, foot_bevel, h - lip_radius, 40)
101
+ # the foot: flat underside, then a quarter round of `foot_bevel` up into
102
+ # the side (the cap's radius at the bevel's top is ~1.0)
103
+ foot = [(0.0, 0.0, 0.0, -1.0), (1.0 - foot_bevel, 0.0, 0.0, -1.0)]
104
+ for i in range(1, 7):
105
+ a = -math.pi / 2 + (math.pi / 2) * i / 6
106
+ foot.append((1.0 - foot_bevel + foot_bevel * math.cos(a), foot_bevel + foot_bevel * math.sin(a),
107
+ math.cos(a), math.sin(a)))
108
+ lip = []
109
+ for i in range(1, 9):
110
+ a = (math.pi / 2) * i / 8
111
+ lip.append((r_rim - lip_radius + lip_radius * math.cos(a), h - lip_radius + lip_radius * math.sin(a),
112
+ math.cos(a), math.sin(a)))
113
+ lip += [(inset_r + 0.015, h, 0.0, 1.0), (inset_r, h - inset_depth * 0.5, -1.0, 0.0),
114
+ (inset_r - 0.01, h - inset_depth, 0.0, 1.0), (0.0, h - inset_depth, 0.0, 1.0)]
115
+ body = lathe_mesh(foot + side + lip, segments=96)
116
+ top = lathe_mesh([(0.0, h - inset_depth + 0.006, 0.0, 1.0),
117
+ (inset_r - 0.05, h - inset_depth + 0.006, 0.0, 1.0)], segments=96)
118
+
119
+ # ── the base: ONE piece - a collar under the cap's foot rolling over
120
+ # and flaring out to the desk in a single convex curve ──
121
+ def base_radius(y):
122
+ t = max(0.0, min(1.0, (-0.05 - y) / BASE_HEIGHT))
123
+ return 1.04 + 0.56 * math.sin(math.pi / 2 * t) ** 0.7
124
+ y0 = -0.05 - BASE_HEIGHT
125
+ base = ([(0.0, y0 - 0.05, 0.0, -1.0), (1.52, y0 - 0.05, 0.0, -1.0), (1.60, y0 - 0.02, 1.0, -0.4)]
126
+ + _lathe_profile(base_radius, y0, -0.05, 28)
127
+ + [(1.0, -0.05, 0.0, 1.0), (0.0, -0.05, 0.0, 1.0)])
128
+ base = lathe_mesh(base, segments=96)
129
+ # the blue ring: a shelf on the base, under the cap's foot
130
+ ring = lathe_mesh([(1.07, -0.06, 1.0, 0.0), (1.07, -0.03, 1.0, 0.0), (1.07, -0.03, 0.0, 1.0),
131
+ (0.97, -0.03, 0.0, 1.0)], segments=96)
132
+ # the silver bezel around the glossy disc: a flat ring sitting in the inset
133
+ # (outer wall UP, center INWARD, inner wall DOWN - the lathe faces outward
134
+ # along that order; the first version ran the other way and culled)
135
+ # flat ring, a hairline chamfer on its outer edge
136
+ bezel = lathe_mesh([(inset_r + 0.012, h - inset_depth + 0.004, 1.0, 0.0),
137
+ (inset_r + 0.012, h - inset_depth + 0.011, 1.0, 0.0),
138
+ (inset_r + 0.009, h - inset_depth + 0.013, 0.7, 0.7),
139
+ (inset_r - 0.05, h - inset_depth + 0.013, 0.0, 1.0),
140
+ (inset_r - 0.05, h - inset_depth + 0.004, -1.0, 0.0)], segments=96)
141
+ return {"body": body, "top": top, "ring": ring, "base": base, "bezel": bezel}
142
+
143
+
144
+ # [tint=(0.9, 0.6, 0.2)]
145
+ CAP_HEIGHT = 0.85
146
+
147
+
148
+ def inset_depth_of_cap():
149
+ """How far the glossy disc sits below the cap's rim (see _cap_meshes)."""
150
+ return 0.03
151
+ # [tint=(0.9, 0.6, 0.2)]
152
+ BASE_HEIGHT = 0.5
153
+ # The cap hovers this far above the base's shelf and pivots about its FOOT
154
+ # centre, so a full tilt (twist radians) swings the foot's far edge down by
155
+ # sin(twist) × 1 — below HOVER_GAP it would cut into the base.
156
+ # [tint=(0.9, 0.6, 0.2)]
157
+ HOVER_GAP = 0.32
158
+ _CAP_MESHES = globals().get("_CAP_MESHES") or {}
159
+
160
+
161
+ from meltygui.view.input_view import render_puck
162
+
163
+
164
+ # [tint=(0.181, 0.119, 0.294)]
165
+ from meltygui.view.input_view import draw_space_mouse
166
+ draw_space_mouse = window(name='space_mouse', tint=(0.2, 0.21, 0.22))(draw_space_mouse)
167
+
168
+
169
+ def _device_model():
170
+ """The model Toggles.SpaceMouse.model_path names, or None (missing,
171
+ unreadable — reported once — or unset)."""
172
+ path = str(Toggles.SpaceMouse.model_path or "").strip()
173
+ if not path:
174
+ return None
175
+ import os
176
+ if not os.path.isabs(path):
177
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..", "..", path)
178
+ try:
179
+ return load_model(path)
180
+ except Exception as e:
181
+ if _MODEL_WARNED.get(path) != str(e):
182
+ _MODEL_WARNED[path] = str(e)
183
+ print(f"space mouse model {path!r} not used: {e}")
184
+ return None
185
+
186
+
187
+ _MODEL_WARNED: dict = globals().get("_MODEL_WARNED") or {}
188
+
189
+
190
+ def _environment_tint(view_tint):
191
+ """The view's tint as a light colour: normalized to unit brightness (a
192
+ window tint is dark by design) and blended toward white by
193
+ Toggles.SpaceMouse.environment_tint_strength, so the scene sits in
194
+ light of the window's hue without going dim."""
195
+ if not view_tint:
196
+ return (1.0, 1.0, 1.0)
197
+ rgb = [max(0.0, float(c)) for c in tuple(view_tint)[:3]]
198
+ peak = max(rgb) or 1.0
199
+ k = max(0.0, min(1.0, float(Toggles.SpaceMouse.environment_tint_strength)))
200
+ return tuple(1.0 - k + k * (c / peak) for c in rgb)
201
+
202
+
203
+ def _draw_puck(gl_state, axes, width, height, tilt, spin, cam_zoom, travel, twist,
204
+ view_tint=None):
205
+ """render_puck into the view's FBO, shown as an image of width × height."""
206
+ from meltygui.pbr import pbr_pass
207
+ fb = render_puck(gl_state, "target", width, height, axes, tilt, spin, cam_zoom,
208
+ travel, twist, Toggles.SpaceMouse.environment, view_tint=view_tint)
209
+ imgui.image(fb.texture_id, width, height, uv0=(0, 1), uv1=(1, 0))
210
+ if pbr_pass.last_error:
211
+ imgui.text_colored(pbr_pass.last_error.splitlines()[0], 1.0, 0.45, 0.40, 1.0)
212
+
213
+
214
+ def _draw_readout(draw_state, raw, axes, status, bar_width, row_height,
215
+ axis_tint, axis_tint_negative, draw_hz=False):
216
+ """Status line + one row per axis: name, raw units, normalized reading,
217
+ a centred bar. Draw-list rows so the wrapper keeps the layout."""
218
+ draw_list = imgui.get_window_draw_list()
219
+ x, y = imgui.get_cursor_screen_pos()
220
+ left = x + 4
221
+ y += 4
222
+ st = space_mouse.stats()
223
+ text = (f"{status} {st['rate_hz']:.0f} Hz reports, newest {st['age_ms']:.0f} ms old, "
224
+ f"frame {st['frame_ms']:.0f} ms ({Toggles.SpaceMouse.socket_path})")
225
+ tint = (0.55, 0.9, 0.55, 1.0) if status.startswith("connected") else (1.0, 0.6, 0.4, 1.0)
226
+ if draw_hz:
227
+ draw_list.add_text(left, y, pack_color(*tint), text)
228
+ y += row_height
229
+ name_x, raw_x, norm_x = left, left + 30, left + 90
230
+ bar_x = left + 160
231
+ mid = bar_x + bar_width * 0.5
232
+ white = pack_color(0.85, 0.85, 0.85, 1.0)
233
+ dim = pack_color(1.0, 1.0, 1.0, 0.12)
234
+ for name, r, a in zip(space_mouse.AXES, raw, axes):
235
+ draw_list.add_text(name_x, y, white, name)
236
+ draw_list.add_text(raw_x, y, white, f"{int(r):5d}")
237
+ draw_list.add_text(norm_x, y, white, f"{a:+.3f}")
238
+ draw_list.add_rect_filled(bar_x, y + 3, bar_x + bar_width, y + row_height - 3, dim, 2.0)
239
+ draw_list.add_line(mid, y + 2, mid, y + row_height - 2, white, 1.0)
240
+ fill = max(-1.0, min(1.0, a)) * bar_width * 0.5
241
+ color = axis_tint if fill >= 0 else axis_tint_negative
242
+ draw_list.add_rect_filled(min(mid, mid + fill), y + 4, max(mid, mid + fill), y + row_height - 4,
243
+ pack_color(*color, 0.9), 2.0)
244
+ y += row_height
245
+ imgui.dummy(bar_x + bar_width - x, y - imgui.get_cursor_screen_pos()[1] + 4)