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,639 @@
1
+ import ctypes
2
+ import math
3
+ import sys
4
+ import time
5
+ from dataclasses import dataclass, field, replace
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Optional, Tuple
9
+
10
+ import meltygui_imgui as imgui
11
+ import numpy as np
12
+
13
+ from meltygui.state.model_enums import RelaxedEnum
14
+
15
+ _RESOURCES = Path(__file__).parent.parent.parent / "resources"
16
+ _DEJAVU_SANS = str(_RESOURCES / "dejavu" / "DejaVuSans.ttf")
17
+
18
+ # Font Awesome 5+ private use range; trailing 0 terminates the imgui's list.
19
+ _FA_ICON_RANGE: Tuple[int, ...] = (0xF000, 0xFFFF, 0)
20
+
21
+ # Glyph ranges for a monospace terminal font: Latin plus the box-drawing, block,
22
+ # shape, arrow, and dingbat ranges that TUIs (Crude Code, vim, ...) draw their
23
+ # borders/spinners with. Specifying ranges replaces imgui's ASCII-only default, so
24
+ # Latin (0x0020-0x00FF) is listed first. JetBrains Mono covers all of these. Trailing
25
+ # 0 terminates the list.
26
+ _MONO_TUI_RANGE: Tuple[int, ...] = (
27
+ 0x0020, 0x00FF, # ASCII Latin + Latin-1 Supplement
28
+ 0x2010, 0x2027, # general punctuation (dashes, smart quotes, ellipsis)
29
+ 0x2190, 0x21FF, # arrows
30
+ 0x2500, 0x257F, # box drawing
31
+ 0x2580, 0x259F, # block elements
32
+ 0x25A0, 0x25FF, # geometric shapes (○ ◯ □ ▪)
33
+ 0x2600, 0x27BF, # misc symbols + dingbats ( )
34
+ 0x2B00, 0x2BFF, # misc symbols and arrows
35
+ 0,
36
+ )
37
+
38
+
39
+ # Default glyph ranges for every non-merged font. imgui's own default is
40
+ # Latin-only (0x0020-0x00FF), which leaves U+2026 "..." (elisions, "dot..."),
41
+ # en/em dashes, smart quotes, arrows and the geometric bullets rendering
42
+ # as "?" all over the app. DejaVu Sans and JetBrains Mono cover all of
43
+ # these; the atlas grows by a few hundred glyphs per font. Trailing 0
44
+ # terminates the list.
45
+ _UI_RANGE: Tuple[int, ...] = (
46
+ 0x0020, 0x00FF, # Basic Latin + Latin-1 Supplement
47
+ 0x2010, 0x2027, # general punctuation (dashes, smart quotes, ellipsis)
48
+ 0x2190, 0x21FF, # arrows
49
+ 0x25A0, 0x25FF, # geometric shapes (● ◯ ▶ ▪)
50
+ 0x2600, 0x27BF, # misc symbols + dingbats ( )
51
+ 0,
52
+ )
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class FontSpec:
57
+ path: str
58
+ size: float
59
+ merge: bool = False
60
+ glyph_ranges: Optional[Tuple[int, ...]] = None
61
+ extra_spacing: float = 0.0
62
+ # HORIZONTAL oversampling (vertical is always 1, see prewarm). MUST stay
63
+ # 3: the renderer's LCD text shader (split_overlay_renderer.py) reads
64
+ # the atlas as a subpixel bitmap - every glyph quad spans exactly
65
+ # `oversample` atlas texels per sub width, and the shader samples at
66
+ # -1/0/+1 texels for the R/G/B subpixel coverages. stb's box prefilter
67
+ # over those 3 texels is FreeType's 'light' LCD filter. Any other value
68
+ # puts the R/B samples off the subpixel centres.
69
+ oversample: int = 3
70
+ # Replace stb's raw bitmaps with FreeType auto-hinted LCD renders
71
+ # (see FontManager.hint_atlas). Off for decorative sizes where hinting
72
+ # buys nothing; sizes above HINT_MAX_SIZE px are skipped regardless.
73
+ hint: bool = True
74
+ # Baked at boot by prewarm(). Everything else lazy-loads: the first
75
+ # get() queues the font and Melty.apply_ui_scale bakes it between
76
+ # frames. Keep this limited to what the FIRST FRAME needs - every eager
77
+ # font is baked inside boot's critical path (stb rasterize + FreeType
78
+ # hint; the full 17-entry atlas cost 230ms).
79
+ eager: bool = False
80
+ weight: int = 400
81
+
82
+
83
+ _JETBRAINS_MONO = str(_RESOURCES / "JetBrainsMono-Regular.ttf")
84
+
85
+
86
+ def _fa_merge(size: float, eager: bool = False) -> FontSpec:
87
+ """FontAwesome icon spec to fold into the preceding base font.
88
+
89
+ Sized at ~0.78x the host glyph size (matching the 14/18 ratio of the
90
+ DejaVu UI font) so merged icons sit at a comparable cap height.
91
+ """
92
+ return FontSpec(
93
+ str(_RESOURCES / "fontawesome-webfont.ttf"),
94
+ size,
95
+ merge=True,
96
+ glyph_ranges=_FA_ICON_RANGE,
97
+ extra_spacing=2.0,
98
+ eager=eager,
99
+ )
100
+
101
+
102
+ class Font(RelaxedEnum):
103
+ # Order matters: a `merge=True` font is folded into the most recently
104
+ # added non-merged font, so primary fonts must come before their merges.
105
+ # eager=True marks the first-frame fonts (imgui's main font - the first
106
+ # entry - and the editor face); everything else bakes on first get().
107
+ DEJAVU_SANS_18 = FontSpec(_DEJAVU_SANS, 18.0, eager=True)
108
+ FONTAWESOME_14 = _fa_merge(14.0, eager=True)
109
+ DEJAVU_SANS_50 = FontSpec(_DEJAVU_SANS, 50.0)
110
+ # The Fast Dock's Important tab: a step up from the default UI group for
111
+ # larger icon tiles (icons render at the merged FA size, ~0.78x the host).
112
+ DEJAVU_SANS_24 = FontSpec(_DEJAVU_SANS, 24.0)
113
+ FONTAWESOME_18 = _fa_merge(18.5)
114
+ # DEJAVU_SANS_22 = FontSpec(_DEJAVU_SANS, 22.0)
115
+
116
+ JETBRAINS_MONO_30 = FontSpec(_JETBRAINS_MONO, 30.0)
117
+ JETBRAINS_MONO_13 = FontSpec(_JETBRAINS_MONO, 13.0)
118
+ JETBRAINS_MONO_14 = FontSpec(_JETBRAINS_MONO, 14.0)
119
+ JETBRAINS_MONO_15 = FontSpec(_JETBRAINS_MONO, 15.0)
120
+ FONTAWESOME_MONO_15 = _fa_merge(12.0)
121
+ JETBRAINS_MONO_16 = FontSpec(_JETBRAINS_MONO, 16.0, glyph_ranges=_MONO_TUI_RANGE)
122
+ FONTAWESOME_MONO_16 = _fa_merge(12.5)
123
+ JETBRAINS_MONO_18 = FontSpec(_JETBRAINS_MONO, 18.0)
124
+ JETBRAINS_MONO_19 = FontSpec(_JETBRAINS_MONO, 18.5, glyph_ranges=_MONO_TUI_RANGE,
125
+ eager=True)
126
+ FONTAWESOME_MONO_19 = _fa_merge(16.0, eager=True)
127
+
128
+ JETBRAINS_MONO_20 = FontSpec(_JETBRAINS_MONO, 20.0)
129
+ JETBRAINS_MONO_22 = FontSpec(_JETBRAINS_MONO, 22.0)
130
+ # The chat transcript's user prose: the editor face one step up.
131
+ FONTAWESOME_MONO_22 = _fa_merge(19.0)
132
+ JETBRAINS_MONO_40 = FontSpec(_JETBRAINS_MONO, 40.0)
133
+ FONTAWESOME_MONO_40 = _fa_merge(31.0)
134
+ JETBRAINS_MONO_50 = FontSpec(_JETBRAINS_MONO, 50.0)
135
+ FONTAWESOME_MONO_50 = _fa_merge(39.0)
136
+
137
+
138
+ # imgui folds a merge=True spec into the most recently added non-merged font,
139
+ # so a base font and its contiguous merge entries must always bake TOGETHER
140
+ # and in enum order. The lazy-load unit is therefore a GROUP - get() on any
141
+ # member queues the whole group.
142
+ def _build_groups():
143
+ groups = []
144
+ for font in Font:
145
+ if font.value.merge and groups:
146
+ groups[-1].append(font)
147
+ else:
148
+ groups.append([font])
149
+ return {font: tuple(group) for group in groups for font in group}
150
+
151
+
152
+ _GROUP_OF = _build_groups()
153
+
154
+
155
+ def detect_auto_scale(window=None) -> float:
156
+ """dp scale for the monitor the OS window currently sits on. On Windows
157
+ the monitor's display scaling (content scale); elsewhere 1.5 on
158
+ 4k-and-larger panels (video mode at/above 3840 wide or 2160 tall, so a
159
+ portrait 4k still counts), 1.0 otherwise. The window's monitor is found
160
+ by which video mode contains the window's center; falls back to the
161
+ primary monitor when the window is None or sits off every monitor
162
+ (mid-drag between screens). 1.0 on any glfw failure."""
163
+ try:
164
+ import meltygui.core.windowing.window_api as glfw
165
+ target = None
166
+ if window is not None:
167
+ wx, wy = glfw.get_window_pos(window)
168
+ ww, wh = glfw.get_window_size(window)
169
+ cx, cy = wx + ww / 2, wy + wh / 2
170
+ for m in glfw.get_monitors():
171
+ mx, my = glfw.get_monitor_pos(m)
172
+ mode = glfw.get_video_mode(m)
173
+ if (mx <= cx < mx + mode.size.width
174
+ and my <= cy < my + mode.size.height):
175
+ target = m
176
+ break
177
+ if target is None:
178
+ target = glfw.get_primary_monitor()
179
+ if not target:
180
+ return 1.0 # headless session: no monitor to ask
181
+ if sys.platform == 'win32':
182
+ # Windows reports the display scaling the user chose (100 / 125 / 150 %...)
183
+ # per monitor, and a GLFW window there is sized in physical pixels, so that
184
+ # factor IS the dp scale. Elsewhere the compositor scales or the figure is
185
+ # a session-wide DPI guess, so the panel resolution stays the signal.
186
+ return float(max(glfw.get_monitor_content_scale(target)))
187
+ mode = glfw.get_video_mode(target)
188
+ return 1.5 if (mode.size.width >= 3840 or mode.size.height >= 2160) else 1.0
189
+ except Exception:
190
+ return 1.0
191
+
192
+
193
+ # The native weight faces of a base font file: {weight: path}, only those
194
+ # that exist. Probed ONCE per path - styled_font() per view per frame and
195
+ # a stat per face there was 150k stats a second in the 09-13 playground.
196
+ _FACES_BY_PATH: dict = {}
197
+
198
+
199
+ def _native_faces(path: str, weight: int) -> dict:
200
+ available = _FACES_BY_PATH.get(path)
201
+ if available is None:
202
+ faces = {weight: path}
203
+ if path == _DEJAVU_SANS:
204
+ faces = {200: str(_RESOURCES / 'dejavu' / 'DejaVuSans-ExtraLight.ttf'),
205
+ 400: _DEJAVU_SANS, 700: str(_RESOURCES / 'dejavu' / 'DejaVuSans-Bold.ttf')}
206
+ elif path == _JETBRAINS_MONO:
207
+ faces = {400: _JETBRAINS_MONO}
208
+ for w, label in ((100, 'Thin'), (200, 'ExtraLight'), (300, 'Light'),
209
+ (500, 'Medium'), (600, 'SemiBold'), (700, 'Bold'), (800, 'ExtraBold')):
210
+ faces[w] = str(_RESOURCES / 'jetbrains-weights' / f'JetBrainsMono-{label}.ttf')
211
+ available = {w: p for w, p in faces.items() if Path(p).is_file()}
212
+ _FACES_BY_PATH[path] = available
213
+ return available
214
+
215
+
216
+ # Fonts baked larger than this (px, after UI scale) keep stb's bitmaps:
217
+ # hinting is a small-size legibility aid and the FreeType hint costs ~1 ms
218
+ # per glyph row-set, so the 40/50 px display faces are not worth it.
219
+ HINT_MAX_SIZE = 32.0
220
+
221
+
222
+ def _expand_ranges(ranges: Tuple[int, ...]):
223
+ """imgui glyph-range list (lo, hi, lo, hi, ..., 0) -> codepoints."""
224
+ out = []
225
+ it = iter(ranges)
226
+ for lo in it:
227
+ if lo == 0:
228
+ break
229
+ hi = next(it, 0)
230
+ out.extend(range(lo, hi + 1))
231
+ return out
232
+
233
+
234
+ class FontManager:
235
+ # no external change
236
+ """Owns the imgui font atlas: one handle per Font enum entry, all baked
237
+ at `scale` x their authored pixel size.
238
+
239
+ The scale is baked into the RASTERIZED size rather than applied as a
240
+ draw-time multiplier (io.font_global_scale) so glyphs stay sharp — a
241
+ scaled atlas is resampled text, which is exactly the soft/aliased look
242
+ the whole-interface zoom had. The cost is that changing the scale means
243
+ re-rasterizing every font (see `rebuild`), so it happens only when the
244
+ UIScale value actually moves.
245
+ """
246
+
247
+ # Floor on a baked glyph size: sub-pixel-ish sizes rasterize to mush and
248
+ # can make imgui's atlas build fail outright.
249
+ MIN_SIZE = 4.0
250
+
251
+ def __init__(self, io, scale: float = 1.0):
252
+ self.io = io
253
+ self.scale = float(scale)
254
+ self._handles: dict = {}
255
+ # imgui keeps a POINTER to each font's glyph ranges until the atlas is
256
+ # built, and a GlyphRanges frees its array when collected: the bake
257
+ # owns them here until the next bake replaces the atlas.
258
+ self._glyph_ranges: list = []
259
+ # Fonts baked into the current atlas (load failures included, stamped
260
+ # None in _handles so they never re-queue) / fonts get() queued for
261
+ # the next between-frames bake (flush_pending).
262
+ self._loaded: set = set()
263
+ self._pending: set = set()
264
+ self._variant_groups = {}
265
+ # variant FontSpec -> the flush count it was last get()ed at, the
266
+ # retention clock for _bake (see Toggles.Fonts.variant_idle_flushes).
267
+ self._variant_last_used = {}
268
+ self._flush_count = 0
269
+ # (base font, half-pixel size, native weight) -> variant FontSpec, so
270
+ # the per-frame get() of a steady style allocates nothing.
271
+ self._variant_memo = {}
272
+
273
+ def rebuild(self, scale: float, impl=None) -> bool:
274
+ """Re-bake every LOADED font at `scale` and hand the new atlas to the
275
+ renderer. No-op (False) when the scale is unchanged.
276
+
277
+ MUST run between frames — outside imgui.new_frame()/render() — since
278
+ it clears the atlas the in-flight draw data would reference. Every
279
+ previously returned handle is dangling afterwards, so callers that
280
+ cached one (Melty.large_font, LSDStudio.fa_font) have to re-`get` it.
281
+ """
282
+ scale = float(scale)
283
+ if scale == self.scale and self._handles:
284
+ return False
285
+ self.scale = scale
286
+ self._loaded |= self._pending
287
+ self._pending.clear()
288
+ self._bake(self._loaded)
289
+ if impl is not None:
290
+ impl.refresh_font_texture()
291
+ return True
292
+
293
+ def flush_pending(self, impl=None) -> bool:
294
+ """Bake every font get() queued since the last flush — one atlas
295
+ re-bake covers everything the previous frame touched. No-op (False)
296
+ when nothing is pending. Same between-frames contract and same
297
+ dangling-handle consequence as rebuild; Melty.apply_ui_scale is the
298
+ call site and does the handle re-gets + tile invalidation."""
299
+ self._flush_count += 1
300
+ if not self._pending:
301
+ return False
302
+ self._loaded |= self._pending
303
+ self._pending.clear()
304
+ self._bake(self._loaded)
305
+ if impl is not None:
306
+ impl.refresh_font_texture()
307
+ return True
308
+
309
+ def _oversample(self, spec: FontSpec) -> int:
310
+ """Horizontal oversampling for a spec — the authored value at EVERY
311
+ scale. It used to walk down as the UI scale grew (atlas area), but
312
+ the LCD text shader needs exactly 3 atlas texels per screen pixel
313
+ regardless of scale, and with oversample_v pinned to 1 the atlas
314
+ cost is linear in it (3x, not 9x)."""
315
+ return spec.oversample
316
+
317
+ def prewarm(self):
318
+ """Bake the eager (first-frame) fonts only. Everything else loads
319
+ lazily: get() on an unbaked font queues its group and returns None —
320
+ exactly what callers already handle for a failed load (render with
321
+ the current font this frame) — and Melty.apply_ui_scale bakes the
322
+ queue between frames. Baking all 17 enum entries up front cost
323
+ ~230ms of boot (stb rasterize + FreeType hint of a 4096² atlas);
324
+ the eager set is a fraction of that."""
325
+ self._loaded |= {font for font in Font if font.value.eager}
326
+ for font in list(self._loaded):
327
+ self._loaded.update(self._variant_groups.get(font, _GROUP_OF.get(font, (font,))))
328
+ self._bake(self._loaded)
329
+
330
+ def _bake(self, include):
331
+ """Clear the atlas and re-add every font in `include`, in enum order
332
+ (a merge follower must directly follow its base — enum order plus the
333
+ _GROUP_OF closure at every queue site guarantee both are present and
334
+ adjacent). Between frames only; every previously returned handle is
335
+ dangling afterwards."""
336
+ # Variant retention. A bake is the only moment a variant can leave the
337
+ # atlas, and it leaves only when it has not been drawn for
338
+ # Toggles.Fonts.variant_idle_flushes surface frames, or when more
339
+ # than Toggles.Fonts.max_font_variants are alive (least recently
340
+ # drawn first). Evicting anything a window drew recently re-queues it
341
+ # on that window's next frame: one bake per frame, the atlas thrash of
342
+ # 09-13 (eight windows, ~30 variants, a 24-entry keep list).
343
+ from meltygui.core.runtime.toggles import Toggles
344
+ alive = sorted((base for base in self._variant_last_used if base in include),
345
+ key=self._variant_last_used.get, reverse=True)
346
+ stale_before = self._flush_count - Toggles.Fonts.variant_idle_flushes
347
+ keep = [base for base in alive if self._variant_last_used[base] >= stale_before]
348
+ # A variant queued this flush carries the current stamp, so it is
349
+ # always inside the idle window and sorts first under the cap.
350
+ keep = keep[:max(0, int(Toggles.Fonts.max_font_variants))]
351
+ retained = {f for f in include if not isinstance(f, FontSpec)}
352
+ for base in keep:
353
+ retained.update(self._variant_groups.get(base, (base,)))
354
+ include.intersection_update(retained)
355
+ self._variant_groups = {f: group for f, group in self._variant_groups.items() if f in include}
356
+ self._variant_last_used = {f: t for f, t in self._variant_last_used.items() if f in include}
357
+ self._handles.clear()
358
+ self.io.fonts.clear()
359
+ self._glyph_ranges.clear()
360
+ variants = sorted((f for f in include if isinstance(f, FontSpec)),
361
+ key=lambda f: (f.path, f.size, f.weight))
362
+ entries = list(Font) + [f for base in variants if not base.merge
363
+ for f in self._variant_groups.get(base, (base,))]
364
+ for font in entries:
365
+ if font not in include:
366
+ continue
367
+ spec = font if isinstance(font, FontSpec) else font.value
368
+ size = max(self.MIN_SIZE, spec.size * self.scale)
369
+ if spec.merge:
370
+ merge_cfg = dict(
371
+ merge_mode=True,
372
+ glyph_extra_spacing_x=spec.extra_spacing * self.scale,
373
+ glyph_extra_spacing_y=spec.extra_spacing * self.scale,
374
+ )
375
+ # Merged icon fonts cover wide ranges, so they dominate the
376
+ # atlas - spread them out as the scale grows. Untouched at
377
+ # scale <= 1 because the authored atlas is unchanged: imgui's
378
+ # own defaults are (h=3, v=1), and spelling them out is
379
+ # what keeps v from silently becoming 3 here.
380
+ if self.scale > 1.0:
381
+ merge_cfg["oversample_h"] = self._oversample(spec)
382
+ merge_cfg["oversample_v"] = 1
383
+ cfg = imgui.FontConfig(**merge_cfg)
384
+ else:
385
+ over = self._oversample(spec)
386
+ # oversample_v stays 1 (imgui's own default): glyph Y is
387
+ # never sub-pixel positioned (RenderText floors pos.y), so
388
+ # vertical oversampling only smears the baseline, x-height and
389
+ # crossbars by ~1/N px - the "slightly soft text" look. It
390
+ # also keeps the atlas rows 1:1 with screen rows, which the
391
+ # LCD shader's horizontal-only taps rely on.
392
+ cfg = imgui.FontConfig(
393
+ oversample_h=over,
394
+ oversample_v=1,
395
+ pixel_snap_h=True,
396
+ )
397
+ try:
398
+ # Merged (icon) fonts use their explicit range; base fonts
399
+ # without one get _UI_RANGE instead of imgui's Latin-only
400
+ # default (see the comment on _UI_RANGE).
401
+ glyph_ranges = spec.glyph_ranges
402
+ if glyph_ranges is None and not spec.merge:
403
+ glyph_ranges = _UI_RANGE
404
+ if glyph_ranges is not None:
405
+ ranges = imgui.GlyphRanges(list(glyph_ranges))
406
+ self._glyph_ranges.append(ranges)
407
+ handle = self.io.fonts.add_font_from_file_ttf(spec.path, size, cfg, ranges)
408
+ else:
409
+ handle = self.io.fonts.add_font_from_file_ttf(spec.path, size, cfg)
410
+ self._handles[font] = handle
411
+ except Exception as e:
412
+ print(f"FontManager: failed to load {self._font_name(font)} from {spec.path}: {e}")
413
+ self._handles[font] = None
414
+
415
+ def get(self, font: Font):
416
+ """Handle for `font`, or None — for a failed load AND for a font not
417
+ baked yet. An unbaked font's group is queued here and baked between
418
+ frames (flush_pending, driven by Melty.apply_ui_scale), so callers
419
+ keep their existing None handling: skip push_font this frame, the
420
+ real handle arrives next frame and the flush invalidates every
421
+ cached tile. A failed font sits in _handles as None and never
422
+ re-queues."""
423
+ from meltygui.core.melty import Melty
424
+ from meltygui.core.runtime.toggles import Toggles
425
+ if (Toggles.dynamic_styles and Melty.font_style_stack
426
+ and isinstance(font, (Font, FontSpec))):
427
+ font = self.styled_font(font, Melty.font_style_stack[-1])
428
+ return self._get(font)
429
+
430
+ def _get(self, font):
431
+ if isinstance(font, FontSpec) and not font.merge:
432
+ self._variant_last_used[font] = self._flush_count
433
+ if font in self._handles:
434
+ return self._handles[font]
435
+ group = self._variant_groups.get(font, _GROUP_OF.get(font))
436
+ if group is not None:
437
+ self._pending.update(group)
438
+ return None
439
+
440
+ @staticmethod
441
+ def _font_name(font):
442
+ return (f"{Path(font.path).stem}@{font.size:g}" if isinstance(font, FontSpec)
443
+ else font.name)
444
+
445
+ def styled_font(self, font, context):
446
+ """Select native outlines at the resolved size; never scale glyph quads.
447
+
448
+ DejaVu offers extra-light/regular/bold. Bundled JetBrains Mono offers
449
+ weights 100 through 800. Choose the nearest available native weight.
450
+ Half-pixel size steps bound atlas churn while preserving 3x LCD texels.
451
+ """
452
+ group = _GROUP_OF.get(font, (font,))
453
+ base = group[0]
454
+ spec = base if isinstance(base, FontSpec) else base.value
455
+ from meltygui.core.styling.style import evaluate_font_style
456
+ if context[:4] == (0.0, 0.0, False, False):
457
+ return font
458
+ size, weight = evaluate_font_style(context, spec.size, spec.weight)
459
+ size = round(2 * max(self.MIN_SIZE, min(96.0, size))) / 2
460
+ weight = max(100, min(900, weight))
461
+ available = _native_faces(spec.path, spec.weight)
462
+ chosen = min(available, key=lambda w: abs(w - weight)) if available else spec.weight
463
+ path = available.get(chosen, spec.path)
464
+ if size == spec.size and path == spec.path:
465
+ return font
466
+ memo_key = (base, size, chosen)
467
+ variant = self._variant_memo.get(memo_key)
468
+ if variant is not None:
469
+ return variant
470
+ variant = replace(spec, path=path, size=float(size), weight=chosen, eager=False)
471
+ self._variant_memo[memo_key] = variant
472
+ if variant not in self._variant_groups:
473
+ # Scale merged icons at their authored ratio and in the same group.
474
+ merges = tuple(replace(member.value, size=member.value.size * size / spec.size,
475
+ eager=False) for member in group[1:])
476
+ self._variant_groups[variant] = (variant,) + merges
477
+ return variant
478
+
479
+ def peek(self, font: Font):
480
+ """Handle for `font` if it is baked, else None — WITHOUT queueing a
481
+ lazy load. For long-lived handle caches (Melty.large_font) that must
482
+ refresh after every re-bake but should never force an unused font
483
+ into the atlas: at boot the eager get(DEJAVU_SANS_50) queued a font
484
+ nothing drew, costing a whole extra re-bake + tile invalidation."""
485
+ return self._handles.get(font)
486
+
487
+ # ------------------------------------------------------------------
488
+ # FreeType hinting pass
489
+ # ------------------------------------------------------------------
490
+ # stb_truetype (imgui's rasterizer) has no hinter: a 1.3 px stem or an
491
+ # x-height at 8.25 px lands wherever the outline puts it and smears over
492
+ # two pixel rows. FreeType's light autohinter grid-fits the outline
493
+ # VERTICALLY (baseline, x-height, crossbars snap to pixel rows) before
494
+ # rasterizing, and its LCD render mode emits 3 subpixel coverages per
495
+ # pixel - exactly the layout the LCD text shader already reads from the
496
+ # 3x-oversampled stb atlas (split_overlay_renderer.py). So the pass
497
+ # keeps imgui's glyph geometry (quads, advances, UVs) unchanged and just
498
+ # swaps the TEXELS: for every glyph whose hinted bitmap fits its stb
499
+ # rect it writes the FreeType coverage there; the few that grow a row
500
+ # under hinting (arrows, accent marks, `i`/`j`/`t` at ~10 px em) keep
501
+ # stb's bitmap. Harmony-mode FreeType pads each LCD bitmap by a zero
502
+ # pixel per side; clipping that to the rect is lossless.
503
+ #
504
+ # Glyph rects are not exposed by pyimgui, so they are PROBED: one
505
+ # throwaway imgui frame per font draws every codepoint with add_text and
506
+ # the quad + UV come back out of the draw data (4 vertices per glyph).
507
+ # Must run between frames (renderer init, FontManager.rebuild).
508
+
509
+ _FT_LOAD = None # resolved lazily: freetype may be absent
510
+
511
+ def _ft_face(self, path: str):
512
+ import freetype
513
+ faces = self.__dict__.setdefault("_ft_faces", {})
514
+ face = faces.get(path)
515
+ if face is None:
516
+ face = freetype.Face(path)
517
+ faces[path] = face
518
+ return face
519
+
520
+ def _probe_glyph_rects(self, handle, cps, tex_w: int, tex_h: int):
521
+ """{cp: (qx0, qy0, qx1, qy1, tx0, ty0, tx1, ty1)} — quad in px
522
+ relative to the add_text pen, texel rect in the atlas. Codepoints
523
+ that emit no quad (spaces) are absent; imgui's fallback glyph shows
524
+ up as repeated rects, deduped by the caller."""
525
+ io = self.io
526
+ saved = (io.display_size, io.delta_time)
527
+ io.display_size = (4096.0, 4096.0)
528
+ io.delta_time = 1.0 / 60.0
529
+ out = {}
530
+ try:
531
+ imgui.new_frame()
532
+ dl = imgui.get_background_draw_list()
533
+ imgui.push_font(handle)
534
+ order = []
535
+ cell = 64
536
+ for i, cp in enumerate(cps):
537
+ x = float((i % 60) * cell)
538
+ y = float((i // 60) * cell)
539
+ n0 = dl.vtx_buffer_size
540
+ dl.add_text(x, y, 0xFFFFFFFF, chr(cp))
541
+ if dl.vtx_buffer_size == n0 + 4:
542
+ order.append((cp, n0, x, y))
543
+ imgui.pop_font()
544
+ imgui.render()
545
+ dd = imgui.get_draw_data()
546
+ lists = dd.commands_lists
547
+ if not lists:
548
+ return out
549
+ cl = lists[0]
550
+ buf = ctypes.string_at(cl.vtx_buffer_data, cl.vtx_buffer_size * imgui.VERTEX_SIZE)
551
+ vs = imgui.VERTEX_SIZE
552
+ import struct
553
+ for cp, n0, x, y in order:
554
+ v0 = struct.unpack_from("ffff", buf, n0 * vs)
555
+ v2 = struct.unpack_from("ffff", buf, (n0 + 2) * vs)
556
+ out[cp] = (v0[0] - x, v0[1] - y, v2[0] - x, v2[1] - y,
557
+ int(round(v0[2] * tex_w)), int(round(v0[3] * tex_h)),
558
+ int(round(v2[2] * tex_w)), int(round(v2[3] * tex_h)))
559
+ finally:
560
+ io.display_size, io.delta_time = saved
561
+ return out
562
+
563
+ def hint_atlas(self, width: int, height: int, pixels: bytes):
564
+ """Return RGBA32 atlas bytes with FreeType light-hinted LCD glyphs
565
+ written into imgui's rects, or None when freetype is unavailable.
566
+ Stats land on `self.hint_stats` as {font name: (hinted, fallback)}."""
567
+ start_time = time.time()
568
+ try:
569
+ import freetype
570
+ except ImportError:
571
+ if not getattr(self, "_ft_warned", False):
572
+ self._ft_warned = True
573
+ print("FontManager: freetype-py not installed; text stays unhinted "
574
+ "(pip install freetype-py)")
575
+ return None
576
+ load_flags = (freetype.FT_LOAD_TARGET_LIGHT | freetype.FT_LOAD_FORCE_AUTOHINT
577
+ | freetype.FT_LOAD_NO_BITMAP)
578
+ atlas = np.frombuffer(pixels, np.uint8).reshape(height, width, 4).copy()
579
+ alpha = atlas[..., 3]
580
+ stats = {}
581
+ for font in self._handles:
582
+ spec = font if isinstance(font, FontSpec) else font.value
583
+ handle = self._handles.get(font)
584
+ if spec.merge or not spec.hint or handle is None:
585
+ continue
586
+ size = max(self.MIN_SIZE, spec.size * self.scale)
587
+ if size > HINT_MAX_SIZE:
588
+ continue
589
+ cps = _expand_ranges(spec.glyph_ranges or _UI_RANGE)
590
+ rects = self._probe_glyph_rects(handle, cps, width, height)
591
+ face = self._ft_face(spec.path)
592
+ box = face.ascender - face.descender # hhea, font units
593
+ scale = size / box # == stbtt_ScaleForPixelHeight
594
+ baseline = math.floor(face.ascender * scale + 1) # imgui: IM_FLOOR(ascent + 1)
595
+ face.set_char_size(0, int(round(size * face.units_per_EM / box * 64)), 72, 72)
596
+ seen = set()
597
+ hinted = fallback = 0
598
+ for cp, (qx0, qy0, qx1, qy1, tx0, ty0, tx1, ty1) in rects.items():
599
+ if (tx0, ty0) in seen:
600
+ continue # fallback-glyph repeats
601
+ seen.add((tx0, ty0))
602
+ if face.get_char_index(cp) == 0:
603
+ continue
604
+ face.load_char(chr(cp), load_flags)
605
+ g = face.glyph
606
+ g.render(freetype.FT_RENDER_MODE_LCD)
607
+ bm = g.bitmap
608
+ rows, bw, pitch = bm.rows, bm.width, abs(bm.pitch)
609
+ if rows == 0 or bw == 0:
610
+ continue
611
+ rect_w, rect_h = tx1 - tx0, ty1 - ty0
612
+ r0 = baseline - g.bitmap_top - int(round(qy0))
613
+ if r0 < 0 or r0 + rows > rect_h:
614
+ fallback += 1 # hinting grew it a row
615
+ continue
616
+ # Subpixel column of bitmap col #0. imgui's quad x0 is in
617
+ # THIRDS of a pixel (stb's 3x bitmap box), so place at
618
+ # subpixel precision - rounding c0 to a whole pixel puts
619
+ # glyphs +-1/3 px from their neighbours (uneven spacing).
620
+ c0 = int(round(3 * (g.bitmap_left - qx0)))
621
+ j0, j1 = max(0, -c0), min(bw, rect_w - c0)
622
+ if j1 <= j0:
623
+ fallback += 1
624
+ continue
625
+ raw = ctypes.string_at(bm._FT_Bitmap.buffer, rows * pitch)
626
+ src = np.frombuffer(raw, np.uint8).reshape(rows, pitch)[:, :bw]
627
+ alpha[ty0:ty1, tx0:tx1] = 0
628
+ alpha[ty0 + r0:ty0 + r0 + rows, tx0 + c0 + j0:tx0 + c0 + j1] = src[:, j0:j1]
629
+ hinted += 1
630
+ stats[self._font_name(font)] = (hinted, fallback)
631
+ self.hint_stats = stats
632
+ total_h = sum(h for h, _ in stats.values())
633
+ total_f = sum(f for _, f in stats.values())
634
+ print(f"FontManager: FreeType-hinted {total_h} glyphs across {len(stats)} fonts "
635
+ f"({total_f} kept stb bitmaps)")
636
+ end_time = time.time()
637
+ print(f"FontManager: hinting pass took {end_time - start_time:.3f} s")
638
+ print(f"wall time {end_time}")
639
+ return atlas.tobytes()