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,355 @@
1
+ """Native mouse-cursor shapes for Melty views (I-beam over text, resize
2
+ arrows over column dividers / window edges / the corner handle, ...).
3
+
4
+ Two ways a view says which shape the pointer should show:
5
+
6
+ * **Subscription-level (preferred).** Pass ``cursor=`` to
7
+ ``draw_state.on_action(...)`` / ``InputHandler.register_hovered``. The
8
+ shape rides on the hover subscription, so it obeys exactly the z-order and
9
+ blocker rules the events do — a closable window in front drops it, and a
10
+ child's subscription beats its parent's. ``InputHandler.process_frame``
11
+ resolves it topmost-first and it STICKS for the whole of a captured drag:
12
+ the shape shown at the press stays until release however far the pointer
13
+ strays from the grab rect, and hover shapes are suppressed meanwhile (no
14
+ I-beam while a window is dragged across an editor). A cursor-only
15
+ subscription is ``on_action([], view_id=..., rect=..., cursor=...)`` — no
16
+ events, just the shape over that rect. Views the wrapper registers for
17
+ events can declare a whole-content shape with ``@render_func(mouse_cursor=…)``.
18
+ ``cursor_gate="left_mouse_dragged"`` ties the shape to a DRAG HANDLE: it
19
+ shows only where this registration is the subscriber that would capture
20
+ that event (the topmost ``left_mouse_dragged`` subscriber under the
21
+ pointer), so a window's move handle shows ``MOVE`` on its bare areas and
22
+ nothing over a child that takes the drag itself (a slider, a text
23
+ selection) — the shape tracks where the drag would actually land.
24
+ * **Immediate.** ``imgui.set_mouse_cursor(shape)`` inside code that runs
25
+ THIS frame (imgui resets it every ``new_frame``) — for gestures whose owner
26
+ is not a hover subscription: the right-drag corner resize block in
27
+ core_render, the OS-window edges in titlebar.py, imgui's own widgets. A
28
+ non-arrow imgui shape wins over the subscription shape.
29
+
30
+ ``apply(window)`` runs once per frame at the render tail (``Melty.render``,
31
+ GLFW's event thread) and pushes the resolved shape to GLFW, on change only.
32
+ It steps aside while another owner (the region-screenshot tool) has set its
33
+ own cursor image — that owner reports through ``note_external_cursor``.
34
+ """
35
+ import os
36
+ import struct
37
+
38
+ import meltygui.core.windowing.window_api as glfw
39
+ import meltygui_imgui as imgui
40
+
41
+ ARROW = imgui.MOUSE_CURSOR_ARROW
42
+ TEXT = imgui.MOUSE_CURSOR_TEXT_INPUT
43
+ RESIZE_ALL = imgui.MOUSE_CURSOR_RESIZE_ALL
44
+ RESIZE_NS = imgui.MOUSE_CURSOR_RESIZE_NS
45
+ RESIZE_EW = imgui.MOUSE_CURSOR_RESIZE_EW
46
+ RESIZE_NESW = imgui.MOUSE_CURSOR_RESIZE_NESW
47
+ RESIZE_NWSE = imgui.MOUSE_CURSOR_RESIZE_NWSE
48
+ HAND = imgui.MOUSE_CURSOR_HAND
49
+ NOT_ALLOWED = imgui.MOUSE_CURSOR_NOT_ALLOWED
50
+
51
+ # Directional shapes - one arrow, pointing the way the edge/corner moves:
52
+ # right_side = |>, left_side = <|, bottom_right_corner = ↘ in the L of the
53
+ # corner, ... They sit past imgui's enum (NOT_ALLOWED = 8 is its last), so
54
+ # imgui.set_mouse_cursor won't carry them: use `request(shape)` for an
55
+ # immediate shape, `cursor=` on a subscription. GLFW has no standard cursor
56
+ # for them either - on Wayland they come from the theme image
57
+ # (load_theme_cursor), elsewhere _GLFW_SHAPE maps them to the double arrows.
58
+ RESIZE_E = 101 # |> right_side
59
+ RESIZE_W = 102 # <| left edge
60
+ RESIZE_N = 103 # top edge
61
+ RESIZE_S = 104 # bottom edge
62
+ RESIZE_NW = 105 # top-left corner
63
+ RESIZE_SE = 106 # bottom-right corner
64
+ RESIZE_NE = 107 # top-right corner
65
+ RESIZE_SW = 108 # bottom-left corner
66
+ # The window-move shape: shown where a left drag would move a window - a
67
+ # meltygui window's move handle (core_render's `window_move` on_action) and
68
+ # the OS window's drag strip / drag-anywhere background (titlebar.py) -
69
+ # gated by `cursor_gate="left_mouse_dragged"` so it only appears where that
70
+ # handle would actually CAPTURE the drag. It is on screen most of the time,
71
+ # so it is NOT the four-way fleur: it is the theme's own arrow (the default
72
+ # pointer, same hotspot) recoloured DARK GREY (`_RECOLORED`,
73
+ # load_theme_cursor) - a quiet "this drags the window" hint.
74
+ MOVE = 109
75
+
76
+ _GLFW_SHAPE = {
77
+ TEXT: glfw.IBEAM_CURSOR,
78
+ RESIZE_ALL: glfw.RESIZE_ALL_CURSOR,
79
+ RESIZE_NS: glfw.RESIZE_NS_CURSOR,
80
+ RESIZE_EW: glfw.RESIZE_EW_CURSOR,
81
+ RESIZE_NESW: glfw.RESIZE_NESW_CURSOR,
82
+ RESIZE_NWSE: glfw.RESIZE_NWSE_CURSOR,
83
+ HAND: glfw.POINTING_HAND_CURSOR,
84
+ NOT_ALLOWED: glfw.NOT_ALLOWED_CURSOR,
85
+ # Directional shapes: the nearest double arrow when no theme image loads.
86
+ RESIZE_E: glfw.RESIZE_EW_CURSOR, RESIZE_W: glfw.RESIZE_EW_CURSOR,
87
+ RESIZE_N: glfw.RESIZE_NS_CURSOR, RESIZE_S: glfw.RESIZE_NS_CURSOR,
88
+ RESIZE_NW: glfw.RESIZE_NWSE_CURSOR, RESIZE_SE: glfw.RESIZE_NWSE_CURSOR,
89
+ RESIZE_NE: glfw.RESIZE_NESW_CURSOR, RESIZE_SW: glfw.RESIZE_NESW_CURSOR,
90
+ MOVE: glfw.ARROW_CURSOR, # no badge without a theme image: the plain arrow
91
+ }
92
+
93
+ # Theme search NAMES per shape - the XDG name first, then the legacy X name,
94
+ # the same pairs GLFW's own standard-cursor lookup tries.
95
+ _SHAPE_NAMES = {
96
+ TEXT: ("text", "xterm"),
97
+ RESIZE_ALL: ("all-scroll", "fleur"),
98
+ RESIZE_NS: ("ns-resize", "sb_v_double_arrow"),
99
+ RESIZE_EW: ("ew-resize", "sb_h_double_arrow"),
100
+ RESIZE_NESW: ("nesw-resize", "fd_double_arrow"),
101
+ RESIZE_NWSE: ("nwse-resize", "bd_double_arrow"),
102
+ HAND: ("pointer", "hand2"),
103
+ NOT_ALLOWED: ("not-allowed", "crossed_circle"),
104
+ RESIZE_E: ("e-resize", "right_side"),
105
+ RESIZE_W: ("w-resize", "left_side"),
106
+ RESIZE_N: ("n-resize", "top_side"),
107
+ RESIZE_S: ("s-resize", "bottom_side"),
108
+ RESIZE_NW: ("nw-resize", "top_left_corner"),
109
+ RESIZE_SE: ("se-resize", "bottom_right_corner"),
110
+ RESIZE_NE: ("ne-resize", "top_right_corner"),
111
+ RESIZE_SW: ("sw-resize", "bottom_left_corner"),
112
+ MOVE: ("default", "left_ptr"), # the regular arrow, recoloured below
113
+ }
114
+ # Recoloured shapes: `shape -> (body grey, outline grey)`. The theme image's
115
+ # dark pixels (the body) go to the body grey, its bright pixels (the outline)
116
+ # to the outline grey, by luminance - so a black-on-white arrow keeps its
117
+ # white outline but reads on light AND dark. How to tune: raise the body
118
+ # grey for a lighter pointer, lower the outline grey to darken the rim.
119
+ _RECOLORED = {
120
+ MOVE: (70, 255),
121
+ }
122
+ # libXcursor's default search path (XCURSOR_PATH overrides it).
123
+ _XCURSOR_DEFAULT_PATH = "~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps"
124
+ _XCURSOR_IMAGE_TYPE = 0xFFFD0002
125
+ _GLFW_DEFAULT_CURSOR_SIZE = 16 # wl_init.c loadCursorTheme, XCURSOR_SIZE unset
126
+
127
+ _cursors = {} # (shape, theme names, greys) -> GLFWcursor | (None = the platform default arrow)
128
+ _applied = ARROW # shape last pushed to GLFW; None = unknown, re-push
129
+ _applied_immediate = False # the last push came from an immediate shape
130
+ _requested = None # request(): immediate shape for THIS frame, consumed at the tail
131
+ _external = False # another owner has set its own cursor image
132
+
133
+
134
+ def _theme_dirs():
135
+ path = os.environ.get("XCURSOR_PATH") or _XCURSOR_DEFAULT_PATH
136
+ return [os.path.expanduser(d) for d in path.split(":") if d]
137
+
138
+
139
+ def _find_cursor_file(theme, names, _seen=None):
140
+ """`<dir>/<theme>/cursors/<name>` across the search path, trying every
141
+ name in `names` per theme before following the theme's index.theme
142
+ `Inherits=` chain — libXcursor's resolution order."""
143
+ _seen = _seen if _seen is not None else set()
144
+ if theme in _seen:
145
+ return None
146
+ _seen.add(theme)
147
+ inherits = []
148
+ for directory in _theme_dirs():
149
+ base = os.path.join(directory, theme)
150
+ for name in names:
151
+ candidate = os.path.join(base, "cursors", name)
152
+ if os.path.isfile(candidate):
153
+ return candidate
154
+ index = os.path.join(base, "index.theme")
155
+ if os.path.isfile(index):
156
+ try:
157
+ with open(index, encoding="utf-8", errors="replace") as f:
158
+ for line in f:
159
+ key, sep, value = line.partition("=")
160
+ if sep and key.strip().lower() == "inherits":
161
+ inherits += [t.strip() for t in value.replace(";", ",").split(",") if t.strip()]
162
+ except OSError:
163
+ pass
164
+ for parent in inherits:
165
+ found = _find_cursor_file(parent, names, _seen)
166
+ if found:
167
+ return found
168
+ return None
169
+
170
+
171
+ def load_theme_cursor(shape, size, theme=None):
172
+ """The theme's image for `shape` at the nominal `size` nearest to what
173
+ the theme ships (Xcursor's rule, ties to the larger), as
174
+ (PIL RGBA image, xhot, yhot) ready for glfw.create_cursor. Frame 0 only.
175
+ A `_RECOLORED` shape comes back recoloured. None when the theme has no
176
+ such shape."""
177
+ theme = theme or os.environ.get("XCURSOR_THEME") or "default"
178
+ loaded = _load_theme_image(theme, _SHAPE_NAMES.get(shape), size)
179
+ greys = _RECOLORED.get(shape)
180
+ if loaded is None or greys is None:
181
+ return loaded
182
+ image, xhot, yhot = loaded
183
+ return _recolor(image, *greys), xhot, yhot
184
+
185
+
186
+ def _recolor(image, body_grey, outline_grey):
187
+ """`image` with every pixel's RGB replaced by a grey between `body_grey`
188
+ (where the source is black) and `outline_grey` (where it is white),
189
+ picked by the source pixel's luminance; alpha untouched."""
190
+ import numpy as np
191
+ from PIL import Image
192
+ pixels = np.asarray(image).astype(np.float32)
193
+ luminance = pixels[..., :3].mean(axis=-1, keepdims=True) / 255.0
194
+ pixels[..., :3] = body_grey + (outline_grey - body_grey) * luminance
195
+ return Image.fromarray(pixels.round().astype(np.uint8), "RGBA")
196
+
197
+
198
+ def _load_theme_image(theme, names, size):
199
+ """One theme cursor file (`names` tried in order, then the `default`
200
+ theme) decoded to (PIL RGBA image, xhot, yhot). Xcursor pixels are
201
+ premultiplied ARGB; GLFW wants straight RGBA, so the alpha is divided
202
+ back out. None when the theme has no such shape."""
203
+ if not names:
204
+ return None
205
+ path = _find_cursor_file(theme, names)
206
+ if path is None and theme != "default":
207
+ path = _find_cursor_file("default", names)
208
+ if path is None:
209
+ return None
210
+ with open(path, "rb") as f:
211
+ data = f.read()
212
+ magic, header_size, version, toc_count = struct.unpack_from("<4sIII", data, 0)
213
+ if magic != b"Xcur":
214
+ return None
215
+ images = [] # (nominal-size, position) in file order = frame order
216
+ for i in range(toc_count):
217
+ entry_type, subtype, position = struct.unpack_from("<III", data, header_size + 12 * i)
218
+ if entry_type == _XCURSOR_IMAGE_TYPE:
219
+ images.append((subtype, position))
220
+ if not images:
221
+ return None
222
+ best = min(images, key=lambda e: (abs(e[0] - size), -e[0]))[0]
223
+ position = next(pos for nominal, pos in images if nominal == best)
224
+ (_chunk_size, _type, _subtype, _version,
225
+ width, height, xhot, yhot, _delay) = struct.unpack_from("<9I", data, position)
226
+ import numpy as np
227
+ from PIL import Image
228
+ argb = np.frombuffer(data, dtype="<u4", count=width * height, offset=position + 36).reshape(height, width)
229
+ alpha = (argb >> 24).astype(np.uint32)
230
+ channels = [(argb >> 16) & 255, (argb >> 8) & 255, argb & 255]
231
+ safe_alpha = np.where(alpha == 0, 1, alpha)
232
+ straight = [np.minimum(255, (c * 255 + safe_alpha // 2) // safe_alpha) for c in channels]
233
+ rgba = np.stack(straight + [alpha], axis=-1).astype(np.uint8)
234
+ return Image.fromarray(rgba, "RGBA"), int(xhot), int(yhot)
235
+
236
+
237
+ def _wayland():
238
+ try:
239
+ return glfw.get_platform() == glfw.PLATFORM_WAYLAND
240
+ except Exception:
241
+ return bool(os.environ.get("WAYLAND_DISPLAY"))
242
+
243
+
244
+ def _cursor_size():
245
+ """The nominal size GLFW itself loaded the theme at (its default arrow
246
+ stays GLFW's), so our images match it: XCURSOR_SIZE — exported from
247
+ GNOME at boot by glfw_utils.export_desktop_cursor_env — else 16."""
248
+ try:
249
+ return int(os.environ.get("XCURSOR_SIZE", "")) or _GLFW_DEFAULT_CURSOR_SIZE
250
+ except ValueError:
251
+ return _GLFW_DEFAULT_CURSOR_SIZE
252
+
253
+
254
+ def _glfw_cursor(shape):
255
+ """The GLFW cursor object for `shape`, created once.
256
+
257
+ On Wayland the theme's image is loaded HERE and handed to GLFW as an
258
+ image cursor (glfw.create_cursor), not requested as a standard cursor:
259
+ a standard cursor is a wl_cursor, and GLFW arms its animation timer
260
+ from the frame's `delay` without checking the frame count — Bibata
261
+ (and other clickgen themes) stamp delay=13 on every STATIC shape — so
262
+ the same buffer was re-attached and committed every 13 ms, a light
263
+ irregular flicker of exactly the I-beam / resize shapes (the default
264
+ arrow is a stack-local cursor GLFW never re-sets). An image cursor has
265
+ no wl_cursor, so no timer. Image cursors are buffer scale 1: right-sized
266
+ on scale-1 monitors, blurry (not wrong-sized) on scale-2 ones. Any
267
+ failure falls back to the standard cursor; a shape the theme can't
268
+ provide at all falls back to the arrow and is not retried."""
269
+ # Keyed by the theme names and recolour greys too: a hotswap that edits
270
+ # them (this module's registries to re-exec) gets the new image at once.
271
+ key = (shape, _SHAPE_NAMES.get(shape), _RECOLORED.get(shape))
272
+ if key in _cursors:
273
+ return _cursors[key]
274
+ std = _GLFW_SHAPE.get(shape)
275
+ cur = None
276
+ if std is not None and _wayland():
277
+ try:
278
+ loaded = load_theme_cursor(shape, _cursor_size())
279
+ if loaded is not None:
280
+ image, xhot, yhot = loaded
281
+ cur = glfw.create_cursor(image, xhot, yhot)
282
+ except Exception as e:
283
+ print(f"mouse_cursor: theme image for shape {shape} failed, using GLFW's: {e}")
284
+ cur = None
285
+ if std is not None and cur is None:
286
+ try:
287
+ cur = glfw.create_standard_cursor(std)
288
+ except Exception as e:
289
+ print(f"mouse_cursor: no native cursor for shape {shape}: {e}")
290
+ cur = None
291
+ _cursors[key] = cur
292
+ return cur
293
+
294
+
295
+ def request(shape):
296
+ """Immediate shape for this frame — the module-side twin of
297
+ imgui.set_mouse_cursor for shapes imgui's enum can't carry (the
298
+ directional ones). Same contract: call it from code that runs THIS
299
+ frame; the render-tail apply consumes it, so it must be re-asserted
300
+ every frame it applies. Wins over subscription shapes like imgui's."""
301
+ global _requested
302
+ _requested = shape
303
+
304
+
305
+ def note_external_cursor(on):
306
+ """Another owner (region_screenshot's crosshair) has set (`on`) or
307
+ released the window cursor. While on, `apply` leaves the cursor alone;
308
+ on release the next frame re-pushes ours (GLFW is at the default then)."""
309
+ global _external, _applied, _applied_immediate
310
+ _external = bool(on)
311
+ if not on:
312
+ _applied = None
313
+ _applied_immediate = False
314
+
315
+
316
+ def apply(window, early=False):
317
+ """Push the resolved cursor shape to GLFW. Render thread only.
318
+
319
+ Called TWICE per frame. `early=True` from Melty.begin_frame right after
320
+ the input handler resolved `cursor_shape` against the freshest pointer
321
+ position — BEFORE the draw pass, so a slow frame (draw_text re-rendering)
322
+ can't hold an I-beam the pointer has already left; the shape leads the
323
+ frame instead of trailing it. `early=False` from the render tail, where
324
+ imgui's immediate shapes (titlebar edges, corner resize) are known. An
325
+ immediate shape is re-asserted every frame it applies, so while the tail
326
+ is showing one the early push leaves it alone — otherwise the two would
327
+ alternate. (What this can't fix: the render thread is also the GLFW
328
+ event thread, so nothing moves the shape while a frame is mid-draw.)"""
329
+ global _applied, _applied_immediate, _requested
330
+ if _external:
331
+ return
332
+ if imgui.get_io().config_flags & imgui.CONFIG_NO_MOUSE_CURSOR_CHANGE:
333
+ return
334
+ shape = imgui.get_mouse_cursor()
335
+ immediate = shape != ARROW and shape != imgui.MOUSE_CURSOR_NONE
336
+ if not immediate and _requested is not None:
337
+ shape, immediate = _requested, True
338
+ if not early:
339
+ _requested = None # consumed; re-asserted per frame by its owner
340
+ if not immediate:
341
+ from meltygui.core.melty import Melty
342
+ shape = Melty.event_handler.cursor_shape
343
+ if shape is None:
344
+ shape = ARROW
345
+ if early and _applied_immediate:
346
+ return
347
+ if shape == _applied:
348
+ _applied_immediate = immediate
349
+ return
350
+ try:
351
+ glfw.set_cursor(window, _glfw_cursor(shape))
352
+ except Exception as e:
353
+ print(f"mouse_cursor: set_cursor failed for shape {shape}: {e}")
354
+ _applied = shape
355
+ _applied_immediate = immediate