diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7cc4785..782b4fd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,9 +13,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22 - with: - components: rustfmt + # The version is `rust-toolchain.toml` and nothing else. `rustup toolchain + # install` with no argument reads that file, so there is one place to bump + # and no way for CI to test a version the working tree does not. Installing + # the floating `stable` channel instead is what reddened a green main on + # 2026-10-01: rustc 1.99.0 sharpened a clippy lint and no commit had changed. + - name: Install the pinned toolchain + run: rustup toolchain install - name: Check formatting run: cargo fmt --all --check @@ -30,9 +34,8 @@ jobs: # fonts-dejavu-core: font *files*, not just the libraries. A bare runner has none, # and usvg silently drops an 's when no face resolves. run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libxkbcommon-x11-dev libwayland-dev libxcb-cursor-dev libasound2-dev fonts-dejavu-core - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22 - with: - components: clippy + - name: Install the pinned toolchain + run: rustup toolchain install - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Clippy # `--features rustmotion/studio` n'est pas décoratif : le studio est @@ -53,7 +56,8 @@ jobs: # fonts-dejavu-core: font *files*, not just the libraries. A bare runner has none, # and usvg silently drops an 's when no face resolves. run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libxkbcommon-x11-dev libwayland-dev libxcb-cursor-dev libasound2-dev fonts-dejavu-core - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22 + - name: Install the pinned toolchain + run: rustup toolchain install - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Run tests # Même raison qu'au job clippy : sans le feature, les tests du studio @@ -64,7 +68,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22 + - name: Install the pinned toolchain + run: rustup toolchain install - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Install cargo-audit run: cargo install cargo-audit --locked diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 96c7da2..ff82174 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -23,7 +23,8 @@ jobs: # asound: required by cpal, which rodio pulls in for preview audio run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libxkbcommon-x11-dev libwayland-dev libxcb-cursor-dev libasound2-dev - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22 + - name: Install the pinned toolchain + run: rustup toolchain install # La version se lit via `cargo metadata`, pas en grepant un manifeste : elle # est déclarée dans `[workspace.package]` et héritée, donc un `grep diff --git a/crates/rustmotion-components/src/caption.rs b/crates/rustmotion-components/src/caption.rs index be4764e..ce62d54 100644 --- a/crates/rustmotion-components/src/caption.rs +++ b/crates/rustmotion-components/src/caption.rs @@ -2,17 +2,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, FontStyle, Rect}; -use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, - WhiteSpace as CssWhiteSpace, -}; +use rustmotion_core::css::style::{FontStyle as CssFontStyle, WhiteSpace as CssWhiteSpace}; use rustmotion_core::css::units::LengthContext; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, - subpixel_font, typeface_with_fallback, + css_font_weight, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, subpixel_font, typeface_with_fallback, }; use rustmotion_core::schema::{CaptionStyle, CaptionWord, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -289,14 +286,7 @@ impl Caption { } fn resolve_font_style(style: &CssStyle) -> FontStyle { - let weight = match &style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { - skia_safe::font_style::Weight::BOLD - } - Some(CssFontWeight::Number(n)) if *n >= 600 => skia_safe::font_style::Weight::BOLD, - Some(CssFontWeight::Number(n)) => skia_safe::font_style::Weight::from(*n as i32), - _ => skia_safe::font_style::Weight::NORMAL, - }; + let weight = css_font_weight(style.font_weight.as_ref()); let slant = match style.font_style { Some(CssFontStyle::Italic) => skia_safe::font_style::Slant::Italic, Some(CssFontStyle::Oblique) => skia_safe::font_style::Slant::Oblique, @@ -338,7 +328,7 @@ fn ease_out_back(t: f32) -> f32 { #[cfg(test)] mod tests { use super::*; - use rustmotion_core::css::style::CssStyle; + use rustmotion_core::css::style::{CssStyle, FontWeight as CssFontWeight, FontWeightKw}; use rustmotion_core::css::Length; use rustmotion_core::schema::CaptionWord; @@ -661,6 +651,36 @@ mod tests { assert_eq!(*Caption::resolve_font_style(&numeric).weight(), 350); } + #[test] + fn resolve_font_style_does_not_collapse_the_heavy_weights_to_bold() { + for declared in [600u16, 700, 800, 900] { + let style = CssStyle { + font_weight: Some(CssFontWeight::Number(declared)), + ..Default::default() + }; + assert_eq!( + *Caption::resolve_font_style(&style).weight(), + i32::from(declared), + "a numeric font-weight must be passed through, not rounded to 700" + ); + } + } + + #[test] + fn resolve_font_style_reads_bolder_and_lighter_as_the_measurer_does() { + let bolder = CssStyle { + font_weight: Some(CssFontWeight::Keyword(FontWeightKw::Bolder)), + ..Default::default() + }; + assert_eq!(*Caption::resolve_font_style(&bolder).weight(), 800); + + let lighter = CssStyle { + font_weight: Some(CssFontWeight::Keyword(FontWeightKw::Lighter)), + ..Default::default() + }; + assert_eq!(*Caption::resolve_font_style(&lighter).weight(), 300); + } + #[test] fn resolve_font_style_honours_italic() { let italic = CssStyle { diff --git a/crates/rustmotion-components/src/counter.rs b/crates/rustmotion-components/src/counter.rs index 91f1088..bbbb308 100644 --- a/crates/rustmotion-components/src/counter.rs +++ b/crates/rustmotion-components/src/counter.rs @@ -3,18 +3,16 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, FontStyle, PaintStyle}; -use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, -}; +use rustmotion_core::css::style::{FontStyle as CssFontStyle, TextAlign as CssTextAlign}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, format_counter_value, measure_text_with_fallback, - paint_from_hex, subpixel_font, typeface_with_fallback, + css_font_weight, draw_text_with_fallback, emoji_typeface, format_counter_value, + measure_text_with_fallback, paint_from_hex, subpixel_font, typeface_with_fallback, }; use rustmotion_core::schema::{ - EasingType, FontStyleType, FontWeight, Stroke, TextAlign, TextShadow, TimelineStep, + EasingType, FontStyleType, Stroke, TextAlign, TextShadow, TimelineStep, }; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -104,14 +102,6 @@ impl Counter { .as_deref() .unwrap_or_else(|| self.style.color_str_or("#FFFFFF")); let font_family = self.style.font_family_or("Inter"); - let font_weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { - FontWeight::Bold - } - Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, - Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), - _ => FontWeight::Normal, - }; let font_style_type = match self.style.font_style { Some(CssFontStyle::Italic) => FontStyleType::Italic, Some(CssFontStyle::Oblique) => FontStyleType::Oblique, @@ -138,11 +128,7 @@ impl Counter { FontStyleType::Italic => skia_safe::font_style::Slant::Italic, FontStyleType::Oblique => skia_safe::font_style::Slant::Oblique, }; - let weight = match font_weight { - FontWeight::Bold => skia_safe::font_style::Weight::BOLD, - FontWeight::Normal => skia_safe::font_style::Weight::NORMAL, - FontWeight::Weight(w) => skia_safe::font_style::Weight::from(w as i32), - }; + let weight = css_font_weight(self.style.font_weight.as_ref()); let skia_font_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); let typeface = typeface_with_fallback(font_family, skia_font_style)?; diff --git a/crates/rustmotion-components/src/gradient_text.rs b/crates/rustmotion-components/src/gradient_text.rs index 1f404e2..97f4f66 100644 --- a/crates/rustmotion-components/src/gradient_text.rs +++ b/crates/rustmotion-components/src/gradient_text.rs @@ -4,15 +4,15 @@ use skia_safe::gradient::{self, Colors, Gradient}; use skia_safe::{Canvas, Color4f, FontStyle, Point}; use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, - TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, + FontStyle as CssFontStyle, TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, }; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, - parse_hex_color, subpixel_font, typeface_with_fallback, wrap_text_with_tracking, + css_font_weight, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, parse_hex_color, subpixel_font, typeface_with_fallback, + wrap_text_with_tracking, }; use rustmotion_core::schema::{TextAlign, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -80,13 +80,7 @@ impl GradientText { Some(CssFontStyle::Oblique) => skia_safe::font_style::Slant::Oblique, _ => skia_safe::font_style::Slant::Upright, }; - let weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { - skia_safe::font_style::Weight::BOLD - } - Some(CssFontWeight::Number(n)) => skia_safe::font_style::Weight::from(*n as i32), - _ => skia_safe::font_style::Weight::NORMAL, - }; + let weight = css_font_weight(self.style.font_weight.as_ref()); let skia_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); typeface_with_fallback(font_family, skia_style).ok() diff --git a/crates/rustmotion-components/src/intrinsic.rs b/crates/rustmotion-components/src/intrinsic.rs index f77e41a..c55184b 100644 --- a/crates/rustmotion-components/src/intrinsic.rs +++ b/crates/rustmotion-components/src/intrinsic.rs @@ -1,14 +1,16 @@ +use skia_safe::font_style::Weight; use skia_safe::{Font, FontStyle as SkFontStyle, Typeface}; use rustmotion_core::css::style::{ - CssStyle, FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, LineHeight, - TextAlign as CssTextAlign, WhiteSpace, TEXT_AUTOFIT_MIN_FONT_PX, + CssStyle, FontStyle as CssFontStyle, LineHeight, TextAlign as CssTextAlign, WhiteSpace, + TEXT_AUTOFIT_MIN_FONT_PX, }; use rustmotion_core::engine::box_tree::{AvailableSpace, IntrinsicMeasure}; use rustmotion_core::engine::deps::{TextMetrics, TextMetricsProvider}; use rustmotion_core::engine::renderer::{ - compute_glyph_metrics, emoji_typeface, format_counter_value, measure_text_with_fallback, - subpixel_font, typeface_with_fallback, wrap_text_with_tracking, GlyphMetric, + compute_glyph_metrics, css_font_weight, emoji_typeface, format_counter_value, + measure_text_with_fallback, subpixel_font, typeface_with_fallback, wrap_text_with_tracking, + GlyphMetric, }; use crate::badge::{Badge, BadgeSize}; @@ -39,7 +41,7 @@ pub struct TextIntrinsic { font_family: Option, font_size: f32, line_height_resolved: f32, - weight: u16, + weight: Weight, italic: bool, letter_spacing: f32, max_width: Option, @@ -81,7 +83,7 @@ impl TextIntrinsic { font_family: style.font_family.clone(), font_size, line_height_resolved, - weight: weight_to_u16(style.font_weight.as_ref()), + weight: css_font_weight(style.font_weight.as_ref()), italic: matches!(style.font_style, Some(CssFontStyle::Italic)), letter_spacing, max_width, @@ -184,8 +186,7 @@ impl TextIntrinsic { } else { skia_safe::font_style::Slant::Upright }; - let weight = skia_safe::font_style::Weight::from(self.weight as i32); - SkFontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant) + SkFontStyle::new(self.weight, skia_safe::font_style::Width::NORMAL, slant) } fn typeface(&self) -> Option { @@ -384,16 +385,6 @@ fn shrink_to_fit( lo } -fn weight_to_u16(w: Option<&CssFontWeight>) -> u16 { - match w { - Some(CssFontWeight::Keyword(FontWeightKw::Bold)) => 700, - Some(CssFontWeight::Keyword(FontWeightKw::Bolder)) => 800, - Some(CssFontWeight::Keyword(FontWeightKw::Lighter)) => 300, - Some(CssFontWeight::Keyword(FontWeightKw::Normal)) | None => 400, - Some(CssFontWeight::Number(n)) => (*n).clamp(1, 1000), - } -} - pub struct GradientTextIntrinsic(TextIntrinsic); impl GradientTextIntrinsic { diff --git a/crates/rustmotion-components/src/number_wheel.rs b/crates/rustmotion-components/src/number_wheel.rs index 9ab3567..1eb6a80 100644 --- a/crates/rustmotion-components/src/number_wheel.rs +++ b/crates/rustmotion-components/src/number_wheel.rs @@ -2,17 +2,15 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, ClipOp, Font, FontStyle, Rect}; -use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, -}; +use rustmotion_core::css::style::{FontStyle as CssFontStyle, TextAlign as CssTextAlign}; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::{ease, AnimatedProperties}; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, measure_text_with_fallback, paint_from_hex, subpixel_font, - typeface_with_fallback, + css_font_weight, draw_text_with_fallback, measure_text_with_fallback, paint_from_hex, + subpixel_font, typeface_with_fallback, }; -use rustmotion_core::schema::{EasingType, FontStyleType, FontWeight, TimelineStep}; +use rustmotion_core::schema::{EasingType, FontStyleType, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; /// How many full 0-9 revolutions a reel makes before landing. @@ -165,24 +163,12 @@ impl NumberWheel { pub(crate) fn build_font(&self, font_size: f32) -> Option { let font_family = self.style.font_family_or("Inter"); - let weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { - FontWeight::Bold - } - Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, - Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), - _ => FontWeight::Normal, - }; let slant = match self.style.font_style { Some(CssFontStyle::Italic) => skia_safe::font_style::Slant::Italic, Some(CssFontStyle::Oblique) => skia_safe::font_style::Slant::Oblique, _ => skia_safe::font_style::Slant::Upright, }; - let weight = match weight { - FontWeight::Bold => skia_safe::font_style::Weight::BOLD, - FontWeight::Normal => skia_safe::font_style::Weight::NORMAL, - FontWeight::Weight(w) => skia_safe::font_style::Weight::from(w as i32), - }; + let weight = css_font_weight(self.style.font_weight.as_ref()); let _ = FontStyleType::Normal; let typeface = typeface_with_fallback( font_family, diff --git a/crates/rustmotion-components/src/rich_text.rs b/crates/rustmotion-components/src/rich_text.rs index 760d2ea..aaa4b53 100644 --- a/crates/rustmotion-components/src/rich_text.rs +++ b/crates/rustmotion-components/src/rich_text.rs @@ -1,17 +1,17 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use skia_safe::font_style::Weight; use skia_safe::{Canvas, Font, FontStyle, Point, RRect, Rect, Typeface}; use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, - TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, + FontStyle as CssFontStyle, TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, }; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::{AnimatedProperties, ResolvedCharAnimation}; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, - subpixel_font, typeface_with_fallback, + css_font_weight, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, subpixel_font, typeface_with_fallback, }; use rustmotion_core::schema::{ FontStyleType, FontWeight, TextAlign, TextAnimGranularity, TimelineStep, @@ -102,7 +102,7 @@ rustmotion_core::impl_traits!(RichText { fn make_font( family: &str, - weight: &FontWeight, + weight: Weight, font_style_type: &FontStyleType, size: f32, ) -> Option { @@ -111,12 +111,7 @@ fn make_font( FontStyleType::Italic => skia_safe::font_style::Slant::Italic, FontStyleType::Oblique => skia_safe::font_style::Slant::Oblique, }; - let weight_val = match weight { - FontWeight::Bold => skia_safe::font_style::Weight::BOLD, - FontWeight::Normal => skia_safe::font_style::Weight::NORMAL, - FontWeight::Weight(w) => skia_safe::font_style::Weight::from(*w as i32), - }; - let skia_style = FontStyle::new(weight_val, skia_safe::font_style::Width::NORMAL, slant); + let skia_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); let typeface = typeface_with_fallback(family, skia_style).ok()?; Some(subpixel_font(typeface, size)) } @@ -135,12 +130,7 @@ fn resolve_span_fonts( ) -> Vec> { let default_color = style.color_str_or("#FFFFFF"); let default_family = style.font_family_or("Inter"); - let default_weight = match &style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => FontWeight::Bold, - Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, - Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), - _ => FontWeight::Normal, - }; + let default_weight = css_font_weight(style.font_weight.as_ref()); let default_font_style = match style.font_style { Some(CssFontStyle::Italic) => FontStyleType::Italic, Some(CssFontStyle::Oblique) => FontStyleType::Oblique, @@ -152,7 +142,10 @@ fn resolve_span_fonts( .map(|span| { let size = span.font_size.unwrap_or(default_size); let family = span.font_family.as_deref().unwrap_or(default_family); - let weight = span.font_weight.as_ref().unwrap_or(&default_weight); + let weight = span + .font_weight + .as_ref() + .map_or(default_weight, |w| Weight::from(w.to_skia_weight())); let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style); let color = span.color.as_deref().unwrap_or(default_color).to_string(); let letter_spacing = span.letter_spacing.unwrap_or(default_letter_spacing); diff --git a/crates/rustmotion-components/src/text.rs b/crates/rustmotion-components/src/text.rs index 88f7817..89a0561 100644 --- a/crates/rustmotion-components/src/text.rs +++ b/crates/rustmotion-components/src/text.rs @@ -4,19 +4,18 @@ use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, Font, FontStyle, Paint, PaintStyle, Rect}; use rustmotion_core::css::style::{ - FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, - TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, + FontStyle as CssFontStyle, TextAlign as CssTextAlign, WhiteSpace as CssWhiteSpace, }; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, - subpixel_font, typeface_with_fallback, wrap_text_with_tracking, + css_font_weight, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, subpixel_font, typeface_with_fallback, wrap_text_with_tracking, }; use rustmotion_core::schema::{ - CaretConfig, CaretShape, FontStyleType, FontWeight, Stroke, TextAlign, TextBackground, - TextShadow, TextState, TextSwapConfig, TimelineStep, + CaretConfig, CaretShape, FontStyleType, Stroke, TextAlign, TextBackground, TextShadow, + TextState, TextSwapConfig, TimelineStep, }; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -198,14 +197,6 @@ impl Text { .as_deref() .unwrap_or_else(|| self.style.color_str_or("#FFFFFF")); let font_family = self.style.font_family_or("Inter"); - let font_weight = match &self.style.font_weight { - Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => { - FontWeight::Bold - } - Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold, - Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n), - _ => FontWeight::Normal, - }; let font_style_type = match self.style.font_style { Some(CssFontStyle::Italic) => FontStyleType::Italic, Some(CssFontStyle::Oblique) => FontStyleType::Oblique, @@ -222,11 +213,7 @@ impl Text { FontStyleType::Italic => skia_safe::font_style::Slant::Italic, FontStyleType::Oblique => skia_safe::font_style::Slant::Oblique, }; - let weight = match font_weight { - FontWeight::Bold => skia_safe::font_style::Weight::BOLD, - FontWeight::Normal => skia_safe::font_style::Weight::NORMAL, - FontWeight::Weight(w) => skia_safe::font_style::Weight::from(w as i32), - }; + let weight = css_font_weight(self.style.font_weight.as_ref()); let skia_font_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); let typeface = typeface_with_fallback(font_family, skia_font_style)?; diff --git a/crates/rustmotion-core/src/engine/renderer/fonts.rs b/crates/rustmotion-core/src/engine/renderer/fonts.rs index fafbefa..837792e 100644 --- a/crates/rustmotion-core/src/engine/renderer/fonts.rs +++ b/crates/rustmotion-core/src/engine/renderer/fonts.rs @@ -2,8 +2,10 @@ use std::cell::RefCell; use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; +use skia_safe::font_style::Weight; use skia_safe::{Font, FontHinting, FontMgr, FontStyle, Typeface}; +use crate::css::style::{FontWeight as CssFontWeight, FontWeightKw}; use crate::error::{Result, RustmotionError}; use crate::schema::FontEntry; @@ -237,6 +239,16 @@ pub fn typeface_with_fallback(family: &str, style: FontStyle) -> Result) -> Weight { + Weight::from(match weight { + Some(CssFontWeight::Keyword(FontWeightKw::Bold)) => 700, + Some(CssFontWeight::Keyword(FontWeightKw::Bolder)) => 800, + Some(CssFontWeight::Keyword(FontWeightKw::Lighter)) => 300, + Some(CssFontWeight::Keyword(FontWeightKw::Normal)) | None => 400, + Some(CssFontWeight::Number(n)) => i32::from((*n).clamp(1, 1000)), + }) +} + pub fn subpixel_font(typeface: impl Into, size: impl Into>) -> Font { let mut font = Font::from_typeface(typeface, size); font.set_subpixel(true); @@ -281,6 +293,53 @@ pub fn fallback_typeface_for_char( mod tests { use super::*; + #[test] + fn css_font_weight_passes_numbers_through_and_maps_only_the_keywords() { + let cases = [ + (None, 400), + (Some(CssFontWeight::Keyword(FontWeightKw::Normal)), 400), + (Some(CssFontWeight::Keyword(FontWeightKw::Bold)), 700), + (Some(CssFontWeight::Keyword(FontWeightKw::Bolder)), 800), + (Some(CssFontWeight::Keyword(FontWeightKw::Lighter)), 300), + (Some(CssFontWeight::Number(100)), 100), + (Some(CssFontWeight::Number(350)), 350), + (Some(CssFontWeight::Number(600)), 600), + (Some(CssFontWeight::Number(800)), 800), + (Some(CssFontWeight::Number(900)), 900), + (Some(CssFontWeight::Number(0)), 1), + (Some(CssFontWeight::Number(5000)), 1000), + ]; + for (declared, expected) in cases { + assert_eq!( + *css_font_weight(declared.as_ref()), + expected, + "{declared:?} must resolve to {expected}" + ); + } + } + + #[test] + fn a_heavy_numeric_weight_selects_its_own_face_not_the_bold_one() { + let family = "RmProbeHeavyWeightFamily"; + register_custom_font_variant(family, vec![8], 800, false); + register_custom_font_variant(family, vec![9], 900, false); + + let declared = CssFontWeight::Number(900); + let resolved = *css_font_weight(Some(&declared)); + assert_eq!(resolved, 900); + assert_eq!( + custom_font_bytes(family, resolved, false), + Some(vec![9]), + "font-weight 900 must reach the face registered at 900" + ); + assert_eq!( + custom_font_bytes(family, 700, false), + Some(vec![8]), + "asking for 700, which the painter used to do for any weight >= 600, lands on \ + the 800 face instead" + ); + } + fn local_entry(path: &str) -> FontEntry { FontEntry { path: Some(path.to_string()), diff --git a/crates/rustmotion-core/src/error.rs b/crates/rustmotion-core/src/error.rs index 49a1e11..a22c705 100644 --- a/crates/rustmotion-core/src/error.rs +++ b/crates/rustmotion-core/src/error.rs @@ -1,3 +1,5 @@ +#![allow(clippy::redundant_field_names)] + use thiserror::Error; pub type Result = std::result::Result; diff --git a/crates/rustmotion/CLAUDE.md b/crates/rustmotion/CLAUDE.md index 46c404f..cf5ffb6 100644 --- a/crates/rustmotion/CLAUDE.md +++ b/crates/rustmotion/CLAUDE.md @@ -225,6 +225,16 @@ pub trait Painter { > The constructor is the only place those three settings live; calling Skia directly > puts the snapping back for that one component. +> A declared `font-weight` becomes a number through `renderer::css_font_weight`, and +> nowhere else. The mapping used to exist seven times — once for measurement, six +> times in the painters — and they disagreed: a numeric weight of 600 or more was +> painted as 700, `bolder` measured 800 and painted 700, `lighter` measured 300 and +> painted 400. A box measured on one face and painted with a wider one wraps at paint +> time while keeping a single-line height, so the second line lands on the next +> sibling. The schema `FontWeight` is still the right input where a component reads +> one from JSON (`shape`'s embedded text, `rich_text`'s per-span override); it is not +> a thing to re-encode a `CssStyle` into. + ### Structure des crates ``` diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..0fd1b3a --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.99.0" +components = ["clippy", "rustfmt"]