Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,9 +34,8 @@ jobs:
# fonts-dejavu-core: font *files*, not just the libraries. A bare runner has none,
# and usvg silently drops an <svg>'s <text> 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
Expand All @@ -53,7 +56,8 @@ jobs:
# fonts-dejavu-core: font *files*, not just the libraries. A bare runner has none,
# and usvg silently drops an <svg>'s <text> 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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 35 additions & 15 deletions crates/rustmotion-components/src/caption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 5 additions & 19 deletions crates/rustmotion-components/src/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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,
Expand All @@ -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)?;
Expand Down
16 changes: 5 additions & 11 deletions crates/rustmotion-components/src/gradient_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 9 additions & 18 deletions crates/rustmotion-components/src/intrinsic.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -39,7 +41,7 @@ pub struct TextIntrinsic {
font_family: Option<String>,
font_size: f32,
line_height_resolved: f32,
weight: u16,
weight: Weight,
italic: bool,
letter_spacing: f32,
max_width: Option<f32>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Typeface> {
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 5 additions & 19 deletions crates/rustmotion-components/src/number_wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -165,24 +163,12 @@ impl NumberWheel {

pub(crate) fn build_font(&self, font_size: f32) -> Option<Font> {
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,
Expand Down
Loading
Loading