Skip to content

Commit 476a774

Browse files
authored
chore(workspace): add CI and clean formatting/clippy drift (#25)
Closes #9. rustfmt across all crates, zero clippy warnings (targeted allows where fixes would break serde compat), clippy.toml thresholds, CI workflow fmt/clippy/test with Dioxus system deps.
1 parent 909e578 commit 476a774

150 files changed

Lines changed: 5385 additions & 2128 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/dev-methodology.local.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# dev-methodology preferences (user answers — change only when the user does)
2+
commit: claude-allowed
3+
create-pr: claude-allowed
4+
merge-pr: claude-allowed # granted 2026-07-18 ("tu as l'autorisation de squash and merge toi-même")
5+
feature-merge: squash # feature → main
6+
workstream-merge: squash
7+
tracking: github-issues # parent chantier issue + one issue per workstream
8+
model-strategy: split # orchestration/review: session model (Fable); implementation sub-agents: sonnet
9+
attribution: none # never cite Claude as author/co-author in commits, PRs, or issues

‎.github/workflows/ci.yaml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
env:
9+
CARGO_TERM_COLOR: always
10+
11+
jobs:
12+
fmt:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: dtolnay/rust-toolchain@stable
17+
with:
18+
components: rustfmt
19+
- name: Check formatting
20+
run: cargo fmt --all --check
21+
22+
clippy:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- name: Install system dependencies
27+
# webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop)
28+
run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev
29+
- uses: dtolnay/rust-toolchain@stable
30+
with:
31+
components: clippy
32+
- uses: Swatinem/rust-cache@v2
33+
- name: Clippy
34+
run: cargo clippy --workspace --all-targets -- -D warnings
35+
36+
test:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
- name: Install system dependencies
41+
# webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop)
42+
run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev
43+
- uses: dtolnay/rust-toolchain@stable
44+
- uses: Swatinem/rust-cache@v2
45+
- name: Run tests
46+
run: cargo test --workspace

‎clippy.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Painter/render entry points legitimately thread many context parameters
2+
# (canvas, layout, props, ctx, …); restructuring them into bags would hurt
3+
# call-site clarity more than it helps.
4+
too-many-arguments-threshold = 16
5+
type-complexity-threshold = 400

‎crates/rustmotion-cli/src/commands/geometry.rs‎

Lines changed: 95 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
//! This walker runs the new CSS-engine pipeline (taffy + cosmic-text) so the
1818
//! geometry it checks matches what the renderer will actually paint.
1919
20+
use rustmotion::components::box_builder::build_scene_from_refs;
21+
use rustmotion::components::intrinsic::TextIntrinsic;
2022
use rustmotion::components::{ChildComponent, Component};
2123
use rustmotion::core::css::taffy_bridge::ConversionContext;
2224
use rustmotion::core::engine::box_tree::{AvailableSpace, BoxNode, IntrinsicMeasure};
2325
use rustmotion::core::engine::layout_pass::{run_layout, BoxLayout, LayoutResult};
2426
use rustmotion::engine::animator::{
2527
apply_orbits, apply_wiggles, extract_effects, resolve_animations, AnimatedProperties,
2628
};
27-
use rustmotion::components::box_builder::build_scene_from_refs;
28-
use rustmotion::components::intrinsic::TextIntrinsic;
2929
use rustmotion::engine::render;
3030
use rustmotion::schema::ResolvedScenario;
3131
use serde::Serialize;
@@ -59,6 +59,7 @@ pub enum Axis {
5959

6060
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
6161
#[serde(rename_all = "snake_case")]
62+
#[allow(clippy::enum_variant_names)] // "Overflow" postfix is load-bearing: serde output matches CLI docs
6263
pub enum ViolationKind {
6364
/// Component bbox crosses the viewport edge.
6465
ViewportOverflow,
@@ -271,7 +272,10 @@ fn check_unwrappable_text(
271272
if let Component::Text(t) = component {
272273
let nowrap = matches!(
273274
t.style.white_space,
274-
Some(rustmotion::core::css::style::WhiteSpace::Nowrap | rustmotion::core::css::style::WhiteSpace::Pre)
275+
Some(
276+
rustmotion::core::css::style::WhiteSpace::Nowrap
277+
| rustmotion::core::css::style::WhiteSpace::Pre
278+
)
275279
);
276280
if !nowrap {
277281
return;
@@ -316,7 +320,11 @@ fn check_auto_scroll(
316320
let font_size = cb.style.font_size_px_or(14.0);
317321
let actual_line_height = cb.style.line_height_for(font_size);
318322
let line_count = cb.code.lines().count().max(1) as f32;
319-
let chrome_h = if cb.chrome.as_ref().is_some_and(|c| c.enabled) { 36.0 } else { 0.0 };
323+
let chrome_h = if cb.chrome.as_ref().is_some_and(|c| c.enabled) {
324+
36.0
325+
} else {
326+
0.0
327+
};
320328
let pad = 32.0; // ~16 top + 16 bottom default
321329
let natural_h = chrome_h + pad + line_count * actual_line_height;
322330
if natural_h > bbox.h + 0.5 {
@@ -587,15 +595,25 @@ fn resolve_props_for_validation(
587595
return AnimatedProperties::default();
588596
}
589597
}
590-
let base = if let Some(start) = start_at { time - start } else { time };
598+
let base = if let Some(start) = start_at {
599+
time - start
600+
} else {
601+
time
602+
};
591603
base.max(0.0)
592604
} else {
593605
time
594606
};
595607

596608
let mut props = AnimatedProperties::default();
597609
for (preset, preset_config) in &extracted.presets {
598-
let p = resolve_animations(&[], Some(preset), Some(preset_config), anim_time, scene_duration);
610+
let p = resolve_animations(
611+
&[],
612+
Some(preset),
613+
Some(preset_config),
614+
anim_time,
615+
scene_duration,
616+
);
599617
props.merge(&p);
600618
}
601619
if !extracted.keyframes.is_empty() {
@@ -671,6 +689,37 @@ fn hint_for_animated(
671689
}
672690
}
673691

692+
/// Render a violation for human consumption (multi-line, color-free).
693+
pub fn format_violation(v: &GeometryViolation) -> String {
694+
let axis_str = match v.axis {
695+
Axis::X => "x",
696+
Axis::Y => "y",
697+
Axis::Both => "x+y",
698+
};
699+
let kind_str = match v.kind {
700+
ViolationKind::ViewportOverflow => "viewport overflow",
701+
ViolationKind::UnwrappableTextOverflow => "wrap=false but text too wide",
702+
ViolationKind::AutoScrollDisabledOverflow => "auto_scroll=false but content too tall",
703+
ViolationKind::AnimatedTextOverflow => "animation pushes content outside viewport",
704+
};
705+
format!(
706+
"ERROR: {} ({})\n view: {}, scene: {}\n path: {}\n bbox: [{:.0}, {:.0}] -> [{:.0}, {:.0}] (viewport: {}x{})\n axis: {}\n hint: {}",
707+
v.component,
708+
kind_str,
709+
v.view_index,
710+
v.scene_index,
711+
v.path,
712+
v.bbox.x,
713+
v.bbox.y,
714+
v.bbox.x + v.bbox.w,
715+
v.bbox.y + v.bbox.h,
716+
v.viewport.0,
717+
v.viewport.1,
718+
axis_str,
719+
v.hint,
720+
)
721+
}
722+
674723
#[cfg(test)]
675724
mod tests {
676725
use super::*;
@@ -698,7 +747,11 @@ mod tests {
698747
}"##;
699748
let scenario = parse(json);
700749
let violations = validate_geometry(&scenario);
701-
assert!(violations.is_empty(), "expected clean, got: {:?}", violations);
750+
assert!(
751+
violations.is_empty(),
752+
"expected clean, got: {:?}",
753+
violations
754+
);
702755
}
703756

704757
#[test]
@@ -721,12 +774,27 @@ mod tests {
721774
}"##;
722775
let scenario = parse(json);
723776
let violations = validate_geometry(&scenario);
724-
let viewport = violations.iter().find(|v| v.kind == ViolationKind::ViewportOverflow);
725-
assert!(viewport.is_some(), "missing viewport violation in {:?}", violations);
777+
let viewport = violations
778+
.iter()
779+
.find(|v| v.kind == ViolationKind::ViewportOverflow);
780+
assert!(
781+
viewport.is_some(),
782+
"missing viewport violation in {:?}",
783+
violations
784+
);
726785
let v = viewport.unwrap();
727-
assert_eq!(v.axis, Axis::X, "expected X-axis overflow, got {:?}", v.axis);
786+
assert_eq!(
787+
v.axis,
788+
Axis::X,
789+
"expected X-axis overflow, got {:?}",
790+
v.axis
791+
);
728792
assert_eq!(v.component, "shape");
729-
assert!(v.path.contains("children[0]"), "path missing index: {}", v.path);
793+
assert!(
794+
v.path.contains("children[0]"),
795+
"path missing index: {}",
796+
v.path
797+
);
730798
}
731799

732800
#[test]
@@ -751,8 +819,14 @@ mod tests {
751819
}"##;
752820
let scenario = parse(json);
753821
let violations = validate_geometry(&scenario);
754-
let v = violations.iter().find(|v| v.kind == ViolationKind::UnwrappableTextOverflow);
755-
assert!(v.is_some(), "missing UnwrappableTextOverflow in {:?}", violations);
822+
let v = violations
823+
.iter()
824+
.find(|v| v.kind == ViolationKind::UnwrappableTextOverflow);
825+
assert!(
826+
v.is_some(),
827+
"missing UnwrappableTextOverflow in {:?}",
828+
violations
829+
);
756830
let v = v.unwrap();
757831
assert_eq!(v.component, "text");
758832
assert_eq!(v.axis, Axis::X);
@@ -801,39 +875,14 @@ mod tests {
801875
}"##;
802876
let scenario = parse(json);
803877
let violations = validate_geometry(&scenario);
804-
let v = violations.iter().find(|v| v.kind == ViolationKind::AutoScrollDisabledOverflow);
805-
assert!(v.is_some(), "missing AutoScrollDisabledOverflow in {:?}", violations);
878+
let v = violations
879+
.iter()
880+
.find(|v| v.kind == ViolationKind::AutoScrollDisabledOverflow);
881+
assert!(
882+
v.is_some(),
883+
"missing AutoScrollDisabledOverflow in {:?}",
884+
violations
885+
);
806886
assert_eq!(v.unwrap().component, "codeblock");
807887
}
808888
}
809-
810-
/// Render a violation for human consumption (multi-line, color-free).
811-
pub fn format_violation(v: &GeometryViolation) -> String {
812-
let axis_str = match v.axis {
813-
Axis::X => "x",
814-
Axis::Y => "y",
815-
Axis::Both => "x+y",
816-
};
817-
let kind_str = match v.kind {
818-
ViolationKind::ViewportOverflow => "viewport overflow",
819-
ViolationKind::UnwrappableTextOverflow => "wrap=false but text too wide",
820-
ViolationKind::AutoScrollDisabledOverflow => "auto_scroll=false but content too tall",
821-
ViolationKind::AnimatedTextOverflow => "animation pushes content outside viewport",
822-
};
823-
format!(
824-
"ERROR: {} ({})\n view: {}, scene: {}\n path: {}\n bbox: [{:.0}, {:.0}] -> [{:.0}, {:.0}] (viewport: {}x{})\n axis: {}\n hint: {}",
825-
v.component,
826-
kind_str,
827-
v.view_index,
828-
v.scene_index,
829-
v.path,
830-
v.bbox.x,
831-
v.bbox.y,
832-
v.bbox.x + v.bbox.w,
833-
v.bbox.y + v.bbox.h,
834-
v.viewport.0,
835-
v.viewport.1,
836-
axis_str,
837-
v.hint,
838-
)
839-
}

‎crates/rustmotion-cli/src/commands/info.rs‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ pub fn cmd_info(input: &PathBuf) -> Result<()> {
1616
let total_layers: usize = all_scenes.iter().map(|s| s.children.len()).sum();
1717

1818
println!("File: {}", input.display());
19-
println!("Resolution: {}x{}", scenario.video.width, scenario.video.height);
19+
println!(
20+
"Resolution: {}x{}",
21+
scenario.video.width, scenario.video.height
22+
);
2023
println!("FPS: {}", fps);
2124
println!("Duration: {:.1}s ({} frames)", total_duration, total_frames);
2225
println!("Views: {}", scenario.views.len());
@@ -29,7 +32,12 @@ pub fn cmd_info(input: &PathBuf) -> Result<()> {
2932
schema::ViewType::Slide => "Slide",
3033
schema::ViewType::World => "World",
3134
};
32-
println!(" View {}: {} ({} scenes)", vi + 1, vtype, view.scenes.len());
35+
println!(
36+
" View {}: {} ({} scenes)",
37+
vi + 1,
38+
vtype,
39+
view.scenes.len()
40+
);
3341
for (si, scene) in view.scenes.iter().enumerate() {
3442
let scene_frames = (scene.duration * fps as f64).round() as u32;
3543
println!(

0 commit comments

Comments
 (0)