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
9 changes: 1 addition & 8 deletions crates/rustmotion-components/src/arrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,37 +85,30 @@ rustmotion_core::impl_traits!(Arrow {
});

impl Arrow {
/// Build the bezier path for this arrow (without arrowheads).
fn build_path(&self) -> Path {
let mut path = PathBuilder::new();
path.move_to((self.x1, self.y1));

if let (Some(cp1), Some(cp2)) = (&self.cp1, &self.cp2) {
// Cubic bezier
path.cubic_to((cp1.x, cp1.y), (cp2.x, cp2.y), (self.x2, self.y2));
} else if let Some(cp) = &self.cp {
// Quadratic bezier
path.quad_to((cp.x, cp.y), (self.x2, self.y2));
} else if let Some(curve) = self.curve {
// Auto-generate a quadratic control point based on curve intensity
let mid_x = (self.x1 + self.x2) / 2.0;
let mid_y = (self.y1 + self.y2) / 2.0;
let dx = self.x2 - self.x1;
let dy = self.y2 - self.y1;
let len = (dx * dx + dy * dy).sqrt();
// Perpendicular offset
let perp_x = -dy / len * curve * len * 0.3;
let perp_y = dx / len * curve * len * 0.3;
path.quad_to((mid_x + perp_x, mid_y + perp_y), (self.x2, self.y2));
} else {
// Straight line
path.line_to((self.x2, self.y2));
}

path.detach()
}

/// Draw an arrowhead at the given position along the path.
fn draw_arrowhead(
canvas: &Canvas,
path: &Path,
Expand Down Expand Up @@ -144,7 +137,7 @@ impl Arrow {
};

let angle = tangent.y.atan2(tangent.x);
let half_angle = std::f32::consts::PI / 6.0; // 30 degrees
let half_angle = std::f32::consts::PI / 6.0;

let mut arrow_path = PathBuilder::new();
arrow_path.move_to(pos);
Expand Down
3 changes: 0 additions & 3 deletions crates/rustmotion-components/src/audio_spectrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,10 @@ impl AudioSpectrum {

let n = self.bars.max(1) as usize;

// Empty cache / missing track → all-zero values; the painter clamps
// each bar to `min_height`, so this degrades to a flat baseline.
let Some(analysis) = analysis else {
return vec![0.0; n];
};

// Resample from 16 bands to n bars
let num_bands = 16usize;
(0..n)
.map(|i| {
Expand Down
5 changes: 0 additions & 5 deletions crates/rustmotion-components/src/avatar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ impl Avatar {
let w = layout_w;
let h = layout_h;

// Load image
let cache = asset_cache();
let img = if let Some(cached) = cache.get(&self.src) {
cached.clone()
Expand All @@ -98,7 +97,6 @@ impl Avatar {
decoded
};

// Circular clip + draw image with cover fit
let oval_rect = Rect::from_xywh(0.0, 0.0, w, h);
let oval_rrect = RRect::new_oval(oval_rect);

Expand All @@ -118,7 +116,6 @@ impl Avatar {

canvas.restore();

// Border
let border_width = self.border_width.unwrap_or(0.0);
if border_width > 0.0 {
if let Some(border_color) = &self.border_color {
Expand All @@ -133,7 +130,6 @@ impl Avatar {
}
}

// Status indicator
if !matches!(self.status, AvatarStatus::NoStatus) {
let dot_radius = w * 0.15;
let dot_color = self
Expand All @@ -149,7 +145,6 @@ impl Avatar {
let cy = h - dot_radius;
canvas.draw_circle((cx, cy), dot_radius, &dot_paint);

// White border around dot
let mut border_paint = paint_from_hex("#FFFFFF");
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_stroke_width(2.0);
Expand Down
9 changes: 0 additions & 9 deletions crates/rustmotion-components/src/avatar_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,15 @@ impl AvatarGroup {
let overflow = self.overflow_count();
let cache = asset_cache();

// Draw avatars in reverse order so first avatar is on top
for rev_i in (0..visible).rev() {
let avatar = &self.avatars[rev_i];
let x = rev_i as f32 * step;

// Border circle (background ring)
let mut border_paint = paint_from_hex(&self.border_color);
border_paint.set_style(PaintStyle::Fill);
border_paint.set_anti_alias(true);
canvas.draw_circle((x + s / 2.0, s / 2.0), s / 2.0, &border_paint);

// Load image
let img = if let Some(cached) = cache.get(&avatar.src) {
cached.clone()
} else {
Expand All @@ -122,7 +119,6 @@ impl AvatarGroup {
decoded
};

// Clip to circle inset by border_width
let inset = self.border_width;
let inner_r = s / 2.0 - inset;
let cx = x + s / 2.0;
Expand All @@ -135,7 +131,6 @@ impl AvatarGroup {
canvas.save();
canvas.clip_rrect(clip_rrect, skia_safe::ClipOp::Intersect, true);

// Draw image with cover fit
let img_w = img.width() as f32;
let img_h = img.height() as f32;
let d = inner_r * 2.0;
Expand All @@ -150,26 +145,22 @@ impl AvatarGroup {
canvas.restore();
}

// "+N" overflow badge
if overflow > 0 {
let x = visible as f32 * step;
let cx = x + s / 2.0;
let cy = s / 2.0;

// Background circle
let mut bg_paint = paint_from_hex("#374151");
bg_paint.set_style(PaintStyle::Fill);
bg_paint.set_anti_alias(true);
canvas.draw_circle((cx, cy), s / 2.0, &bg_paint);

// Border
let mut border_paint = paint_from_hex(&self.border_color);
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_stroke_width(self.border_width);
border_paint.set_anti_alias(true);
canvas.draw_circle((cx, cy), s / 2.0 - self.border_width / 2.0, &border_paint);

// Text
let text = format!("+{}", overflow);
let font_size = s * 0.35;
let font_style = skia_safe::FontStyle::bold();
Expand Down
34 changes: 0 additions & 34 deletions crates/rustmotion-components/src/badge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ pub enum BadgeSize {

impl BadgeSize {
fn params(&self) -> (f32, f32, f32, f32) {
// (font_size, h_padding, v_padding, icon_size)
match self {
BadgeSize::Sm => (12.0, 8.0, 4.0, 14.0),
BadgeSize::Md => (14.0, 12.0, 6.0, 18.0),
Expand Down Expand Up @@ -99,9 +98,6 @@ fn default_badge_style() -> CssStyle {
}
}

/// Deserializes `style` normally, then defaults `align-self` to
/// `flex-start` when the author didn't set it explicitly — see the doc
/// comment on [`Badge::style`].
fn deserialize_no_stretch_style<'de, D>(deserializer: D) -> Result<CssStyle, D::Error>
where
D: serde::Deserializer<'de>,
Expand All @@ -120,20 +116,13 @@ rustmotion_core::impl_traits!(Badge {
});

impl Badge {
/// Resolves `font-size` against a real per-frame viewport (`rem`/`vw`/
/// `vh` now resolve instead of silently dropping to 0px — lot B, wave
/// S). `em`/`%` on `font-size` itself remain approximate — see
/// `crate::intrinsic::font_size_ctx`'s doc comment.
fn resolved_font_size(&self, ctx: &PaintCtx) -> f32 {
self.style.font_size_px_ctx(
&crate::intrinsic::font_size_ctx(ctx.video_width as f32, ctx.video_height as f32, 0.0),
self.badge_size.params().0,
)
}

/// Returns (h_padding, v_padding, icon_size) scaled proportionally
/// to the resolved font size. If style.font_size overrides the default,
/// padding and icon scale with it.
fn resolved_params(&self, ctx: &PaintCtx) -> (f32, f32, f32) {
let (default_fs, h_pad, v_pad, icon_size) = self.badge_size.params();
let actual_fs = self.resolved_font_size(ctx);
Expand Down Expand Up @@ -161,7 +150,6 @@ impl Badge {
let h = layout_h;
let radius = h / 2.0;

// Background / outline
let rect = Rect::from_xywh(0.0, 0.0, w, h);
let rrect = RRect::new_rect_xy(rect, radius, radius);

Expand All @@ -180,7 +168,6 @@ impl Badge {
}
}

// Icon
let mut x_offset = h_pad;
if let Some(icon_id) = &self.icon {
let icon_color = if matches!(self.variant, BadgeVariant::Solid) {
Expand Down Expand Up @@ -243,7 +230,6 @@ impl Badge {
x_offset += icon_size + 6.0 * ratio;
}

// Text
let text_color = if matches!(self.variant, BadgeVariant::Solid) {
"#FFFFFF"
} else {
Expand All @@ -259,7 +245,6 @@ impl Badge {

let (_, metrics) = font.metrics();
let ascent = -metrics.ascent;
// Use cap height for visual centering (excludes descenders like g, p, y)
let cap_h = if metrics.cap_height > 0.0 {
metrics.cap_height
} else {
Expand All @@ -278,14 +263,12 @@ impl Badge {
&text_paint,
);

// Dot indicator (top-right)
if self.dot {
let dot_r = font_size * 0.3;
let dot_cx = w - dot_r * 0.5;
let dot_cy = dot_r * 0.5;
let dot_color = self.dot_color.as_deref().unwrap_or(color);

// Pulse ring animation
if self.pulse {
let phase = (time * 2.0).fract() as f32;
let pulse_r = dot_r * (1.0 + phase * 1.5);
Expand All @@ -303,7 +286,6 @@ impl Badge {
canvas.draw_circle((dot_cx, dot_cy), dot_r, &dot_paint);
}

// Count badge (top-right, outside bounds)
if let Some(count) = self.count {
let count_text = if count > 99 {
"99+".to_string()
Expand All @@ -327,15 +309,13 @@ impl Badge {
let badge_x = w - badge_w * 0.5;
let badge_y = -badge_h * 0.3;

// Red background pill
let badge_rect = Rect::from_xywh(badge_x, badge_y, badge_w, badge_h);
let badge_rrect = RRect::new_rect_xy(badge_rect, badge_h / 2.0, badge_h / 2.0);
let mut count_bg = paint_from_hex("#EF4444");
count_bg.set_style(PaintStyle::Fill);
count_bg.set_anti_alias(true);
canvas.draw_rrect(badge_rrect, &count_bg);

// Count text
let mut count_paint = paint_from_hex("#FFFFFF");
count_paint.set_anti_alias(true);
let (_, count_metrics) = count_font.metrics();
Expand Down Expand Up @@ -377,20 +357,12 @@ mod tests {

#[test]
fn style_defaults_to_flex_start_when_absent() {
// #127: `align-items: stretch` (the flex column default) was
// winning over `BadgeIntrinsic`, stretching every badge in a card
// to the container's full width. No `style` key at all in the
// JSON is the common case — this must still default away from
// stretch.
let badge = parse(r#"{"type":"badge","text":"v1"}"#);
assert_eq!(badge.style.align_self, Some(AlignSelf::FlexStart));
}

#[test]
fn style_defaults_to_flex_start_with_other_style_keys_present() {
// Same fix, but exercised through the `deserialize_with` path
// (some `style` object present, just not `align-self`) rather
// than the field-level `default` path (`style` entirely absent).
let badge = parse(r##"{"type":"badge","text":"v1","style":{"background":"#f00"}}"##);
assert_eq!(badge.style.align_self, Some(AlignSelf::FlexStart));
assert_eq!(badge.style.background_color_str(), Some("#f00"));
Expand All @@ -402,8 +374,6 @@ mod tests {
assert_eq!(badge.style.align_self, Some(AlignSelf::Center));
}

// ─── Lot B, wave S: relative `font-size` units ─────────────────────────

fn test_ctx() -> PaintCtx {
PaintCtx {
time: 0.0,
Expand All @@ -419,8 +389,6 @@ mod tests {

#[test]
fn rem_font_size_paints_visible_ink() {
// Reproduction: `font-size: "2rem"` used to resolve to 0px via the
// context-free `font_size_px_or`.
let mut badge = parse(r#"{"type":"badge","text":"v1"}"#);
badge.style.font_size = Some(rustmotion_core::css::Length::String("2rem".into()));
const W: i32 = 200;
Expand All @@ -446,8 +414,6 @@ mod tests {
skia_safe::image::CachingHint::Disallow,
);
assert!(ok, "pixel read should succeed");
// Solid variant text is always white — probe for white ink
// specifically, since the pill background paints regardless.
let text_ink = buf
.as_chunks::<4>()
.0
Expand Down
Loading
Loading