Repository navigation
font-weight 800/900 is painted as 700 while measured at its real weight, so text wraps over its next sibling when several weights are loaded #466
Description
Activity
Working on this.
Mini-spec
Sized M, not S: the collapse is in five files, not one, and measuring the other direction turned up two more divergences the issue does not mention.
The full divergence. Measurement resolves through
intrinsic.rs::weight_to_u16, painting through amatchrepeated at six sites. They disagree on three inputs, not one:declared measured painted a number >= 600 that number 700 bolder800 700 lighter300 400 gradient_textalready passes numbers through, so it is right on the first row and wrong on the other two.captionresolves straight to a skiaWeightwith noFontWeightin between. The>= 600arm is intext,caption,counter,number_wheelandrich_text.Shape. One function,
renderer::css_font_weight(Option<&CssFontWeight>) -> skia::Weight, is the only thing that turns a declared weight into a resolved one. The six paint sites and the measure site call it; none of them keeps amatch. The invariant stops being something tests check and becomes something there is only one place to express.Invariants.
- Measurement and painting resolve the same declared weight to the same number, for every input, because they call the same function.
- A numeric weight is passed through, clamped to 1..1000. Only
boldmaps to 700.
Out of scope.
shape.rs::text_in_shape_font_stylematches on the schemaFontWeight, which is a different input deserialised from a shape's embedded text config, not aCssStyle. Its mapping is already exact and it keepsFontWeight::to_skia_weight.Acceptance.
- The issue's scenario renders the title on one line with
weights: [800, 900], as it already does with[900]. font-weight: 900selects the 900 face when 800 and 900 are both registered: the painted ink mass matches the[900]render.bolderresolves to 800 andlighterto 300 on both sides.- A numeric weight below 600 is unchanged, so the fix does not move what already worked.
Verify (exit).
cargo fmt --all --check;cargo clippy --workspace --all-targets --features rustmotion/studio -- -D warnings;cargo test --workspace --features rustmotion/studio.Reproduced offline on
main(Inter 400-900 are already cached, so nothing is fetched). Title ink rows, same scenario, varying onlyfonts[0].weights:loaded title rows lines black ink px [900]436-548 1 80885 [800, 900]436-698 2 67713 [500, 900]436-698 2 46412 [400 ... 900]436-698 2 60161 [500, 900]carrying the least ink is the tie at distance 200 resolving to the first registered variant, as the issue says.Fixed in #469.
The collapse was in five files, not one, and measuring the other direction turned up two divergences the issue does not mention:
boldermeasured 800 and painted 700,lightermeasured 300 and painted 400.gradient_textwas already right on the numeric case and wrong on both keywords, so fixing only the>= 600arm would have left two thirds of it standing.All seven resolutions — six painters plus
intrinsic.rs::weight_to_u16— now go through one function,renderer::css_font_weight. The agreement between measuring and painting is no longer a rule each painter has to remember.Verified on your scenario, offline (Inter 400-900 are already cached, nothing is fetched). Title ink rows and black ink mass, varying only
fonts[0].weights:loaded before after [900]436-548, 1 line, 80885 436-548, 1 line, 80885 [800, 900]436-698, 2 lines, 67713 436-548, 1 line, 80885 [500, 900]436-698, 2 lines, 46412 436-548, 1 line, 80885 [400 … 900]436-698, 2 lines, 60161 436-548, 1 line, 80885 The identical ink mass is the part that matters: it shows the painted face is the 900 one, not just that the wrap stopped.
Four tests, none needing a font file or the network: a table over every input of
css_font_weightincluding the 1..1000 clamp; one that registers two faces at 800 and 900 and asserts 900 reaches the 900 one while asking for 700 lands on the 800 face; and two onCaption::resolve_font_style, a real paint path, over 600/700/800/900 and overbolder/lighter. The two tests that already existed there covered onlyboldand 350, which is why they never caught this. Full suite: 2150 passed, 0 failed.Left alone:
shape::text_in_shape_font_styleandrich_text's per-spanfont_weight, which take the schemaFontWeightdeserialised from JSON rather than a re-encoding of aCssStyle. Their mapping was already exact.
The
textpainter collapses every numericfont-weightof 600 or more toBold, i.e. it asks for weight 700, while the text measurer asks for the exact weight. When several weights of a family are loaded, the two resolve different font files:font-weight: 800/900is painted in the wrong weight (700, or whatever registered weight is closest to 700);crates/rustmotion-components/src/text.rs(paint):crates/rustmotion-components/src/intrinsic.rs(measure):weight: weight_to_u16(style.font_weight.as_ref()), thenWeight::from(self.weight as i32), i.e. the exact 800 or 900.Both go through
typeface_with_fallback->closest_variant, which picks the registered weight with the smallest|v.weight - requested|, the first registered one on a tie.Reproduction
{ "version": "1.0", "video": { "width": 1920, "height": 1080, "fps": 30, "background": "#FFFFFF" }, "fonts": [{ "family": "Inter", "source": "google", "weights": [800, 900] }], "scenes": [{ "duration": 1.0, "children": [{ "type": "div", "position": "absolute", "x": 0, "y": 0, "style": { "width": 1920, "height": 1080, "flex-direction": "column", "align-items": "center", "justify-content": "center" }, "children": [ { "type": "text", "content": "UN SEUL COMPTE", "style": { "font-family": "Inter", "font-weight": 900, "font-size": 150, "color": "#000000", "line-height": 1.0 } }, { "type": "text", "content": "NEXT LINE", "style": { "font-family": "Inter", "font-weight": 900, "font-size": 96, "color": "#3366CC", "line-height": 1.0 } } ] }] }] }With
"weights": [900]the title renders on one line. With[800, 900]it renders asUN SEUL/COMPTE, andCOMPTEis drawn on top ofNEXT LINE.Same scenario, varying only
fonts[0].weights(render --frame 0):nowrap)[900][400, 900][500, 900][800, 900][400 ... 900]The box is the same in every case (measured with the 900 face). Static Inter files are near-equal in advance width across weights (
UN SEUL COMPTEat 150px: 900 = 1332px, 800 = 1335px, 700 = 1335px), so the painted face is a few pixels wider than the box and the line breaks.letter-spacingplays no part: the table above is withletter-spacing: 0. The visible symptom first showed up on titles with negative tracking, which is why it looked tracking-related.Expected
The painter resolves the same weight as the measurer: a numeric
font-weightis passed through as-is (Weight::from(n)), and only thebold/bolderkeywords map to 700. The>= 600 => Boldarm looks like a leftover from a two-weight model.