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
meltygui/hdr_color.py ADDED
@@ -0,0 +1,757 @@
1
+ """Melty colour: extended-sRGB tuples in Python, HDR/P3 through imgui's u32.
2
+
3
+ The convention every colour in Melty follows:
4
+
5
+ * A colour TUPLE ``(r, g, b[, a])`` is EXTENDED sRGB: the sRGB transfer
6
+ curve, mirrored for negatives, with no ceiling. ``1.0`` is the desktop's
7
+ SDR reference white (what an untagged sRGB window shows as white), values
8
+ above it are brighter, negative components are colours outside the sRGB
9
+ gamut (a P3 red is ``(1.093, -0.227, -0.150)``, the same numbers CSS gives
10
+ for ``color(display-p3 1 0 0)`` in ``srgb``). Every existing colour stays
11
+ valid, and the helpers ``white(n)`` / ``p3(r, g, b)`` build the rest.
12
+
13
+ * The working space on the GPU is LINEAR scRGB: sRGB primaries, linear
14
+ light, fp16, negatives allowed. That is what the compositor (Hyprland's
15
+ ``ext_linear`` surfaces) composites natively.
16
+
17
+ * Between the two sits imgui's 32-bit vertex colour, the one narrow pipe.
18
+ ``pack_color`` encodes into it and the vertex shader (``GLSL_DECODE``)
19
+ decodes out of it; the two are the only code that knows the layout:
20
+
21
+ bit 31 : SDR bit — 1 = the RGB bytes are plain 8-bit sRGB in [0, 1]
22
+ (byte-identical to imgui's own packing, so an opaque
23
+ colour imgui converts itself — ``imgui.image``'s white
24
+ tint, ``0xFFFFFFFF`` — reads correctly), 0 = HDR: the
25
+ RGB bytes are DISPLAY P3 primaries on a log curve over
26
+ [0, Toggles.HDR.vertex_range] (both headroom AND gamut
27
+ ride the one flag; P3 contains sRGB so anything the
28
+ panel can show packs non-negative)
29
+ bits 24..30 : 7-bit alpha (128 levels, both modes)
30
+ bits 0..23 : R, G, B bytes (little-endian, R lowest — imgui's order)
31
+
32
+ imgui itself only ever touches the alpha BYTE of a packed colour (the
33
+ style-alpha multiply, the alpha-zero skip), never the RGB bytes, which is
34
+ what makes re-purposing them safe. An SDR colour keeps every one of its
35
+ 256 codes per channel; an HDR colour gets its own 256 codes over the
36
+ extended range instead of sharing them.
37
+
38
+ Style-table entries go through ``style_color`` so imgui's own float→u32
39
+ conversion reproduces these bytes exactly.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import math
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # The curve (extended sRGB) and primaries
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def srgb_to_linear(c: float) -> float:
51
+ """Extended sRGB decode: the sRGB curve, mirrored for negatives, no ceiling."""
52
+ s = -1.0 if c < 0.0 else 1.0
53
+ c = abs(c)
54
+ if c <= 0.04045:
55
+ return s * c / 12.92
56
+ return s * ((c + 0.055) / 1.055) ** 2.4
57
+
58
+
59
+ def linear_to_srgb(v: float) -> float:
60
+ """Extended sRGB encode: inverse of ``srgb_to_linear``."""
61
+ s = -1.0 if v < 0.0 else 1.0
62
+ v = abs(v)
63
+ if v <= 0.0031308:
64
+ return s * v * 12.92
65
+ return s * (1.055 * v ** (1.0 / 2.4) - 0.055)
66
+
67
+
68
+ def _mat_mul_vec(m, v):
69
+ return tuple(m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2] for i in range(3))
70
+
71
+
72
+ def _mat_mul(a, b):
73
+ return tuple(tuple(sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)) for i in range(3))
74
+
75
+
76
+ def _mat_inv(m):
77
+ (a, b, c), (d, e, f), (g, h, i) = m
78
+ det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
79
+ return (
80
+ ((e * i - f * h) / det, (c * h - b * i) / det, (b * f - c * e) / det),
81
+ ((f * g - d * i) / det, (a * i - c * g) / det, (c * d - a * f) / det),
82
+ ((d * h - e * g) / det, (b * g - a * h) / det, (a * e - b * d) / det),
83
+ )
84
+
85
+
86
+ # RGB → XYZ (D65) for the two primaries sets. P3 here is DISPLAY P3 (the
87
+ # sRGB curve on DCI-P3 primaries with a D65 white), what the panel and CSS mean.
88
+ _SRGB_TO_XYZ = ((0.4123908, 0.3575843, 0.1804808),
89
+ (0.2126390, 0.7151687, 0.0721923),
90
+ (0.0193308, 0.1191948, 0.9505322))
91
+ _P3_TO_XYZ = ((0.4865709, 0.2656677, 0.1982173),
92
+ (0.2289746, 0.6917385, 0.0792869),
93
+ (0.0000000, 0.0451134, 1.0439444))
94
+
95
+ SRGB_TO_P3 = _mat_mul(_mat_inv(_P3_TO_XYZ), _SRGB_TO_XYZ)
96
+ P3_TO_SRGB = _mat_mul(_mat_inv(_SRGB_TO_XYZ), _P3_TO_XYZ)
97
+
98
+
99
+ def linear_srgb_to_p3(rgb):
100
+ return _mat_mul_vec(SRGB_TO_P3, rgb)
101
+
102
+
103
+ def linear_p3_to_srgb(rgb):
104
+ return _mat_mul_vec(P3_TO_SRGB, rgb)
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Oklab: the perceptual space HDR colour SCALES are authored in
109
+ # ---------------------------------------------------------------------------
110
+ # Björn Ottosson's Oklab on LINEAR sRGB light. It is scale-invariant (no
111
+ # absolute nits: lightness is a cube root, so 16 × white sits at L = 2.52)
112
+ # and works on extended values (negative components = P3), which is what a
113
+ # ramp that keeps climbing past the desktop's white needs. Its cube-root
114
+ # lightness is a fair match for equal perceptual steps above white; it does
115
+ # NOT model the Hunt effect (bright colours reading more saturated), so a
116
+ # uniform-L ramp is "uniform" in the CIELAB sense, not a full HDR appearance
117
+ # model.
118
+
119
+ _OKLAB_M1 = ((0.4122214708, 0.5363325363, 0.0514459929),
120
+ (0.2119034982, 0.6806995451, 0.1073969566),
121
+ (0.0883024619, 0.2817188376, 0.6299787005))
122
+ _OKLAB_M2 = ((0.2104542553, 0.7936177850, -0.0040720468),
123
+ (1.9779984951, -2.4285922050, 0.4505937099),
124
+ (0.0259040371, 0.7827717662, -0.8086757660))
125
+ _OKLAB_M1_INV = _mat_inv(_OKLAB_M1)
126
+ _OKLAB_M2_INV = _mat_inv(_OKLAB_M2)
127
+
128
+
129
+ def _cbrt(x: float) -> float:
130
+ return math.copysign(abs(x) ** (1.0 / 3.0), x)
131
+
132
+
133
+ def linear_to_oklab(rgb):
134
+ """Linear sRGB (extended) → Oklab ``(L, a, b)``; white (1, 1, 1) is L = 1."""
135
+ lms = _mat_mul_vec(_OKLAB_M1, rgb)
136
+ return _mat_mul_vec(_OKLAB_M2, tuple(_cbrt(c) for c in lms))
137
+
138
+
139
+ def oklab_to_linear(lab):
140
+ """Oklab ``(L, a, b)`` → linear sRGB (extended)."""
141
+ lms = _mat_mul_vec(_OKLAB_M2_INV, lab)
142
+ return _mat_mul_vec(_OKLAB_M1_INV, tuple(c * c * c for c in lms))
143
+
144
+
145
+ def oklab_hue(rgb) -> float:
146
+ """Oklab hue angle (radians) of a linear sRGB colour."""
147
+ _, a, b = linear_to_oklab(rgb)
148
+ return math.atan2(b, a)
149
+
150
+
151
+ def oklab_max_chroma(lightness: float, hue: float, peak: float = 1.0,
152
+ steps: int = 40) -> float:
153
+ """The largest Oklab chroma at ``(lightness, hue)`` whose colour still
154
+ fits the DISPLAY-P3 box ``[0, peak]³`` in linear light — ``peak`` = the
155
+ brightest white the scale may reach, in multiples of the desktop's
156
+ white (1 = the SDR gamut). This is the gamut boundary an HDR colour
157
+ scale rides: it pinches to grey at black AND at the peak, so a ramp of
158
+ max-chroma colours whitens PROGRESSIVELY toward its top instead of
159
+ switching to white. Bisection: chroma grows monotonically out of the
160
+ box along a ray of fixed lightness and hue."""
161
+ if lightness <= 0.0 or lightness >= _cbrt(peak):
162
+ return 0.0
163
+ ca, sa = math.cos(hue), math.sin(hue)
164
+
165
+ def fits(chroma):
166
+ p3_rgb = linear_srgb_to_p3(oklab_to_linear((lightness, chroma * ca, chroma * sa)))
167
+ return all(-1e-6 <= c <= peak + 1e-6 for c in p3_rgb)
168
+
169
+ lo, hi = 0.0, 1.0 + _cbrt(peak)
170
+ if fits(hi):
171
+ return hi
172
+ for _ in range(steps):
173
+ mid = (lo + hi) * 0.5
174
+ if fits(mid):
175
+ lo = mid
176
+ else:
177
+ hi = mid
178
+ return lo
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Helpers for writing colours
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def white(scale: float):
187
+ """A neutral ``scale`` × the SDR reference white, as an extended-sRGB
188
+ tuple: ``white(1)`` is sRGB white, ``white(4)`` is 1000 nits on a 250-nit
189
+ desktop. The helper for "how bright", independent of hue."""
190
+ e = linear_to_srgb(float(scale))
191
+ return (e, e, e)
192
+
193
+
194
+ def p3(r: float, g: float, b: float, scale: float = 1.0):
195
+ """A Display-P3 colour (P3 primaries, sRGB curve — the CSS ``display-p3``
196
+ triple) as an extended-sRGB tuple. ``scale`` multiplies the linear light,
197
+ so ``p3(1, 0, 0, scale=16)`` is P3 red at 16 × reference white."""
198
+ lin = tuple(srgb_to_linear(c) * scale for c in (r, g, b))
199
+ return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin))
200
+
201
+
202
+ def scale_saturation(saturation: float, factor: float) -> float:
203
+ """``saturation * factor`` for an EXTENDED-HSV saturation (colorsys on an
204
+ extended-sRGB tuple: s > 1 means a channel below 0 — outside the sRGB
205
+ gamut), capped at the colour's OWN gamut edge: 1.0 for a tint inside
206
+ sRGB, its own s for a wider one. A boost factor above 1 (the bg ramps'
207
+ 1.1, the gutter's 1.05) must not push an sRGB primary out past P3 —
208
+ the packer clips per channel in linear P3, so an sRGB red tint and a
209
+ P3 red tint landed on the SAME P3-edge colour and P3 tints looked no
210
+ more saturated than sRGB ones (09-07). Before HDR imgui's u32 clamp
211
+ capped every boost at the sRGB edge; this keeps that look for SDR
212
+ tints and lets a wide tint keep exactly the chroma it was given."""
213
+ if saturation <= 0.0:
214
+ return 0.0
215
+ return min(max(1.0, saturation), saturation * factor)
216
+
217
+
218
+ def scale(color, k: float):
219
+ """``color`` (an extended-sRGB tuple) with its linear light multiplied by
220
+ ``k``; alpha, if present, is kept."""
221
+ rgb = tuple(linear_to_srgb(srgb_to_linear(c) * k) for c in color[:3])
222
+ return rgb + tuple(color[3:])
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # The u32 pipe
227
+ # ---------------------------------------------------------------------------
228
+
229
+ SDR_BIT = 0x80000000
230
+ ALPHA_MASK = 0x7F000000
231
+ RGB_MASK = 0x00FFFFFF
232
+
233
+
234
+ def _curve_params():
235
+ """(range, octaves) of the HDR byte curve — Toggles.HDR, read live so a
236
+ toggle edit reaches the packer and the shaders (uniforms) together."""
237
+ from meltygui.core.runtime.toggles import Toggles
238
+ return float(Toggles.HDR.vertex_range), float(Toggles.HDR.vertex_octaves)
239
+
240
+
241
+ def _encode_hdr_byte(v: float, rng: float, octaves: float) -> int:
242
+ """Linear P3 component → byte on the log curve: code 0 is exactly 0,
243
+ codes 1..255 span [range · 2^-octaves, range] evenly in log2."""
244
+ if v <= 0.0:
245
+ return 0
246
+ t = 1.0 + math.log2(v / rng) / octaves # 1.0 at the ceiling
247
+ code = int(1.0 + 254.0 * t + 0.5)
248
+ return 0 if code < 1 else (255 if code > 255 else code)
249
+
250
+
251
+ def _decode_hdr_byte(code: int, rng: float, octaves: float) -> float:
252
+ if code <= 0:
253
+ return 0.0
254
+ return rng * 2.0 ** (octaves * ((code - 1) / 254.0 - 1.0))
255
+
256
+
257
+ # (r, g, b, alpha7) -> packed, HDR branch only. Cleared when the curve
258
+ # toggles change (clear_cache) - hotswap keeps the dict.
259
+ _HDR_PACK_CACHE = globals().get("_HDR_PACK_CACHE", {})
260
+
261
+
262
+ def clear_cache() -> None:
263
+ """Forget memoized HDR packs — call after editing Toggles.HDR.vertex_*."""
264
+ _HDR_PACK_CACHE.clear()
265
+
266
+
267
+ def _is_sdr(r, g, b) -> bool:
268
+ return 0.0 <= r <= 1.0 and 0.0 <= g <= 1.0 and 0.0 <= b <= 1.0
269
+
270
+
271
+ def pack_color(r: float, g: float, b: float, a: float = 1.0) -> int:
272
+ """Extended-sRGB components → imgui u32. The drop-in for
273
+ ``imgui.get_color_u32_rgba``: identical bytes for a colour inside [0, 1]
274
+ (bar the 7-bit alpha), the HDR layout for anything outside it."""
275
+ if a != a:
276
+ a = 0.0
277
+ alpha7 = int(a * 127.0 + 0.5)
278
+ alpha7 = 0 if alpha7 < 0 else (127 if alpha7 > 127 else alpha7)
279
+ if _is_sdr(r, g, b):
280
+ return (SDR_BIT | (alpha7 << 24)
281
+ | (int(b * 255.0 + 0.5) << 16)
282
+ | (int(g * 255.0 + 0.5) << 8)
283
+ | int(r * 255.0 + 0.5))
284
+ # Tiny negative noise from colour math (a computed purple tint like
285
+ # (0, -0.005, -0.009)) is not a wide-gamut intent: snap it to SDR.
286
+ if r >= -0.02 and g >= -0.02 and b >= -0.02 and r <= 1.0 and g <= 1.0 and b <= 1.0:
287
+ return pack_color(max(0.0, r), max(0.0, g), max(0.0, b), a)
288
+ # The HDR path is ~25 us of matrix + log math. HDR colours are few
289
+ # distinct values but many samples, so memoize on the exact inputs.
290
+ key = (r, g, b, alpha7)
291
+ packed = _HDR_PACK_CACHE.get(key)
292
+ if packed is not None:
293
+ return packed
294
+ rng, octaves = _curve_params()
295
+ lin = linear_srgb_to_p3((srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)))
296
+ codes = [_encode_hdr_byte(min(rng, max(0.0, c)), rng, octaves) for c in lin]
297
+ packed = (alpha7 << 24) | (codes[2] << 16) | (codes[1] << 8) | codes[0]
298
+ if len(_HDR_PACK_CACHE) > 4096:
299
+ _HDR_PACK_CACHE.clear()
300
+ _HDR_PACK_CACHE[key] = packed
301
+ return packed
302
+
303
+
304
+ def unpack_color(packed: int):
305
+ """imgui u32 → extended-sRGB ``(r, g, b, a)``. Inverse of ``pack_color``
306
+ up to the byte quantization."""
307
+ packed &= 0xFFFFFFFF
308
+ a = ((packed >> 24) & 0x7F) / 127.0
309
+ rb, gb, bb = packed & 0xFF, (packed >> 8) & 0xFF, (packed >> 16) & 0xFF
310
+ if packed & SDR_BIT:
311
+ return (rb / 255.0, gb / 255.0, bb / 255.0, a)
312
+ rng, octaves = _curve_params()
313
+ lin_p3 = tuple(_decode_hdr_byte(c, rng, octaves) for c in (rb, gb, bb))
314
+ return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin_p3)) + (a,)
315
+
316
+
317
+ def is_sdr_packed(packed: int) -> bool:
318
+ return bool(packed & SDR_BIT)
319
+
320
+
321
+ def with_alpha(packed: int, a: float) -> int:
322
+ """``packed`` with its alpha replaced — the flag and RGB bytes kept."""
323
+ alpha7 = int(a * 127.0 + 0.5)
324
+ alpha7 = 0 if alpha7 < 0 else (127 if alpha7 > 127 else alpha7)
325
+ return (packed & (SDR_BIT | RGB_MASK)) | (alpha7 << 24)
326
+
327
+
328
+ def packed_alpha(packed: int) -> float:
329
+ return ((packed >> 24) & 0x7F) / 127.0
330
+
331
+
332
+ def scale_alpha(packed: int, k: float) -> int:
333
+ """``packed`` with its alpha multiplied by ``k`` (the fade helper)."""
334
+ return with_alpha(packed, packed_alpha(packed) * k)
335
+
336
+
337
+ def style_color(r: float, g: float, b: float, a: float = 1.0):
338
+ """The float4 to store in an imgui STYLE entry for this colour: imgui
339
+ converts style floats to u32 with ``round(x * 255)`` per channel, so the
340
+ stored floats are the packed bytes / 255 and the conversion lands on
341
+ exactly the bytes ``pack_color`` would produce."""
342
+ packed = pack_color(r, g, b, a)
343
+ return ((packed & 0xFF) / 255.0, ((packed >> 8) & 0xFF) / 255.0,
344
+ ((packed >> 16) & 0xFF) / 255.0, ((packed >> 24) & 0xFF) / 255.0)
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # GLSL
349
+ # ---------------------------------------------------------------------------
350
+
351
+ # Splice this into any vertex shader that reads imgui's `Color` attribute
352
+ # (normalized ubytes). `MeltyHdrRange` / `MeltyHdrOctaves` are uniforms so a
353
+ # Toggles.HDR edit reaches the shader live - set them with
354
+ # `set_decode_uniforms(program)` each draw.
355
+ GLSL_DECODE = """
356
+ uniform float MeltyHdrRange; // Toggles.HDR.vertex_range
357
+ uniform float MeltyHdrOctaves; // Toggles.HDR.vertex_octaves
358
+ const mat3 MELTY_P3_TO_SRGB = mat3(%s);
359
+
360
+ vec3 melty_srgb_to_linear(vec3 c) {
361
+ vec3 lo = c / 12.92;
362
+ vec3 hi = pow((c + 0.055) / 1.055, vec3(2.4));
363
+ return mix(lo, hi, step(0.04045, c));
364
+ }
365
+
366
+ // imgui vertex colour (normalized ubytes) -> linear scRGB + alpha.
367
+ vec4 melty_decode_color(vec4 c) {
368
+ float abyte = c.a * 255.0;
369
+ bool sdr = abyte >= 127.5;
370
+ float alpha = (sdr ? abyte - 128.0 : abyte) / 127.0;
371
+ vec3 rgb;
372
+ if (sdr) {
373
+ rgb = melty_srgb_to_linear(c.rgb);
374
+ } else {
375
+ vec3 code = c.rgb * 255.0;
376
+ vec3 v = MeltyHdrRange * exp2(MeltyHdrOctaves * ((code - 1.0) / 254.0 - 1.0));
377
+ v *= step(0.5, code); // code 0 is exactly 0
378
+ rgb = MELTY_P3_TO_SRGB * v;
379
+ }
380
+ return vec4(rgb, alpha);
381
+ }
382
+
383
+ // The VARYING form: rgb PREMULTIPLIED by alpha. imgui's anti-aliased fills
384
+ // and strokes give the feather's outer vertices the same RGB bytes with an
385
+ // alpha BYTE of 0 — which clears the SDR bit, so decoded per vertex those
386
+ // bytes read as an HDR log code (a 61/255 grey becomes linear 0.11, four
387
+ // times brighter) and the rasterizer interpolates that brightness across
388
+ // the feather: a light 1-px fringe around every rounded rect (09-07). A
389
+ // zero-alpha vertex must contribute NOTHING to the colour, which is what
390
+ // premultiplied interpolation does; the fragment stage divides the alpha
391
+ // back out (melty_unpremultiply) so the blend stays straight-alpha.
392
+ vec4 melty_decode_premultiplied(vec4 c) {
393
+ vec4 d = melty_decode_color(c);
394
+ return vec4(d.rgb * d.a, d.a);
395
+ }
396
+ """
397
+
398
+ # Fragment-side inverse of melty_decode_premultiplied (the fragment shaders
399
+ # don't include GLSL_DECODE, so this is spliced on its own).
400
+ GLSL_UNPREMULTIPLY = """
401
+ vec4 melty_unpremultiply(vec4 f) {
402
+ return vec4(f.rgb / max(f.a, 1e-6), f.a);
403
+ }
404
+ """
405
+
406
+
407
+ def _glsl_mat3(m) -> str:
408
+ # GLSL mat3 is column-major: list column by column.
409
+ cols = [[m[r][c] for r in range(3)] for c in range(3)]
410
+ return ", ".join(f"{v:.7f}" for col in cols for v in col)
411
+
412
+
413
+ GLSL_DECODE = GLSL_DECODE % _glsl_mat3(P3_TO_SRGB)
414
+
415
+ # Text brightness ceiling (Toggles.HDR.text_max_stops): scales the glyph's
416
+ # colour down uniformly - hue and saturation kept, P3 negatives ride along -
417
+ # until its brightest channel sits at MeltyTextMax (multiples of reference
418
+ # white). Spliced into the fragment stage of the draw-list renderer (glyph
419
+ # pixels only) and the text-texture bake; the uniform rides with
420
+ # set_decode_uniforms so both stay live with the toggle.
421
+ GLSL_TEXT_CLAMP = """
422
+ uniform float MeltyTextMax; // 2^Toggles.HDR.text_max_stops
423
+
424
+ vec3 melty_clamp_text(vec3 c) {
425
+ float peak = max(max(c.r, c.g), c.b);
426
+ return peak > MeltyTextMax ? c * (MeltyTextMax / peak) : c;
427
+ }
428
+ """
429
+
430
+
431
+ def set_decode_uniforms(program: int) -> None:
432
+ """Set the curve uniforms `GLSL_DECODE` declares on a bound program."""
433
+ import OpenGL.GL as gl
434
+ rng, octaves = _curve_params()
435
+ loc = gl.glGetUniformLocation(program, "MeltyHdrRange")
436
+ if loc >= 0:
437
+ gl.glUniform1f(loc, rng)
438
+ loc = gl.glGetUniformLocation(program, "MeltyHdrOctaves")
439
+ if loc >= 0:
440
+ gl.glUniform1f(loc, octaves)
441
+ loc = gl.glGetUniformLocation(program, "MeltyTextMax") # GLSL_TEXT_CLAMP
442
+ if loc >= 0:
443
+ from meltygui.core.runtime.toggles import Toggles
444
+ gl.glUniform1f(loc, float(2.0 ** float(Toggles.HDR.text_max_stops)))
445
+
446
+
447
+ # Linear scRGB -> display encodings, for the presentation pass.
448
+ GLSL_ENCODE = """
449
+ vec3 melty_linear_to_srgb(vec3 v) {
450
+ v = clamp(v, 0.0, 1.0);
451
+ vec3 lo = v * 12.92;
452
+ vec3 hi = 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055;
453
+ return mix(lo, hi, step(0.0031308, v));
454
+ }
455
+
456
+ // BT.709/sRGB primaries -> BT.2020 primaries (linear, D65).
457
+ const mat3 MELTY_SRGB_TO_BT2020 = mat3(
458
+ 0.6274040, 0.0690970, 0.0163916,
459
+ 0.3292820, 0.9195400, 0.0880132,
460
+ 0.0433136, 0.0113612, 0.8955950);
461
+
462
+ // SMPTE ST 2084 (PQ) encode of linear light in NITS.
463
+ vec3 melty_pq_encode(vec3 nits) {
464
+ const float m1 = 0.1593017578125, m2 = 78.84375;
465
+ const float c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;
466
+ vec3 y = clamp(nits / 10000.0, 0.0, 1.0);
467
+ vec3 yp = pow(y, vec3(m1));
468
+ return pow((c1 + c2 * yp) / (1.0 + c3 * yp), vec3(m2));
469
+ }
470
+ """
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # The wide-gamut picker's colour model (draw_color_picker's "Wide" tab)
475
+ # ---------------------------------------------------------------------------
476
+ #
477
+ # The picker works in DISPLAY P3 HSV plus an EXPOSURE: (h, s, v) is the
478
+ # classic square on P3 primaries (P3 saturates further than sRGB), and
479
+ # exposure is a linear multiplier above white - v = 1 with exposure 4 is
480
+ # white(4). Every displayable colour has one such tuple: the P3 gamut
481
+ # contains sRGB, so anything the display can show packs non-negative in P3.
482
+
483
+ import colorsys as _colorsys
484
+
485
+
486
+ def p3_hsv_from_extended(r: float, g: float, b: float):
487
+ """Extended-sRGB → (h, s, v, exposure) in P3 HSV. A component outside P3
488
+ (negative after the primaries conversion) clips to the P3 edge."""
489
+ lin = linear_srgb_to_p3((srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)))
490
+ lin = tuple(max(0.0, c) for c in lin)
491
+ peak = max(lin)
492
+ exposure = peak if peak > 1.0 else 1.0
493
+ enc = tuple(linear_to_srgb(c / exposure) for c in lin) # Display P3 uses the sRGB curve
494
+ h, s, v = _colorsys.rgb_to_hsv(*enc)
495
+ return h, s, v, exposure
496
+
497
+
498
+ def extended_from_p3_hsv(h: float, s: float, v: float, exposure: float = 1.0):
499
+ """(h, s, v, exposure) in P3 HSV → extended-sRGB (r, g, b)."""
500
+ enc = _colorsys.hsv_to_rgb(h, s, v)
501
+ lin = tuple(srgb_to_linear(c) * exposure for c in enc)
502
+ return tuple(linear_to_srgb(c) for c in linear_p3_to_srgb(lin))
503
+
504
+
505
+ def wide_square_linear(hue: float, size: int, top_fraction: float, max_stops: float):
506
+ """The picker square for one hue as a (size, size, 4) float32 array of
507
+ LINEAR scRGB (row 0 = top). X is P3 saturation 0→1. The top
508
+ `top_fraction` of the rows is exposure: 2^max_stops at the top down to
509
+ 1.0 (white) at the seam; the rest is the classic value axis 1→0."""
510
+ import numpy as np
511
+ n = int(size)
512
+ top = max(1, int(round(n * top_fraction)))
513
+ s = np.linspace(0.0, 1.0, n, dtype=np.float32)[None, :, None] # columns
514
+ rows = np.arange(n, dtype=np.float32)
515
+ v = np.ones(n, dtype=np.float32)
516
+ exposure = np.ones(n, dtype=np.float32)
517
+ exposure[:top] = 2.0 ** (max_stops * (1.0 - rows[:top] / top))
518
+ v[top:] = 1.0 - (rows[top:] - top) / max(1, n - top - 1)
519
+ v = np.clip(v, 0.0, 1.0)[:, None, None]
520
+ exposure = exposure[:, None, None]
521
+ hue_rgb = np.asarray(_colorsys.hsv_to_rgb(hue, 1.0, 1.0), dtype=np.float32)[None, None, :]
522
+ enc = v * (1.0 - s * (1.0 - hue_rgb)) # HSV at fixed hue, P3-encoded
523
+ lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4) * exposure
524
+ m = np.asarray(P3_TO_SRGB, dtype=np.float32)
525
+ out = np.empty((n, n, 4), dtype=np.float32)
526
+ out[..., :3] = lin @ m.T
527
+ out[..., 3] = 1.0
528
+ return out
529
+
530
+
531
+ # ---------------------------------------------------------------------------
532
+ # The "sRGB+" tab: the classic sRGB square, extended past its right edge
533
+ # ---------------------------------------------------------------------------
534
+ #
535
+ # The square is the classic sRGB HSV square, untouched. Each of its rows
536
+ # (an sRGB value v) ends at the sRGB gamut edge (hue, 1, v); the extension
537
+ # to its right carries that edge carried on in DISPLAY P3: same P3 hue,
538
+ # saturation from the edge's own P3 saturation up to 1 and value from the
539
+ # edge's P3 value up to the row's v - so the strip is colour-continuous
540
+ # at the seam, the top row's right edge is a pure P3 primary, and the
541
+ # extension shows all the colours sRGB cannot reach. Exposure
542
+ # (brightness above white) is not on the square: it rides through unchanged.
543
+
544
+
545
+ def srgb_extension_edge(hue: float, v: float):
546
+ """The sRGB gamut edge colour (hue, s=1, v) in P3 HSV: (hp, sp, vp)."""
547
+ r, g, b = _colorsys.hsv_to_rgb(hue, 1.0, v)
548
+ hp, sp, vp, _exposure = p3_hsv_from_extended(r, g, b)
549
+ return hp, sp, vp
550
+
551
+
552
+ def extended_from_srgb_extension(hue: float, s: float, v: float, x: float, exposure: float = 1.0):
553
+ """sRGB+ coordinates → extended-sRGB (r, g, b). (hue, s, v) is the
554
+ classic sRGB HSV square; x in [0, 1] is how far into the extension the
555
+ marker sits (0 = the square's right edge, only meaningful at s = 1)."""
556
+ if x <= 0.0:
557
+ rgb = _colorsys.hsv_to_rgb(hue, s, v)
558
+ return tuple(linear_to_srgb(srgb_to_linear(c) * exposure) for c in rgb)
559
+ hp, sp, vp = srgb_extension_edge(hue, v)
560
+ x = min(x, 1.0)
561
+ return extended_from_p3_hsv(hp, sp + (1.0 - sp) * x, vp + (v - vp) * x, exposure)
562
+
563
+
564
+ def srgb_extension_coords(r: float, g: float, b: float, hue_hint: float = None, steps: int = 6):
565
+ """Extended-sRGB → (hue, s, v, x, exposure) of the sRGB+ strip, the
566
+ inverse of extended_from_srgb_extension. Inside sRGB it is the classic
567
+ HSV with x = 0. Outside, s = 1: for a hue, the row v is the bisection
568
+ root of the extension's P3 value (with x fixed by the P3 saturation)
569
+ against the target's, and the hue is corrected by a few secant steps
570
+ toward the target's P3 hue. hue_hint seeds that search (the cached hue keeps the marker
571
+ steady when the colour is gray)."""
572
+ lin = (srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b))
573
+ peak = max(lin)
574
+ srgb_exposure = peak if peak > 1.0 else 1.0
575
+ norm = tuple(c / srgb_exposure for c in lin)
576
+ if all(-1e-6 <= c <= 1.0 + 1e-6 for c in norm):
577
+ enc = tuple(linear_to_srgb(min(1.0, max(0.0, c))) for c in norm)
578
+ h, s, v = _colorsys.rgb_to_hsv(*enc)
579
+ return h, s, v, 0.0, srgb_exposure
580
+ hp, sp, vp, exposure = p3_hsv_from_extended(r, g, b)
581
+ vp_abs_lin = srgb_to_linear(vp) * exposure # the P3 peak in absolute linear light
582
+ vp_abs = linear_to_srgb(vp_abs_lin) # ... and sRGB-encoded (the HSV value scale)
583
+ if hue_hint is None:
584
+ enc = tuple(linear_to_srgb(min(1.0, max(0.0, c / exposure))) for c in lin)
585
+ hue_hint = _colorsys.rgb_to_hsv(*enc)[0]
586
+
587
+ def solve_row(h):
588
+ """(v, x, exposure) for hue h: x is fixed per row by the P3
589
+ saturation; the row is the bisection root of the extension's P3
590
+ value against the target's ABSOLUTE peak — an HDR colour takes the
591
+ SDR row that reaches it when one does, else the top row with the
592
+ rest as exposure (exposure is a free split of the same colour)."""
593
+ def at(v):
594
+ _hp, sp_e, vp_e = srgb_extension_edge(h, v)
595
+ x = min(max((sp - sp_e) / max(1e-6, 1.0 - sp_e), 0.0), 1.0)
596
+ return vp_e + (v - vp_e) * x, x
597
+ top_value, top_x = at(1.0)
598
+ if top_value <= vp_abs:
599
+ # Top row + exposure. HSV saturation is not invariant under a
600
+ # linear scale (the sRGB curve's offset), and the target's sp
601
+ # was computed from the colour normalized to its peak - so find the
602
+ # x whose top-row colour, normalized the same way, has that
603
+ # saturation (monotone in x), and take the exposure from the
604
+ # ratio of their peaks.
605
+ hp_e, sp_e, vp_e = srgb_extension_edge(h, 1.0)
606
+
607
+ def normalized(x):
608
+ rgb = _colorsys.hsv_to_rgb(hp_e, sp_e + (1.0 - sp_e) * x, vp_e + (1.0 - vp_e) * x)
609
+ lin = tuple(srgb_to_linear(c) for c in rgb)
610
+ peak = max(lin)
611
+ enc = tuple(linear_to_srgb(c / max(1e-9, peak)) for c in lin)
612
+ return _colorsys.rgb_to_hsv(*enc)[1], peak
613
+ lo, hi = 0.0, 1.0
614
+ for _ in range(20):
615
+ mid = 0.5 * (lo + hi)
616
+ if normalized(mid)[0] < sp:
617
+ lo = mid
618
+ else:
619
+ hi = mid
620
+ x = 0.5 * (lo + hi)
621
+ return 1.0, x, vp_abs_lin / max(1e-9, normalized(x)[1])
622
+ lo, hi = 0.0, 1.0
623
+ for _ in range(18):
624
+ mid = 0.5 * (lo + hi)
625
+ if at(mid)[0] < vp_abs:
626
+ lo = mid
627
+ else:
628
+ hi = mid
629
+ v = 0.5 * (lo + hi)
630
+ return v, at(v)[1], 1.0
631
+
632
+ def wrap(d):
633
+ return (d + 0.5) % 1.0 - 0.5
634
+
635
+ def residual(h):
636
+ v, x, e = solve_row(h)
637
+ return wrap(hp - srgb_extension_edge(h, v)[0]), v, x, e
638
+
639
+ h = hue_hint
640
+ delta, v, x, e = residual(h)
641
+ for _ in range(steps):
642
+ if abs(delta) < 1e-7:
643
+ break
644
+ # secant step: the sRGB→P3 hue slope is ~0.5 around green, ~1 elsewhere
645
+ probe = 1e-3
646
+ slope = wrap(srgb_extension_edge(h + probe, v)[0] - srgb_extension_edge(h, v)[0]) / probe
647
+ h = (h + delta / min(max(slope, 0.25), 4.0)) % 1.0
648
+ delta, v, x, e = residual(h)
649
+ if abs(delta) > 1e-4:
650
+ # The secant wandered (a poor hint across a hue sector): bracket the
651
+ # sign change of the residual over the hue circle and bisect it -
652
+ # the sRGB→P3 hue map is monotone, so exactly one bracket holds it.
653
+ n = 24
654
+ samples = [(i / n, residual(i / n)[0]) for i in range(n)]
655
+ lo, hi = None, None
656
+ for i in range(n):
657
+ h0, d0 = samples[i]
658
+ h1, d1 = samples[(i + 1) % n]
659
+ if d0 >= 0.0 > d1 or (d0 >= 0.0 and d1 >= 0.0 and d1 < d0 - 0.5):
660
+ lo, hi = h0, h0 + 1.0 / n
661
+ break
662
+ if lo is None:
663
+ lo, hi = 0.0, 1.0
664
+ for _ in range(22):
665
+ mid = 0.5 * (lo + hi)
666
+ if residual(mid % 1.0)[0] >= 0.0:
667
+ lo = mid
668
+ else:
669
+ hi = mid
670
+ h = (0.5 * (lo + hi)) % 1.0
671
+ _d, v, x, e = residual(h)
672
+ return h, 1.0, v, x, e
673
+
674
+
675
+ def srgb_extension_linear(hue: float, rows: int, cols: int):
676
+ """The extension strip for one sRGB hue as a (rows, cols, 4) float32
677
+ array of LINEAR scRGB (row 0 = top = v 1, bottom = black; column 0 =
678
+ the sRGB edge, last column = P3 saturation 1 at the row's value)."""
679
+ import numpy as np
680
+ n_rows, n_cols = int(rows), int(cols)
681
+ x = np.linspace(0.0, 1.0, n_cols, dtype=np.float32)[None, :, None]
682
+ v_row = np.empty(n_rows, dtype=np.float32)
683
+ hp = np.empty(n_rows, dtype=np.float32)
684
+ sp = np.empty(n_rows, dtype=np.float32)
685
+ vp = np.empty(n_rows, dtype=np.float32)
686
+ for i in range(n_rows):
687
+ v_row[i] = 1.0 - i / max(1, n_rows - 1)
688
+ hp[i], sp[i], vp[i] = srgb_extension_edge(hue, float(v_row[i]))
689
+ hue_rgb = np.asarray([_colorsys.hsv_to_rgb(float(h), 1.0, 1.0) for h in hp],
690
+ dtype=np.float32)[:, None, :] # per-row P3 hue
691
+ s = sp[:, None, None] + (1.0 - sp[:, None, None]) * x # P3 saturation past the edge
692
+ vv = vp[:, None, None] + (v_row[:, None, None] - vp[:, None, None]) * x # P3 value up to the row's v
693
+ enc = vv * (1.0 - s * (1.0 - hue_rgb)) # HSV at that position, P3-encoded
694
+ lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4)
695
+ m = np.asarray(P3_TO_SRGB, dtype=np.float32)
696
+ out = np.empty((n_rows, n_cols, 4), dtype=np.float32)
697
+ out[..., :3] = lin @ m.T
698
+ out[..., 3] = 1.0
699
+ return out
700
+
701
+
702
+ def srgb_plus_linear(hue: float, square: int, ext: int, band: int, max_stops: float):
703
+ """The sRGB+ tab's whole picking area for one hue as a
704
+ (band + square, square + ext, 4) float32 array of LINEAR scRGB: the
705
+ classic sRGB square (rows band.., cols ..square), the P3 extension to
706
+ its right (srgb_extension_linear), and above both an exposure band —
707
+ the top row (v = 1) of each lifted by 2^max_stops at the top down to
708
+ 1.0 at the seam, so the band's bottom row IS the square's top row."""
709
+ import numpy as np
710
+ n_sq, n_ext, n_band = int(square), int(ext), int(band)
711
+ out = np.empty((n_band + n_sq, n_sq + n_ext, 4), dtype=np.float32)
712
+ out[..., 3] = 1.0
713
+ # the classic square, sRGB-encoded (row 0 = v 1, col 0 = s 0)
714
+ s_axis = np.linspace(0.0, 1.0, n_sq, dtype=np.float32)[None, :, None]
715
+ v_axis = np.linspace(1.0, 0.0, n_sq, dtype=np.float32)[:, None, None]
716
+ hue_rgb = np.asarray(_colorsys.hsv_to_rgb(hue, 1.0, 1.0), dtype=np.float32)[None, None, :]
717
+ enc = v_axis * (1.0 - s_axis * (1.0 - hue_rgb))
718
+ square_lin = np.where(enc <= 0.04045, enc / 12.92, ((enc + 0.055) / 1.055) ** 2.4)
719
+ out[n_band:, :n_sq, :3] = square_lin
720
+ if n_ext:
721
+ out[n_band:, n_sq:, :] = srgb_extension_linear(hue, n_sq, n_ext)
722
+ if n_band:
723
+ rows = np.arange(n_band, dtype=np.float32)
724
+ exposure = (2.0 ** (max_stops * (1.0 - rows / n_band)))[:, None, None]
725
+ out[:n_band, :, :3] = out[n_band, :, :3][None, :, :] * exposure
726
+ return out
727
+
728
+
729
+ def srgb_saturation_limit(hue: float, v: float, steps: int = 14) -> float:
730
+ """Largest P3 saturation at (hue, v) whose colour still sits inside the
731
+ sRGB gamut (bisection; 1.0 when every saturation fits, e.g. at black)."""
732
+ def inside(s):
733
+ lin = linear_p3_to_srgb(tuple(srgb_to_linear(c) for c in _colorsys.hsv_to_rgb(hue, s, v)))
734
+ return all(-1e-4 <= c <= 1.0 + 1e-4 for c in lin)
735
+ if inside(1.0):
736
+ return 1.0
737
+ lo, hi = 0.0, 1.0
738
+ for _ in range(steps):
739
+ mid = 0.5 * (lo + hi)
740
+ if inside(mid):
741
+ lo = mid
742
+ else:
743
+ hi = mid
744
+ return lo
745
+
746
+
747
+ def srgb_region_outline(hue: float, top_fraction: float, samples: int = 32):
748
+ """The sRGB-reachable region of the wide square for one hue, as a list
749
+ of (x, y) fractions of the square: along the white seam from the left
750
+ edge to the gamut edge, then down the gamut edge to black."""
751
+ points = [(0.0, top_fraction)]
752
+ for i in range(samples + 1):
753
+ v = 1.0 - i / samples
754
+ x = srgb_saturation_limit(hue, v)
755
+ y = top_fraction + (1.0 - v) * (1.0 - top_fraction)
756
+ points.append((x, y))
757
+ return points