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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- 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()
|