mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
M4: weight steps follow the family's order and keep widths and oblique slants (#1831)
Review follow-up. The step now finds the chars' style in the family's list
(the style menu's order, lightest first) and takes its neighbour, so a style
whose name gives no distinct weight (Semilight) is reachable; a style the
family lacks or a custom wght value still goes by weight, ties going to the
nearer one in that order. "Extra", "Ultra", "Semi" and "Demi" are weight
words only before a weight ("Extra Bold"), so "Extra Condensed" keeps its
width; Oblique names count as slanted for the current style too; W3-style
names are weights; and style_weight reads Hairline as 100, not 400. The
installed-font test picks a family the step actually moves.
This commit is contained in:
1 parent
6365e4339b
commit
dc82e6cac9
4 files changed
+86
-23
No files matched your search
@@ -223,6 +223,7 @@ fn text_case() {
|
||||
assert_eq!(style_weight("Semi Bold Italic"), 600);
|
||||
assert_eq!(style_weight("Black"), 900);
|
||||
assert_eq!(style_weight("Regular"), 400);
|
||||
assert_eq!(style_weight("Hairline"), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -389,8 +389,9 @@ pub fn style_weight(style: &str) -> u16 {
|
||||
{
|
||||
return n;
|
||||
}
|
||||
let table: [(&str, u16); 10] = [
|
||||
let table: [(&str, u16); 11] = [
|
||||
("thin", 100),
|
||||
("hairline", 100),
|
||||
("extralight", 200),
|
||||
("ultralight", 200),
|
||||
("light", 300),
|
||||
|
||||
@@ -122,14 +122,23 @@ fn toggle_italic(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// What a style name says besides its weight and slant ("Narrow Bold Italic" → "narrow").
|
||||
/// Hiragino's weight names: "W3" is 300.
|
||||
fn w_number(word: &str) -> Option<u16> {
|
||||
let n = word.strip_prefix('w')?.parse::<u16>().ok()?;
|
||||
(1..=9).contains(&n).then_some(n * 100)
|
||||
}
|
||||
|
||||
/// What a style name says besides its weight and slant ("Narrow Bold Italic" → "narrow",
|
||||
/// "Extra Condensed Bold" → "extra condensed").
|
||||
fn style_rest(style: &str) -> String {
|
||||
const WORDS: [&str; 25] = [
|
||||
const WEIGHTS: [&str; 25] = [
|
||||
"thin",
|
||||
"hairline",
|
||||
"extralight",
|
||||
"ultralight",
|
||||
"light",
|
||||
"semilight",
|
||||
"demilight",
|
||||
"regular",
|
||||
"normal",
|
||||
"roman",
|
||||
@@ -141,40 +150,64 @@ fn style_rest(style: &str) -> String {
|
||||
"extrabold",
|
||||
"ultrabold",
|
||||
"black",
|
||||
"extrablack",
|
||||
"ultrablack",
|
||||
"heavy",
|
||||
"semi",
|
||||
"demi",
|
||||
"extra",
|
||||
"ultra",
|
||||
"italic",
|
||||
"oblique",
|
||||
"plain",
|
||||
"standard",
|
||||
];
|
||||
// Words that grade the weight word after them ("Extra Bold") but a width otherwise ("Extra Condensed").
|
||||
const GRADES: [&str; 4] = ["extra", "ultra", "semi", "demi"];
|
||||
let s = style.to_ascii_lowercase().replace('-', " ");
|
||||
let words = s.split_whitespace().filter(|w| !WORDS.contains(w) && !w.chars().all(|c| c.is_ascii_digit()));
|
||||
words.collect::<Vec<_>>().join(" ")
|
||||
let words: Vec<&str> = s.split_whitespace().collect();
|
||||
let weight = |w: &str| WEIGHTS.contains(&w) || w_number(w).is_some() || w.chars().all(|c| c.is_ascii_digit());
|
||||
let rest = words.iter().enumerate().filter(|(i, w)| {
|
||||
let graded = GRADES.contains(w) && words.get(i + 1).is_some_and(|next| weight(next) && !matches!(*next, "italic" | "oblique"));
|
||||
!weight(w) && !graded
|
||||
});
|
||||
rest.map(|(_, w)| *w).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// A style name that slants: italic, or oblique as some families name it.
|
||||
fn slanted(style: &str) -> bool {
|
||||
let s = style.to_ascii_lowercase();
|
||||
s.contains("italic") || s.contains("oblique")
|
||||
}
|
||||
|
||||
/// The next heavier (`up`) or lighter style of `styles` (a family's) after the chars' style: the
|
||||
/// nearest weight with the same slant and width, as the style menu lists them. None when the family
|
||||
/// has nothing further that way.
|
||||
/// The weight a style name says ("W3" too).
|
||||
fn name_weight(style: &str) -> u16 {
|
||||
w_number(&style.to_ascii_lowercase()).unwrap_or_else(|| figcraft_doc::style_weight(style))
|
||||
}
|
||||
|
||||
/// The next heavier (`up`) or lighter style of `styles` (a family's, lightest first, as the style
|
||||
/// menu lists them) after the chars' style, with the same slant and width. The chars' style is
|
||||
/// found in the list by name and its neighbour taken; a style the family doesn't have, or a custom
|
||||
/// weight axis value, goes to the nearest weight that way. None when the family has nothing further.
|
||||
pub(crate) fn next_style(styles: &[String], st: &TextStyle, up: bool) -> Option<String> {
|
||||
let (w, italic, rest) = (i32::from(st.weight()), st.italic(), style_rest(&st.font_style));
|
||||
styles
|
||||
.iter()
|
||||
.filter(|x| slanted(x) == italic && style_rest(x) == rest)
|
||||
.map(|x| (i32::from(figcraft_doc::style_weight(x)) - w, x))
|
||||
let italic = st.italic() || slanted(&st.font_style);
|
||||
let rest = style_rest(&st.font_style);
|
||||
let mut same: Vec<&String> = styles.iter().filter(|x| slanted(x) == italic && style_rest(x) == rest).collect();
|
||||
let norm = |s: &str| s.to_lowercase().replace([' ', '-', '_'], "");
|
||||
let here = same.iter().position(|x| norm(x) == norm(&st.font_style));
|
||||
if let Some(i) = here.filter(|_| !st.variations.contains_key("wght")) {
|
||||
let next = if up { i.checked_add(1) } else { i.checked_sub(1) };
|
||||
return next.and_then(|n| same.get(n)).map(|x| (*x).clone());
|
||||
}
|
||||
let w = i32::from(st.weight());
|
||||
// Of equal weights, the one nearest in the family's order: the first going up, the last going down.
|
||||
if !up {
|
||||
same.reverse();
|
||||
}
|
||||
same.into_iter()
|
||||
.map(|x| (i32::from(name_weight(x)) - w, x))
|
||||
.filter(|(d, _)| if up { *d > 0 } else { *d < 0 })
|
||||
.min_by_key(|(d, _)| d.abs())
|
||||
.map(|(_, x)| x.clone())
|
||||
}
|
||||
|
||||
/// `text.weightUp` / `text.weightDown`: each targeted run moves to the next style of its family.
|
||||
fn step_weight(s: &mut Session, p: &Value, up: bool) -> Result<Value> {
|
||||
style(s, p, |st| {
|
||||
let styles = figcraft_text::family_styles(&st.font_family);
|
||||
|
||||
@@ -38,11 +38,39 @@ fn the_steps_follow_the_family_with_the_same_slant_and_width() {
|
||||
assert_eq!(walk(&arial, "Narrow Bold Italic", false), ["Narrow Italic"]);
|
||||
let segoe = styles(&["Light", "Semilight", "Regular", "Italic", "Semibold", "Bold", "Black", "Bold Italic"]);
|
||||
assert_eq!(walk(&segoe, "Regular", true), ["Semibold", "Bold", "Black"]);
|
||||
assert_eq!(walk(&segoe, "Regular", false), ["Light"]);
|
||||
assert_eq!(walk(&segoe, "Regular", false), ["Semilight", "Light"]);
|
||||
// A style the family doesn't have (as the old steps stored) moves to the next real one.
|
||||
assert_eq!(next_style(&segoe, &at("Medium"), true).as_deref(), Some("Semibold"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_and_semi_before_a_width_keep_the_width() {
|
||||
let family = styles(&["Extra Condensed Regular", "Condensed Regular", "Condensed Bold", "Extra Condensed Bold", "Semi Condensed Extra Bold"]);
|
||||
assert_eq!(walk(&family, "Extra Condensed Regular", true), ["Extra Condensed Bold"]);
|
||||
assert_eq!(walk(&family, "Condensed Bold", false), ["Condensed Regular"]);
|
||||
assert_eq!(walk(&family, "Semi Condensed Extra Bold", false), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oblique_names_step_to_oblique_faces() {
|
||||
let dejavu = styles(&["Book", "Oblique", "Bold", "Bold Oblique"]);
|
||||
assert_eq!(walk(&dejavu, "Oblique", true), ["Bold Oblique"]);
|
||||
assert_eq!(walk(&dejavu, "Bold Oblique", false), ["Oblique"]);
|
||||
assert_eq!(walk(&dejavu, "Book", true), ["Bold"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hairline_and_w_numbers_are_weights() {
|
||||
let family = styles(&["Hairline", "Thin", "Regular", "Bold"]);
|
||||
assert_eq!(walk(&family, "Regular", false), ["Thin", "Hairline"]);
|
||||
// Off the list (a stored style the family lacks): by weight, the nearer of two equal ones.
|
||||
assert_eq!(next_style(&family, &at("Extra Light"), false).as_deref(), Some("Thin"));
|
||||
assert_eq!(next_style(&family, &at("Extra Light"), true).as_deref(), Some("Regular"));
|
||||
let hiragino = styles(&["W3", "W6"]);
|
||||
assert_eq!(walk(&hiragino, "W3", true), ["W6"]);
|
||||
assert_eq!(next_style(&hiragino, &at("Bold"), false).as_deref(), Some("W6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_custom_weight_axis_value_steps_from_where_it_is() {
|
||||
let family = styles(&["Light", "Regular", "Medium", "Bold"]);
|
||||
@@ -59,19 +87,19 @@ fn font_style(s: &Session, t: NodeId) -> String {
|
||||
#[test]
|
||||
fn the_commands_land_on_styles_the_family_has() {
|
||||
figcraft_text::scan_system_fonts();
|
||||
// Any installed family with a regular face and a heavier upright one (CI machines differ).
|
||||
let upright = |x: &String| !x.to_lowercase().contains("italic") && !x.to_lowercase().contains("oblique");
|
||||
// Any installed family with a Regular face and a style the step takes it to (CI machines differ).
|
||||
let pick = figcraft_text::family_names().into_iter().find_map(|f| {
|
||||
let st = figcraft_text::family_styles(&f);
|
||||
(st.iter().any(|x| x == "Regular") && st.iter().any(|x| upright(x) && figcraft_doc::style_weight(x) > 400)).then_some((f, st))
|
||||
let up = next_style(&st, &at("Regular"), true).filter(|_| st.iter().any(|x| x == "Regular"))?;
|
||||
Some((f, st, up))
|
||||
});
|
||||
let Some((family, styles)) = pick else { return };
|
||||
let Some((family, styles, heavier)) = pick else { return };
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("node.create", &json!({"type": "text", "text": "Hi", "props": {"fontFamily": family, "fontStyle": "Regular"}})).unwrap();
|
||||
let t = crate::params::id_of(&r["id"]).unwrap();
|
||||
s.execute("text.weightUp", &json!({"ids": [t]})).unwrap();
|
||||
let up = font_style(&s, t);
|
||||
assert!(styles.contains(&up) && figcraft_doc::style_weight(&up) > 400, "{family}: {up} of {styles:?}");
|
||||
assert_eq!(up, heavier, "{family}: {styles:?}");
|
||||
s.execute("text.weightDown", &json!({"ids": [t]})).unwrap();
|
||||
assert_eq!(font_style(&s, t), "Regular", "{family}: {styles:?}");
|
||||
// A family that isn't installed still steps by 100 under the canonical names.
|
||||
|
||||
Reference in new issue
Block a user