Merge pull request #7048 from dotCooCoo/fix/font-menu-shared-filters

Font menu: every font menu shares one set of filters
This commit is contained in:
BFlat
2026-11-13 14:17:18 +07:00
committed by GitHub
4 changed files with 141 additions and 5 deletions
+37 -3
View File
@@ -300,8 +300,9 @@ struct MenuState {
/// The highlighted row and the query and filters it belongs to.
highlight: Option<usize>,
query: String,
/// The filters (kept from one opening to the next): script (0..[`SCRIPTS`]), kind, font
/// technology (variable fonts only) and favourites only.
/// The filters (kept from one opening to the next, and the same in every font menu:
/// [`Filters`]): script (0..[`SCRIPTS`]), kind, font technology (variable fonts only) and
/// favorites only.
script: usize,
class: Option<FontClass>,
variable_only: bool,
@@ -324,6 +325,35 @@ struct MenuState {
wheel_target: Option<WheelTarget>,
}
/// The filters of [`MenuState`], one set for every font menu (the Character panel's, the Control
/// bar's, the Properties panel's…): a filter chosen in one menu is on in all of them. They last
/// for the session.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct Filters {
script: usize,
class: Option<FontClass>,
variable_only: bool,
favorites_only: bool,
}
impl Filters {
fn of(st: &MenuState) -> Self {
Self { script: st.script, class: st.class, variable_only: st.variable_only, favorites_only: st.favorites_only }
}
fn apply(self, st: &mut MenuState) {
st.script = self.script;
st.class = self.class;
st.variable_only = self.variable_only;
st.favorites_only = self.favorites_only;
}
}
/// Where the filters every font menu shares are kept ([`Filters`]).
fn filters_id() -> egui::Id {
egui::Id::unique("font-menu-filters")
}
/// The font family menu showing `current`, `width` points wide, rows sampling `sample` (the
/// selected text; `None`: a generic sample).
pub fn font_menu(
@@ -336,6 +366,7 @@ pub fn font_menu(
) -> Option<FontPick> {
let state_id = ui.scope_id().with(&id).with("font-menu");
let mut st: MenuState = ui.data(|d| d.get_temp(state_id)).unwrap_or_default();
ui.data(|d| d.get_temp::<Filters>(filters_id())).unwrap_or_default().apply(&mut st);
let target = ui.data(|d| d.get_temp(wheel_fields_id(ui.ctx().viewport_id()).with("target")));
let target_changed = st.wheel_target != target;
if target_changed {
@@ -416,7 +447,10 @@ pub fn font_menu(
if matches!(out, Some(FontPick::Chosen(..))) {
st.previewed = None;
}
ui.data_mut(|d| d.insert_temp(state_id, st));
ui.data_mut(|d| {
d.insert_temp(filters_id(), Filters::of(&st));
d.insert_temp(state_id, st);
});
out
}
+102
View File
@@ -790,6 +790,108 @@ fn a_modified_turn_that_starts_a_stream_keeps_it_off_the_field() {
assert_eq!((left, notches), (1, 0), "the stream began as a modified turn");
}
/// The font menus of two panels (other ids, other scopes) share their filters: a filter chosen in
/// one menu is on when the other opens, and it lists its rows by it.
#[test]
fn a_filter_chosen_in_one_panels_menu_is_on_in_the_others() {
use vectorcraft_text::FontClass;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let fields = [("character", "character-font", 10.0), ("control-bar", "control-font", 60.0)];
let field = |top: f32| egui::Rect::from_min_size(egui::pos2(10.0, top), egui::vec2(200.0, 30.0));
// Draws both fields (and the open menu), and returns the texts drawn with their centers and
// each menu's state.
let frame = |events: Vec<egui::Event>| {
let input =
egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 900.0))), ..Default::default() };
let mut ids = [egui::Id::NULL; 2];
let mut out = ctx.run_ui(input, |ui| {
for (i, (scope, id, top)) in fields.into_iter().enumerate() {
ui.push_id(scope, |ui| {
ui.scope_builder(egui::UiBuilder::new().max_rect(field(top)), |ui| {
ids[i] = ui.scope_id().with(id).with("font-menu");
super::font_menu(ui, id, "Inter", 200.0, None, super::MenuLook { samples: false, ..Default::default() });
});
});
}
});
out.textures_delta.clear();
let texts: Vec<(String, egui::Pos2)> = out
.shapes
.iter()
.filter_map(|c| match &c.shape {
egui::Shape::Text(t) => Some((t.galley.text().to_string(), t.visual_bounding_rect().center())),
_ => None,
})
.collect();
let states = ids.map(|id| ctx.data(|d| d.get_temp::<super::MenuState>(id)).unwrap_or_default());
(texts, states)
};
let press = |at: egui::Pos2| {
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(vec![egui::Event::PointerMoved(at), button(true)]);
frame(vec![button(false)]);
for _ in 0..3 {
frame(vec![]);
}
};
let open = |i: usize| press(field(fields[i].2).min + egui::vec2(10.0, 10.0));
let close = || {
let escape = [true, false].map(|pressed| egui::Event::Key {
key: egui::Key::Escape,
physical_key: None,
pressed,
repeat: false,
modifiers: Default::default(),
});
frame(escape.to_vec());
for _ in 0..3 {
frame(vec![]);
}
};
// The topmost text `label`: a filter chip, above the rows.
let chip = |label: &str| {
let (texts, _) = frame(vec![]);
texts
.into_iter()
.filter(|(t, _)| t == label)
.map(|(_, at)| at)
.min_by(|a, b| a.y.total_cmp(&b.y))
.unwrap_or_else(|| panic!("{label} is drawn"))
};
let families = |st: &super::MenuState| st.rows.iter().filter(|r| matches!(r, Row::Family(_))).count();
let filters = |st: &super::MenuState| (st.script, st.class, st.variable_only, st.favorites_only);
frame(vec![]);
// Favorites only, chosen in the first panel's menu, with no favorites: the second lists nothing.
open(0);
press(chip("★"));
close();
let (_, [first, second]) = frame(vec![]);
assert!(first.favorites_only && second.favorites_only, "the second menu has the filter before it opens");
open(1);
let (_, [_, second]) = frame(vec![]);
assert!(second.favorites_only);
assert_eq!(families(&second), 0, "no favorites, so the second menu lists no family");
// A script, a kind and Variable chosen in the second panel's menu are on in the first.
press(chip(super::script_label(3)));
press(chip(super::class_label(FontClass::Monospaced)));
press(chip(super::variable_label()));
close();
open(0);
let (_, [first, _]) = frame(vec![]);
assert_eq!(filters(&first), (3, Some(FontClass::Monospaced), true, true));
// Every filter, turned off in the first menu, is off in the second, which lists families again.
press(chip("★"));
press(chip(super::script_label(0)));
press(chip(super::class_label(FontClass::Monospaced)));
press(chip(super::variable_label()));
close();
open(1);
let (_, [_, second]) = frame(vec![]);
assert_eq!(filters(&second), (0, None, false, false));
assert!(families(&second) > 0, "every family is listed again");
}
/// The Font Technology filter's Variable is the font adjective, not the Variables panel's noun.
#[test]
fn the_variable_filter_is_named_as_fonts_are() {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -34,7 +34,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted; an optional feature that changes the ligature is); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607); the OpenType panel's Position menu sets superscript, subscript, numerator or denominator glyphs (`sups`, `subs`, `numr`, `dnom`). The OpenType panel's Stylistic Sets menu lists the selected font's `ss01`–`ss20` features, using a font-supplied name where available and `Set N` otherwise; several sets can be active, and selecting an active one removes it. For a mixed-font selection it uses the first selected character's font, a VectorCraft design choice ([public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/add-stylistic-sets-to-selected-text.html), [OpenType feature specification](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt)). The Glyphs panel highlights the character selected with the Type tool (a range's first character, else the one before the caret) and scrolls it into view as the selection moves (#4908; `panels::glyphs` tests); its Show menu lists Unicode ranges and Alternates for Current Selection, the selected character's alternate glyphs from the font's `GSUB` table. A double-click applies the OpenType feature that draws a variant to that character alone, or uses a glyph choice when no supported feature draws it; see the Glyphs panel row below for Entire Font and the joining limits. The glyph-cell pop-up of alternates is not there yet |
| Glyphs panel: alternates as OpenType feature values, unencoded glyphs | done | #5514, as [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html) describes (Show › Alternates For Current Selection; replace the selected character by double-clicking the glyph). A character-style feature takes a value (`"salt=2"`, the font's alternate number; `ss01`–`ss20`, `swsh`, `titl` and `aalt` too) in `features` and `featureChanges`, in SVG `font-feature-settings` (`"salt" 2`) and in native files as the plain string. Show › Entire Font applies an unencoded glyph the same way when it is an alternate of the selected character in its word, applies one no feature reaches (`A.salt` in a font with no `salt`) to the selected character as a glyph choice (it may not join), and dims the others with the reason in the tooltip (e.g. `yehfarsi.fina.alt2 belongs to ی (U+06CC) at the end of a word; your selection is ي (U+064A)`); an Arabic presentation form picked there inserts its letter so the word joins. Runs that differ only in their features shape as one piece of text, each run's features over its own characters, so a variant on one letter of an Arabic word keeps its neighbours' initial, medial and final forms and the letters still touch. The panel lists the alternates of the glyph the shaper makes for the selected character in its word (a medial letter shows medial variants), and a double-click applies the OpenType feature that draws the variant (`salt=N` or `ssNN`) to that character alone, in one undo step (`text.setRangeStyle`); a variant no feature reaches falls back to a replacement glyph (`text.setGlyphAlternate`), which its tooltip says may not join. Show › Entire Font also lists the glyphs no character maps, with their name or `gid N` and the feature that gives them. The text crate reads each face's `GSUB` once (alternate sets, single substitutions, the feature that reaches a glyph). Not done: alternates reached through contextual or ligature lookups, and features of the font that the style has no tag for (`cv01`–`cv99`, `hist`). Evidence: `tests::glyph_alternates` in the text crate (a fixture font with a medial teh at `salt=1` to `3`, the neighbours' forms, no gap; Kufi Murabba PIIIX when it is installed), `features::tests::feature_values_are_parsed_validated_and_shaped`, `tests::textedit::feature_values_are_set_replaced_and_saved`, the `panels::glyphs` tests. |
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); Replace With Font lists family and style rows, one click setting both, with a search over them; its list scrolls on its own, apart from the document's font list (#4972); wherever the font menu opens, its filters keep the same four rows, and it opens at least wide enough for them and for a family name up to 220 points wide with its sample (up to 60% of the window), each sample filling its row's height (a VectorCraft layout). |
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); Replace With Font lists family and style rows, one click setting both, with a search over them; its list scrolls on its own, apart from the document's font list (#4972); wherever the font menu opens, its filters keep the same four rows, and it opens at least wide enough for them and for a family name up to 220 points wide with its sample (up to 60% of the window), each sample filling its row's height (a VectorCraft layout); a filter chosen in the font menu of the Character panel, the Control bar or the Properties panel is on in the others, and the filters are cleared when the app quits ([Font browser overview](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/font-options-overview.html)); the font menus of the Glyphs panel and of the dialogs that pick a font share the same filters (a VectorCraft choice). |
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |
| Variable fonts | done | named instances as styles; Character panel › Variable Font sets each axis (`fontAxes`); the font menu's Font Technology › Variable lists only variable font families, with the script, kind and ★ filters ([Find, apply, and adjust variable fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-apply-and-adjust-variable-fonts.html)); a family with a variable file beside static ones counts as variable. Evidence: `font_menu::tests::the_variable_filter_lists_only_variable_families`, `tests_axes::a_family_with_variation_axes_is_variable`, `tests_sysfonts::a_family_with_a_variable_file_beside_static_ones_is_variable` |