Files
craft-libs/crates/font-manager
aiongg 92d392cff1 craft-ui: ComboBox, egui's combo box with a menu that grows back (#387)
* craft-ui: combo boxes whose menu grows back with its contents

egui's ComboBox lays its menu out in the height it had on the previous pass,
and its scroll area takes no more than that. A filtered list that shrinks (a
search with no matches) never grows back when the search is cleared
(emilk/egui#5225, #5138). craft_ui::combo::menu offers its contents the full
height on every pass; craft_ui::combo::combo draws the combo box button
around it. The Font Manager's Language, Script and Sample menus use it.

* craft-ui: combo and menu take the close behaviour

A menu with a search field that closes itself on a pick (DesignCraft's font
menu) closes on a click outside only.

* craft-ui: ComboBox, a drop-in for egui's combo box, with header menus

craft_ui::ComboBox has egui 0.37's ComboBox builder and button (same
atoms, style classes and icon, pixel-identical in a test) with a menu that
grows back. show_ui_with_header and show_rows_with_header keep a search
field in place and scroll only the list; combo::Menu is the same menu under
a field an app draws itself. clippy.toml disallows egui::ComboBox.

The Font Manager's Language menu, the keymap dialog's combo boxes and the
print dialog's printer and duplex choices use it.

* craft-ui: combo menus keep to the room on their side, and footers

A menu is never taller than the room on the side of its field it opens on,
so an unbounded height fits on screen and scrolls. The side is settled once
the menu is measured and kept while it is open, so a menu that shrinks or
grows while typing doesn't jump to the other side of its field.

Footer forms keep a row under the scrolling list. Menu::new takes the
field's Ui for its default height and frame, and Menu::open shows the menu
under a text field that keeps the focus while typing.

* craft-ui: combo menus open by the app keep their side, and take picks

Menu::open takes &mut bool: the menu clears it when it closes (close
behavior, Escape, ui.close(), the field losing the focus to anything but
the menu), so a click on a row under a focused text field is picked.

The side settles only once the menu was shown and measured, also for
menus the app opens (Popup::open_id). A footer that changes height
remeasures the menu before it is shown. The popup frame's margins come
from the popup style; only above/below alignments are tried.

* craft-ui: a resting pointer over a combo menu doesn't keep it open

The menu counts the pointer as having moved into it only on a press or
click. A footer height change also requests a repaint, in case the
discard is denied.

* craft-ui: a footer menu opens without a second sizing pass

A footer height change no longer discards the pass or forces another
sizing pass: the menu measures again on the next pass and settles its
side then. A forced sizing pass ran a search field's focus request in a
pass where the field can't take the focus, so a footer menu opened with
the search unfocused.

* craft-ui: tests use Ui::scope_id

* craft-ui: hide the measuring pass of a footer menu, cap remeasures

The pass that measures a menu before its side is settled is laid out but
drawn fully transparent, so no frame shows a side or height the menu then
leaves. A footer that changes height on every pass remeasures twice per
open, then settles.

* craft-ui: tests satisfy clippy
2026-10-27 09:21:01 -04:00
..

craft-font-manager

Craft Font Manager as a library: every font the suite's apps can use, and the font collections to add to them: Google Fonts, Noto, and index URLs the user adds. ArtCraft Launcher shows it as its Fonts tab; the standalone app (aiongg/craft-font-manager) shows it as its window.

Part Feature What it does
filter always Search over the index: family names ignoring case and accents, category, language (English name, autonym or id) and script
index always The pinned built-in indexes (Google Fonts and Noto) compiled into craft-font-store (bundled), each family's source (source_label: its name and whether it is curated), and the categories
installed always The installed fonts craft-font-catalog sees, grouped by family and source
store always Installer, what the interface needs from the font store: the download consent, install, remove, downloads for a preview, the indexes (indexes, fetch_indexes) and the sources the user added (user_sources, add_user_source, remove_user_source). SuiteStore is craft-font-store's store, FakeStore a stand-in for tests, NoStore one for when the store folder is unknown
FontsPanel egui The whole interface as one egui panel

