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,635 @@
1
+ """wp_color_management_v1 for the studio's Wayland surface.
2
+
3
+ Tags the GLFW window's wl_surface with an IMAGE DESCRIPTION so the
4
+ compositor knows what the pixels mean. Without a tag the surface is plain
5
+ sRGB and the compositor maps 1.0 to its SDR reference white; with a PQ tag
6
+ the presentation pass (scene_target.present, Toggles.HDR.output = "pq")
7
+ encodes the linear scRGB scene as BT.2020 + ST 2084 and colours above
8
+ white(1) / outside sRGB reach the panel.
9
+
10
+ Marshalled by hand over libwayland-client's public API exactly like
11
+ wayland_move.py (whose ctypes mirrors and helpers this reuses): the
12
+ interfaces are built as `wl_interface` structs from the protocol tables
13
+ below, opcodes are the message indices, a second wl_registry on GLFW's
14
+ connection binds `wp_color_manager_v1`. A compositor without the global
15
+ (GNOME, an SDR Hyprland session) leaves `available()` False and the studio
16
+ stays an untagged sRGB window.
17
+
18
+ Flow (`sync(window)` once per frame from Melty.post_frame; "auto", the
19
+ default, resolves to "pq" whenever the manager is bound — resolved_output):
20
+ Toggles.HDR.output "pq" → create_parametric_creator → set_primaries_named
21
+ (bt2020) + set_tf_named (st2084_pq) +
22
+ set_luminances (desktop min, desktop max, reference) → create →
23
+ ready → get_surface → set_image_description
24
+ Toggles.HDR.output "srgb" → unset_image_description
25
+ The next commit (GLFW's swap) applies it. Hotswap-safe module state.
26
+
27
+ REFERENCE WHITE follows the desktop: `attach` also takes a
28
+ wp_color_management_surface_feedback_v1 and `query_preferred` reads the
29
+ compositor's PREFERRED parametric description for the surface
30
+ (get_preferred_parametric → get_information → the `luminances` event) — its
31
+ reference luminance is what the compositor shows an untagged sRGB window's
32
+ white as (Hyprland: the monitor's sdr_max_luminance, see Monitor.cpp
33
+ `applyCMType`), so `desired_reference()` hands it to set_luminances AND the
34
+ PQ encode and a colour of 1.0 lands at the same nits as every SDR window.
35
+ A `preferred_changed` event (the user moved the desktop's SDR white, or
36
+ the window changed monitor) re-queries and re-tags on the next sync.
37
+ Without the feature (no feedback, no luminances event) the fallback is
38
+ `Toggles.HDR.pq_reference_nits`; `Toggles.HDR.follow_desktop_white = False`
39
+ pins the toggle's value regardless.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import ctypes
44
+
45
+ import meltygui.core.windowing.window_api as glfw
46
+
47
+ from meltygui.core.windowing.wayland_move import _c
48
+ from meltygui.core.windowing.wayland_move import _iface_addr
49
+ from meltygui.core.windowing.wayland_move import _wl_interface
50
+ from meltygui.core.windowing.wayland_move import _wl_message
51
+ from meltygui.core.windowing.wayland_move import _wl_message_array
52
+
53
+ # Two scopes (see wayland_move for the same split): _STATE is the SURFACE's
54
+ # - its wp_color_management_surface, feedback, description, and mode -
55
+ # swapped per Surface by surface.swap; _CONN is the CONNECTION's - the
56
+ # registry, the bound manager and what it supports, the interface tables,
57
+ # the listener tables and their ctypes callbacks (`keep`) - bound once per
58
+ # wl_display and shared by every window (never swapped, never collected
59
+ # with the window).
60
+ _STATE = globals().get("_STATE") or {
61
+ "display": None, "surface": None,
62
+ "cm_surface": None, "description": None, "applied": None, "wanted": None,
63
+ "ready": None, "failed": None, "identity": None,
64
+ "reference_nits": None, "error": None, "attached_to": None,
65
+ }
66
+ _CONN = globals().get("_CONN") or {
67
+ "display": None, "registry": None, "manager": None, "manager_version": 0,
68
+ "supported_tf": set(), "supported_primaries": set(), "supported_features": set(),
69
+ "done": False, "keep": [], "ifaces": {}, "error": None,
70
+ }
71
+ # Fields added after the first release (a hotswapped module keeps the old dict).
72
+ for _key, _default in (("feedback", None), ("preferred_reference", None), ("preferred_luminances", None),
73
+ ("preferred_dirty", False),
74
+ ("preferred_query_failed", None), ("info_luminances", None), ("info_done", False),
75
+ ("synced_reference", None)):
76
+ _STATE.setdefault(_key, _default)
77
+
78
+ # wp_color_manager_v1 enums (color-management-v1.xml)
79
+ TF_EXT_LINEAR, TF_SRGB, TF_EXT_SRGB, TF_ST2084_PQ, TF_HLG = 5, 9, 10, 11, 13
80
+ PRIMARIES_SRGB, PRIMARIES_BT2020, PRIMARIES_DISPLAY_P3 = 1, 6, 9
81
+ FEATURE_PARAMETRIC, FEATURE_SET_LUMINANCES, FEATURE_WINDOWS_SCRGB = 1, 4, 7
82
+ INTENT_PERCEPTUAL = 0
83
+ _BIND_VERSION = 1 # v1: enough for a parametric PQ / scRGB description
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Interfaces (message tables lifted from the XML: opcode = index)
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def _build_interfaces(wl, keep):
91
+ """The five wl_interface structs this module speaks, kept alive for the
92
+ proxies' lifetime. A message we never send may carry None types."""
93
+ manager, surface, feedback = _wl_interface(), _wl_interface(), _wl_interface()
94
+ creator, desc, info = _wl_interface(), _wl_interface(), _wl_interface()
95
+ keep.extend([manager, surface, feedback, creator, desc, info])
96
+
97
+ def fill(iface, name, version, methods, events):
98
+ iface.name = name
99
+ iface.version = version
100
+ iface.method_count = len(methods)
101
+ iface.methods = ctypes.cast(_wl_message_array(methods, keep), ctypes.POINTER(_wl_message))
102
+ iface.event_count = len(events)
103
+ iface.events = ctypes.cast(_wl_message_array(events, keep), ctypes.POINTER(_wl_message))
104
+
105
+ a_desc, a_creator, a_surface, a_feedback, a_info = (ctypes.addressof(x) for x in
106
+ (desc, creator, surface, feedback, info))
107
+ wl_surface = _iface_addr(wl, "wl_surface_interface")
108
+ wl_output = _iface_addr(wl, "wl_output_interface")
109
+
110
+ fill(info, b"wp_image_description_info_v1", 1, [], [
111
+ (b"done", b"", []), (b"icc_file", b"hu", [None, None]),
112
+ (b"primaries", b"iiiiiiii", [None] * 8), (b"primaries_named", b"u", [None]),
113
+ (b"tf_power", b"u", [None]), (b"tf_named", b"u", [None]),
114
+ (b"luminances", b"uuu", [None] * 3), (b"target_primaries", b"iiiiiiii", [None] * 8),
115
+ (b"target_luminance", b"uu", [None, None]), (b"target_max_cll", b"u", [None]),
116
+ (b"target_max_fall", b"u", [None]),
117
+ ])
118
+ fill(desc, b"wp_image_description_v1", 1, [
119
+ (b"destroy", b"", []), (b"get_information", b"n", [a_info]),
120
+ ], [
121
+ (b"failed", b"us", [None, None]), (b"ready", b"u", [None]), (b"ready2", b"2uu", [None, None]),
122
+ ])
123
+ fill(creator, b"wp_image_description_creator_params_v1", 1, [
124
+ (b"create", b"n", [a_desc]), (b"set_tf_named", b"u", [None]), (b"set_tf_power", b"u", [None]),
125
+ (b"set_primaries_named", b"u", [None]), (b"set_primaries", b"iiiiiiii", [None] * 8),
126
+ (b"set_luminances", b"uuu", [None] * 3),
127
+ (b"set_mastering_display_primaries", b"iiiiiiii", [None] * 8),
128
+ (b"set_mastering_luminance", b"uu", [None, None]),
129
+ (b"set_max_cll", b"u", [None]), (b"set_max_fall", b"u", [None]),
130
+ ], [])
131
+ fill(surface, b"wp_color_management_surface_v1", 1, [
132
+ (b"destroy", b"", []), (b"set_image_description", b"ou", [a_desc, None]),
133
+ (b"unset_image_description", b"", []),
134
+ ], [])
135
+ fill(feedback, b"wp_color_management_surface_feedback_v1", 1, [
136
+ (b"destroy", b"", []), (b"get_preferred", b"n", [a_desc]), (b"get_preferred_parametric", b"n", [a_desc]),
137
+ ], [
138
+ (b"preferred_changed", b"u", [None]), (b"preferred_changed2", b"2uu", [None, None]),
139
+ ])
140
+ fill(manager, b"wp_color_manager_v1", 1, [
141
+ (b"destroy", b"", []), (b"get_output", b"no", [None, wl_output]),
142
+ (b"get_surface", b"no", [a_surface, wl_surface]),
143
+ (b"get_surface_feedback", b"no", [a_feedback, wl_surface]),
144
+ (b"create_icc_creator", b"n", [None]), (b"create_parametric_creator", b"n", [a_creator]),
145
+ (b"create_windows_scrgb", b"n", [a_desc]), (b"get_image_description", b"2no", [a_desc, None]),
146
+ (b"create_windows_bt2100", b"3n", [a_desc]),
147
+ ], [
148
+ (b"supported_intent", b"u", [None]), (b"supported_feature", b"u", [None]),
149
+ (b"supported_tf_named", b"u", [None]), (b"supported_primaries_named", b"u", [None]),
150
+ (b"done", b"", []),
151
+ ])
152
+ return {"manager": manager, "surface": surface, "feedback": feedback,
153
+ "creator": creator, "desc": desc, "info": info}
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Listeners
158
+ # ---------------------------------------------------------------------------
159
+
160
+ _GLOBAL_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
161
+ ctypes.c_char_p, ctypes.c_uint32)
162
+ _GLOBAL_REMOVE_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32)
163
+ _UINT_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32)
164
+ _VOID_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p)
165
+ _FAILED_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_char_p)
166
+ _UINT2_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32)
167
+ _UINT3_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32,
168
+ ctypes.c_uint32)
169
+ _ICC_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int32, ctypes.c_uint32)
170
+ _INT8_CB = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, *([ctypes.c_int32] * 8))
171
+
172
+
173
+ def _on_global(data, registry, name, interface, version):
174
+ if interface == b"wp_color_manager_v1" and _CONN["manager"] is None:
175
+ _, wl = _c()
176
+ ver = min(int(version), _BIND_VERSION)
177
+ manager = wl.wl_proxy_marshal_flags(
178
+ registry, 0, ctypes.addressof(_CONN["ifaces"]["manager"]), ver, 0,
179
+ ctypes.c_uint32(name), ctypes.c_char_p(b"wp_color_manager_v1"),
180
+ ctypes.c_uint32(ver), ctypes.c_void_p(None))
181
+ if manager:
182
+ _CONN["manager"] = manager
183
+ _CONN["manager_version"] = ver
184
+ wl.wl_proxy_add_listener(manager, _CONN["manager_listener"], None)
185
+
186
+
187
+ def _on_global_remove(data, registry, name):
188
+ pass
189
+
190
+
191
+ def _on_intent(data, proxy, value):
192
+ pass
193
+
194
+
195
+ def _on_feature(data, proxy, value):
196
+ _CONN["supported_features"].add(int(value))
197
+
198
+
199
+ def _on_tf(data, proxy, value):
200
+ _CONN["supported_tf"].add(int(value))
201
+
202
+
203
+ def _on_primaries(data, proxy, value):
204
+ _CONN["supported_primaries"].add(int(value))
205
+
206
+
207
+ def _on_done(data, proxy):
208
+ _CONN["done"] = True
209
+
210
+
211
+ def _on_failed(data, proxy, cause, msg):
212
+ _STATE["failed"] = (int(cause), (msg or b"").decode(errors="replace"))
213
+ _STATE["ready"] = False
214
+
215
+
216
+ def _on_ready(data, proxy, identity):
217
+ _STATE["identity"] = int(identity)
218
+ _STATE["ready"] = True
219
+
220
+
221
+ def _on_ready2(data, proxy, hi, lo):
222
+ _STATE["identity"] = (int(hi) << 32) | int(lo)
223
+ _STATE["ready"] = True
224
+
225
+
226
+ def _on_preferred_changed(data, proxy, *identity):
227
+ # The compositor's preferred description for the surface changed (SDR
228
+ # white moved, monitor changed): re-read it on the next sync, which
229
+ # runs in post_frame — so ask for a frame (an idle app otherwise keeps
230
+ # the stale tag until the next input).
231
+ _STATE["preferred_dirty"] = True
232
+ from meltygui.core.windowing.glfw_utils import request_render
233
+ request_render()
234
+
235
+
236
+ def _on_info_luminances(data, proxy, min_lum, max_lum, reference):
237
+ # min is in 0.0001 cd/m², max and reference in cd/m²
238
+ _STATE["info_luminances"] = (int(min_lum) / 10000.0, float(max_lum), float(reference))
239
+
240
+
241
+ def _on_info_done(data, proxy):
242
+ _STATE["info_done"] = True
243
+
244
+
245
+ def _on_info_icc(data, proxy, fd, size):
246
+ import os
247
+ try:
248
+ os.close(int(fd)) # never sent for a parametric info; a leak otherwise
249
+ except OSError:
250
+ pass
251
+
252
+
253
+ def _on_info_ignore(data, proxy, *args):
254
+ pass
255
+
256
+
257
+ def _table(callbacks):
258
+ cbs = [ctor(fn) for ctor, fn in callbacks]
259
+ table = (ctypes.c_void_p * len(cbs))(*[ctypes.cast(cb, ctypes.c_void_p).value for cb in cbs])
260
+ _CONN["keep"].extend(cbs + [table])
261
+ return ctypes.addressof(table)
262
+
263
+
264
+ def _build_listeners():
265
+ _CONN["registry_listener"] = _table([(_GLOBAL_CB, _on_global), (_GLOBAL_REMOVE_CB, _on_global_remove)])
266
+ _CONN["manager_listener"] = _table([(_UINT_CB, _on_intent), (_UINT_CB, _on_feature),
267
+ (_UINT_CB, _on_tf), (_UINT_CB, _on_primaries),
268
+ (_VOID_CB, _on_done)])
269
+ _CONN["desc_listener"] = _table([(_FAILED_CB, _on_failed), (_UINT_CB, _on_ready),
270
+ (_UINT2_CB, _on_ready2)])
271
+ _CONN["feedback_listener"] = _table([(_UINT_CB, _on_preferred_changed),
272
+ (_UINT2_CB, _on_preferred_changed)])
273
+ # wp_image_description_info_v1 events, in table order (see _build_interfaces)
274
+ _CONN["info_listener"] = _table([
275
+ (_VOID_CB, _on_info_done), (_ICC_CB, _on_info_icc), (_INT8_CB, _on_info_ignore),
276
+ (_UINT_CB, _on_info_ignore), (_UINT_CB, _on_info_ignore), (_UINT_CB, _on_info_ignore),
277
+ (_UINT3_CB, _on_info_luminances), (_INT8_CB, _on_info_ignore), (_UINT2_CB, _on_info_ignore),
278
+ (_UINT_CB, _on_info_ignore), (_UINT_CB, _on_info_ignore),
279
+ ])
280
+
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # Attach / describe / apply
284
+ # ---------------------------------------------------------------------------
285
+
286
+ def attach(display, surface) -> bool:
287
+ """Bind the colour manager on `display` (a wl_display*) for `surface`
288
+ (a wl_surface*). Returns True when the compositor offers it; the reason
289
+ for a False sits in last_error(). Idempotent per (display, surface)."""
290
+ if _STATE["attached_to"] == (display, surface):
291
+ return available()
292
+ _STATE["attached_to"] = (display, surface)
293
+ try:
294
+ _, wl = _c()
295
+ if not display or not surface:
296
+ _STATE["error"] = "no Wayland display/surface"
297
+ return False
298
+ if _STATE["display"] != display or _STATE["surface"] != surface:
299
+ _STATE.update(cm_surface=None, description=None, applied=None, wanted=None,
300
+ feedback=None, preferred_reference=None, preferred_luminances=None,
301
+ preferred_dirty=False, preferred_query_failed=None, synced_reference=None)
302
+ _STATE.update(display=display, surface=surface)
303
+ if not hasattr(wl, "_melty_color_bound"):
304
+ wl.wl_proxy_destroy.argtypes = [ctypes.c_void_p]
305
+ wl.wl_proxy_destroy.restype = None
306
+ wl._melty_color_bound = True
307
+ if not _bind_connection(display):
308
+ _STATE["error"] = _CONN["error"]
309
+ return False
310
+ _STATE["error"] = None
311
+ query_preferred() # the desktop's SDR white; a failure here only means the fallback
312
+ return True
313
+ except Exception as e: # never take the studio down over a colour tag
314
+ _STATE["error"] = f"{type(e).__name__}: {e}"
315
+ return False
316
+
317
+
318
+ def _bind_connection(display) -> bool:
319
+ """Once per wl_display: the registry, the colour manager and its
320
+ supported_* tables. False with the reason in _CONN["error"]."""
321
+ _, wl = _c()
322
+ if _CONN["display"] != display:
323
+ # A new connection (fresh GLFW instance): the old proxies are dangling.
324
+ _CONN.update(registry=None, manager=None, manager_version=0, done=False, supported_tf=set(),
325
+ supported_primaries=set(), supported_features=set(), error=None)
326
+ _CONN["display"] = display
327
+ if not _CONN["ifaces"]:
328
+ _CONN["ifaces"] = _build_interfaces(wl, _CONN["keep"])
329
+ _build_listeners()
330
+ if _CONN["registry"] is None:
331
+ reg_iface = _iface_addr(wl, "wl_registry_interface")
332
+ registry = wl.wl_proxy_marshal_flags(display, 1, reg_iface, wl.wl_proxy_get_version(display), 0,
333
+ ctypes.c_void_p(None))
334
+ if not registry:
335
+ _CONN["error"] = "wl_display.get_registry failed"
336
+ return False
337
+ _CONN["registry"] = registry
338
+ wl.wl_proxy_add_listener(registry, _CONN["registry_listener"], None)
339
+ wl.wl_display_roundtrip(display) # globals → manager bound
340
+ wl.wl_display_roundtrip(display) # manager's supported_* + done
341
+ if _CONN["manager"] is None:
342
+ _CONN["error"] = "compositor offers no wp_color_manager_v1"
343
+ return False
344
+ _CONN["error"] = None
345
+ return True
346
+
347
+
348
+ def detach(surface=None):
349
+ """The surface is going away (Surface.destroy, before glfw.destroy_window):
350
+ destroy ITS colour-management objects — feedback, surface tag,
351
+ description — and forget them. The connection's manager stays bound
352
+ for the other windows. ``surface`` None: whatever _STATE holds."""
353
+ if surface is not None and _STATE["surface"] != surface:
354
+ return
355
+ for key in ("feedback", "cm_surface", "description"):
356
+ try:
357
+ _destroy(key)
358
+ except Exception:
359
+ _STATE[key] = None
360
+ _STATE.update(surface=None, attached_to=None, applied=None, wanted=None, synced_reference=None,
361
+ preferred_reference=None, preferred_luminances=None, preferred_dirty=False)
362
+
363
+
364
+ def available() -> bool:
365
+ return bool(_CONN["manager"] and _STATE["error"] is None)
366
+
367
+
368
+ def last_error():
369
+ return _STATE["error"]
370
+
371
+
372
+ def supported() -> dict:
373
+ return {"tf": sorted(_CONN["supported_tf"]), "primaries": sorted(_CONN["supported_primaries"]),
374
+ "features": sorted(_CONN["supported_features"]), "version": _CONN["manager_version"]}
375
+
376
+
377
+ def _destroy(key):
378
+ proxy = _STATE.get(key)
379
+ if proxy:
380
+ _, wl = _c()
381
+ wl.wl_proxy_marshal_flags(proxy, 0, ctypes.c_void_p(None), wl.wl_proxy_get_version(proxy),
382
+ 1) # WL_MARSHAL_FLAG_DESTROY: request 0 is destroy on all of these
383
+ _STATE[key] = None
384
+
385
+
386
+ def create_description(primaries: int, tf: int, reference_nits: float | None = None,
387
+ max_nits: float = 10000.0, min_nits: float = 0.0) -> bool:
388
+ """A parametric image description; blocks for the compositor's ready /
389
+ failed. On success it replaces `_STATE["description"]`."""
390
+ _, wl = _c()
391
+ manager, display = _CONN["manager"], _STATE["display"]
392
+ if not manager:
393
+ return False
394
+ if tf not in _CONN["supported_tf"] or primaries not in _CONN["supported_primaries"]:
395
+ _STATE["error"] = f"compositor lacks tf {tf} / primaries {primaries}: {supported()}"
396
+ return False
397
+ ifaces = _CONN["ifaces"]
398
+ ver = _CONN["manager_version"]
399
+ creator = wl.wl_proxy_marshal_flags(manager, 5, ctypes.addressof(ifaces["creator"]), ver, 0,
400
+ ctypes.c_void_p(None))
401
+ if not creator:
402
+ _STATE["error"] = "create_parametric_creator failed"
403
+ return False
404
+ wl.wl_proxy_marshal_flags(creator, 3, ctypes.c_void_p(None), ver, 0, ctypes.c_uint32(primaries))
405
+ wl.wl_proxy_marshal_flags(creator, 1, ctypes.c_void_p(None), ver, 0, ctypes.c_uint32(tf))
406
+ used_reference = None
407
+ if reference_nits and FEATURE_SET_LUMINANCES in _CONN["supported_features"]:
408
+ # min_lum is in 0.0001 cd/m², the others in cd/m²
409
+ wl.wl_proxy_marshal_flags(creator, 5, ctypes.c_void_p(None), ver, 0,
410
+ ctypes.c_uint32(int(round(min_nits * 10000))), ctypes.c_uint32(int(max_nits)),
411
+ ctypes.c_uint32(int(round(reference_nits))))
412
+ used_reference = float(int(round(reference_nits)))
413
+ _STATE.update(ready=None, failed=None, identity=None)
414
+ # create(new_id) consumes the creator (the protocol destroys it); we
415
+ # still free the client proxy.
416
+ desc = wl.wl_proxy_marshal_flags(creator, 0, ctypes.addressof(ifaces["desc"]), ver, 0,
417
+ ctypes.c_void_p(None))
418
+ wl.wl_proxy_destroy(creator)
419
+ if not desc:
420
+ _STATE["error"] = "image description create failed"
421
+ return False
422
+ wl.wl_proxy_add_listener(desc, _CONN["desc_listener"], None)
423
+ for _ in range(4):
424
+ wl.wl_display_roundtrip(display)
425
+ if _STATE["ready"] is not None:
426
+ break
427
+ if not _STATE["ready"]:
428
+ _STATE["error"] = f"image description failed: {_STATE['failed']}"
429
+ wl.wl_proxy_marshal_flags(desc, 0, ctypes.c_void_p(None), ver, 1)
430
+ return False
431
+ _destroy("description")
432
+ _STATE["description"] = desc
433
+ _STATE["reference_nits"] = used_reference
434
+ return True
435
+
436
+
437
+ def set_surface_description(intent: int = INTENT_PERCEPTUAL) -> bool:
438
+ """Tag the surface with the current description (create_description first)."""
439
+ _, wl = _c()
440
+ manager, surface, desc = _CONN["manager"], _STATE["surface"], _STATE["description"]
441
+ if not (manager and surface and desc):
442
+ return False
443
+ ver = _CONN["manager_version"]
444
+ if _STATE["cm_surface"] is None:
445
+ cm = wl.wl_proxy_marshal_flags(manager, 2, ctypes.addressof(_CONN["ifaces"]["surface"]), ver, 0,
446
+ ctypes.c_void_p(None), ctypes.c_void_p(surface))
447
+ if not cm:
448
+ _STATE["error"] = "get_surface failed"
449
+ return False
450
+ _STATE["cm_surface"] = cm
451
+ wl.wl_proxy_marshal_flags(_STATE["cm_surface"], 1, ctypes.c_void_p(None), ver, 0,
452
+ ctypes.c_void_p(desc), ctypes.c_uint32(intent))
453
+ wl.wl_display_flush(_STATE["display"])
454
+ return True
455
+
456
+
457
+ def unset_surface_description() -> bool:
458
+ _, wl = _c()
459
+ cm = _STATE["cm_surface"]
460
+ if not cm:
461
+ return True
462
+ wl.wl_proxy_marshal_flags(cm, 2, ctypes.c_void_p(None), _CONN["manager_version"], 0)
463
+ wl.wl_display_flush(_STATE["display"])
464
+ return True
465
+
466
+
467
+ def reference_nits():
468
+ """The reference white the compositor was told for the applied PQ tag,
469
+ or None (untagged / luminances unsupported → the compositor's default)."""
470
+ return _STATE["reference_nits"] if _STATE["applied"] == "pq" else None
471
+
472
+
473
+ def _ensure_feedback() -> bool:
474
+ _, wl = _c()
475
+ manager, surface = _CONN["manager"], _STATE["surface"]
476
+ if not (manager and surface):
477
+ return False
478
+ if _STATE["feedback"] is None:
479
+ fb = wl.wl_proxy_marshal_flags(manager, 3, ctypes.addressof(_CONN["ifaces"]["feedback"]),
480
+ _CONN["manager_version"], 0, ctypes.c_void_p(None),
481
+ ctypes.c_void_p(surface))
482
+ if not fb:
483
+ _STATE["preferred_query_failed"] = "get_surface_feedback failed"
484
+ return False
485
+ _STATE["feedback"] = fb
486
+ wl.wl_proxy_add_listener(fb, _CONN["feedback_listener"], None)
487
+ return True
488
+
489
+
490
+ def query_preferred() -> float | None:
491
+ """Ask the compositor for the surface's PREFERRED parametric image
492
+ description and read its reference luminance (nits) — the desktop's SDR
493
+ white. Blocks for a few roundtrips. Returns the reference (also kept in
494
+ `_STATE["preferred_reference"]`), or None with the reason in
495
+ `preferred_query_failed()`."""
496
+ _, wl = _c()
497
+ _STATE["preferred_dirty"] = False
498
+ if not _ensure_feedback():
499
+ return None
500
+ ver, display = _CONN["manager_version"], _STATE["display"]
501
+ ifaces = _CONN["ifaces"]
502
+ _STATE.update(ready=None, failed=None, identity=None, info_luminances=None, info_done=False)
503
+ desc = wl.wl_proxy_marshal_flags(_STATE["feedback"], 2, ctypes.addressof(ifaces["desc"]), ver, 0,
504
+ ctypes.c_void_p(None))
505
+ if not desc:
506
+ _STATE["preferred_query_failed"] = "get_preferred_parametric failed"
507
+ return None
508
+ wl.wl_proxy_add_listener(desc, _CONN["desc_listener"], None)
509
+ info = None
510
+ try:
511
+ for _ in range(4):
512
+ wl.wl_display_roundtrip(display)
513
+ if _STATE["ready"] is not None:
514
+ break
515
+ if not _STATE["ready"]:
516
+ _STATE["preferred_query_failed"] = f"preferred description failed: {_STATE['failed']}"
517
+ return None
518
+ info = wl.wl_proxy_marshal_flags(desc, 1, ctypes.addressof(ifaces["info"]), ver, 0,
519
+ ctypes.c_void_p(None))
520
+ if not info:
521
+ _STATE["preferred_query_failed"] = "get_information failed"
522
+ return None
523
+ wl.wl_proxy_add_listener(info, _CONN["info_listener"], None)
524
+ for _ in range(4):
525
+ wl.wl_display_roundtrip(display)
526
+ if _STATE["info_done"]:
527
+ break
528
+ lums = _STATE["info_luminances"]
529
+ if not lums or lums[2] <= 0:
530
+ _STATE["preferred_query_failed"] = "preferred description carries no luminances"
531
+ return None
532
+ _STATE["preferred_reference"] = float(lums[2])
533
+ _STATE["preferred_luminances"] = (float(lums[0]), float(lums[1]), float(lums[2]))
534
+ _STATE["preferred_query_failed"] = None
535
+ return _STATE["preferred_reference"]
536
+ finally:
537
+ # The info object has no destroy request (it ends with `done`); the
538
+ # description is destroyed through special request 0.
539
+ if info:
540
+ wl.wl_proxy_destroy(info)
541
+ wl.wl_proxy_marshal_flags(desc, 0, ctypes.c_void_p(None), ver, 1)
542
+
543
+
544
+ def preferred_reference():
545
+ """The compositor's reference white for this surface in nits, or None."""
546
+ return _STATE["preferred_reference"]
547
+
548
+
549
+ def preferred_query_failed():
550
+ return _STATE["preferred_query_failed"]
551
+
552
+
553
+ def desired_reference() -> float:
554
+ """Reference white (nits) for the PQ tag + encode: the desktop's, when it
555
+ told us and `Toggles.HDR.follow_desktop_white`; else the toggle."""
556
+ from meltygui.core.runtime.toggles import Toggles
557
+ if _STATE["preferred_dirty"] and available():
558
+ try:
559
+ query_preferred()
560
+ except Exception as e:
561
+ _STATE["preferred_query_failed"] = f"{type(e).__name__}: {e}"
562
+ if Toggles.HDR.follow_desktop_white and _STATE["preferred_reference"]:
563
+ return float(_STATE["preferred_reference"])
564
+ return float(Toggles.HDR.pq_reference_nits)
565
+
566
+
567
+ def desired_luminances() -> tuple[float, float, float]:
568
+ """(min, max, reference) nits for the PQ tag. The range is the
569
+ DESKTOP's (its preferred description: the panel's peak, 1241 here), not
570
+ PQ's 0..10000: Hyprland tone-maps a surface whose declared max exceeds
571
+ the output's (getCMSettings needsTonemap, max >= dst * 1.01) — the
572
+ 10000-nit frog came out compressed under the panel's peak while
573
+ Chromium, which echoes the preferred range, showed it clipped at the
574
+ peak as it is (Lukas 09-11). Declaring the desktop's range makes the
575
+ compositor pass our nits through and clip at the panel, like Chromium."""
576
+ reference = desired_reference()
577
+ lums = _STATE["preferred_luminances"]
578
+ if lums and lums[1] > reference:
579
+ return (lums[0], lums[1], reference)
580
+ return (0.0, 10000.0, reference)
581
+
582
+
583
+ def applied():
584
+ return _STATE["applied"]
585
+
586
+
587
+ def sync(window=None) -> str | None:
588
+ """Once per frame: make the surface tag follow Toggles.HDR.output.
589
+ Cheap when nothing changed. Returns the applied mode."""
590
+ if not available():
591
+ return _STATE["applied"]
592
+ wanted = resolved_output()
593
+ lums = desired_luminances() if wanted == "pq" else None
594
+ if wanted == _STATE["wanted"] and lums == _STATE["synced_reference"]:
595
+ return _STATE["applied"]
596
+ _STATE["wanted"] = wanted
597
+ _STATE["synced_reference"] = lums
598
+ try:
599
+ if wanted == "pq":
600
+ if create_description(PRIMARIES_BT2020, TF_ST2084_PQ, reference_nits=lums[2],
601
+ max_nits=lums[1], min_nits=lums[0]) \
602
+ and set_surface_description():
603
+ _STATE["applied"] = "pq"
604
+ else:
605
+ print(f"wayland_color: PQ tag not applied: {last_error()}")
606
+ _STATE["wanted"] = None # retry next frame's toggle edge
607
+ else:
608
+ unset_surface_description()
609
+ _STATE["applied"] = "srgb"
610
+ except Exception as e:
611
+ _STATE["error"] = f"{type(e).__name__}: {e}"
612
+ print(f"wayland_color: {e}")
613
+ return _STATE["applied"]
614
+
615
+
616
+ def resolved_output() -> str:
617
+ """Toggles.HDR.output with "auto" resolved: PQ when the compositor offers
618
+ colour management, sRGB otherwise."""
619
+ from meltygui.core.runtime.toggles import Toggles
620
+ mode = Toggles.HDR.output
621
+ if mode == "auto":
622
+ return "pq" if available() else "srgb"
623
+ return "pq" if mode == "pq" else "srgb"
624
+
625
+
626
+ def attach_window(window) -> bool:
627
+ """attach() for the GLFW studio window."""
628
+ try:
629
+ if glfw.get_platform() != glfw.PLATFORM_WAYLAND:
630
+ _STATE["error"] = "not a Wayland session"
631
+ return False
632
+ return attach(glfw.get_wayland_display(), glfw.get_wayland_window(window))
633
+ except Exception as e:
634
+ _STATE["error"] = f"{type(e).__name__}: {e}"
635
+ return False
@@ -0,0 +1 @@
1
+ """Shared framework input machinery."""