The panel

The layout follows Adobe Fonts' and Google Fonts' browse pages.

  • Across the top: Browse and Installed, with their counts, and one search field.
  • Filters at the reading start, each under its label: category, language (searchable by its English name or its own, "Japanese · 日本語"), script, source, and installed or not; each option with its count.
  • Family cards, one per row: the name, style count, category and source on one line with the card's state at its end (Install and Preview, Installed, a progress bar while downloading, Couldn't install with Retry, "Not curated" for a source the user added), and a large sample in the face below. The list is virtual: only the cards on screen are laid out, and their faces load as they come into view. The arrow keys step through the cards and Enter opens one.
  • Sample toolbar: a preset (a sentence in the language filtered by, or in the family's own script when it has no Latin; the alphabet; numerals), the sample text, and the size.
  • Details sheet over the list: every style in its own face with Install or Remove for each, Install all and Remove all, the source, designer, licence, scripts and languages, and what a preview downloads.
  • Installed: system and font store fonts as the same cards, filtered by source. Only font store families can be removed; system fonts are removed with the system's own tools.
  • Sources (sources_ui, for a host's settings page): the built-in sources, the index URLs the user added, marked not curated and flagged when the file at their address changed since it was added, adding one, and Update sources.

FontsPanel fills the Ui it is given and has no window, menu or interface fonts of its own; the host's Look gives it the host's colours and font families. The faces it draws samples in are egui font families of its own, added a few per frame and taken out again past 64, without touching the host's fonts. Right-to-left interfaces mirror through craft_ui::direction; sample text keeps its own direction.

let catalog = Arc::clone(Catalog::global());
catalog.scan_in_background();
let installer: Arc<dyn Installer> = match SuiteStore::for_user(Arc::clone(&catalog)) {
    Some(store) => Arc::new(store),
    None => Arc::new(NoStore),
};
let index = installer.indexes().map_err(|e| e.to_string());
let options = Options { catalog, installer, index, runner: Runner::Thread };
let mut panel = FontsPanel::new(options, Settings::default());
// each frame
panel.ui(ui);
Call Use
set_look(look) The host's palettes (dark and light) and font families, and whether labels are set in capitals
settings(), settings_mut() What the host saves: the sample text, preset and size, and whether the downloads note was closed (serde)
downloads_allowed(), set_downloads_allowed(on) The download consent, for a host's settings page. It is the font store's own setting, shared by every Craft app
show_downloads_switch(false) For a host with the switch in its own settings
sources_ui(ui) The font sources, for a host's settings page
set_index(index) The indexes, after the host fetched them
set_language(tag) The interface language, the host's own; English when its catalog isn't embedded
install_interface_fonts(ctx, craft_faces) For a host without interface fonts of its own (the standalone app): craft-ui's font stack for the panel's language, craft-fonts' faces the app embeds with craft-fonts-build, and the interface direction

Network use

Nothing is downloaded until the user allows it. Downloads are off by default; the first Install or Preview asks first. Files come from each source's repositories at the commit or tag its index records, through jsDelivr (raw GitHub as the fallback), and each is checked against the index's size and sha256 before use. Previews stay in memory and are never installed. A source the user adds is an index at an https URL; its families are never curated.

Strings

Every interface string goes through craft-i18n with the English text as the key. The panel embeds its own Japanese and Arabic catalogs (src/panel/catalogs/) until its rows are part of craft-i18n; style and weight names, licence names and product names stay in English.

Testing

cargo test -p craft-font-manager --all-features
CRAFT_FONT_MANAGER_SNAPSHOTS=<dir> cargo test -p craft-font-manager --features egui --test flows screenshots -- --test-threads=1

tests/flows.rs drives the panel with egui_kittest against a small index, a font folder made per test and FakeStore. They install egui's Hack and Ubuntu Light faces into temporary folders. With CRAFT_FONT_MANAGER_GOOGLE_FONTS=<google/fonts checkout>, the fake store also "downloads" the index's files from the checkout, so screenshots show real previews.