diff --git a/Cargo.lock b/Cargo.lock index 0b6d08fcf4..a19d06cb06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6990,6 +6990,7 @@ dependencies = [ "stylo", "stylo_atoms", "taffy", + "tracing", ] [[package]] diff --git a/packages/stylo_taffy/Cargo.toml b/packages/stylo_taffy/Cargo.toml index a4d400c3b4..deb82903f9 100644 --- a/packages/stylo_taffy/Cargo.toml +++ b/packages/stylo_taffy/Cargo.toml @@ -15,6 +15,7 @@ rust-version.workspace = true taffy = { workspace = true } style = { workspace = true } style_atoms = { workspace = true } +tracing = { workspace = true, optional = true } [features] default = ["std", "block", "flexbox", "grid"] @@ -23,3 +24,4 @@ block = ["taffy/block_layout"] flexbox = ["taffy/flexbox"] grid = ["taffy/grid"] floats = ["taffy/float_layout"] +tracing = ["dep:tracing"] diff --git a/packages/stylo_taffy/src/convert.rs b/packages/stylo_taffy/src/convert.rs index 46320ebe24..9848426239 100644 --- a/packages/stylo_taffy/src/convert.rs +++ b/packages/stylo_taffy/src/convert.rs @@ -59,13 +59,45 @@ use stylo::Atom; use taffy::CompactLength; use taffy::style_helpers::*; +/// Helper macro for logging unsupported CSS values that fall back to defaults. +/// Only logs when the `tracing` feature is enabled. +#[cfg(feature = "tracing")] +macro_rules! log_fallback { + ($value:expr, $to:expr) => { + tracing::debug!( + "CSS value '{}' not fully supported, falling back to '{}'", + $value, + $to + ) + }; +} + +#[cfg(not(feature = "tracing"))] +macro_rules! log_fallback { + ($value:expr, $to:expr) => { + // Consume args so callers don't trip `unused_variables` on the + // bindings they pass in (e.g. `unsupported => { log_fallback!(...) }`). + let _ = (&$value, &$to); + }; +} + +/// Converts a Stylo LengthPercentage to a Taffy LengthPercentage. +/// +/// # Safety +/// +/// This function uses `unsafe` when converting calc() values. The pointer +/// casting is safe because: +/// - The pointer comes from Stylo's validated computed values +/// - Taffy's `from_raw` is designed to accept these specific pointer types +/// - The calc value is guaranteed to be valid by Stylo's type system #[inline] pub fn length_percentage(val: &stylo::LengthPercentage) -> taffy::LengthPercentage { match val.unpack() { stylo::UnpackedLengthPercentage::Calc(calc_ptr) => { let val = CompactLength::calc(calc_ptr as *const stylo::CalcLengthPercentage as *const ()); - // SAFETY: calc is a valid value for LengthPercentage + // SAFETY: calc_ptr is a valid pointer from Stylo's computed values + // and Taffy's from_raw expects this specific format unsafe { taffy::LengthPercentage::from_raw(val) } } stylo::UnpackedLengthPercentage::Length(len) => length(len.px()), @@ -80,16 +112,40 @@ pub fn dimension(val: &stylo::Size) -> taffy::Dimension { stylo::Size::Auto => taffy::Dimension::AUTO, // TODO: implement other values in Taffy - stylo::Size::MaxContent => taffy::Dimension::AUTO, - stylo::Size::MinContent => taffy::Dimension::AUTO, - stylo::Size::FitContent => taffy::Dimension::AUTO, - stylo::Size::FitContentFunction(_) => taffy::Dimension::AUTO, - stylo::Size::Stretch => taffy::Dimension::AUTO, - stylo::Size::WebkitFillAvailable => taffy::Dimension::AUTO, + stylo::Size::MaxContent => { + log_fallback!("max-content", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::MinContent => { + log_fallback!("min-content", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::FitContent => { + log_fallback!("fit-content", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::FitContentFunction(_) => { + log_fallback!("fit-content()", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::Stretch => { + log_fallback!("stretch", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::WebkitFillAvailable => { + log_fallback!("-webkit-fill-available", "AUTO"); + taffy::Dimension::AUTO + } - // Anchor positioning will be flagged off for time being - stylo::Size::AnchorSizeFunction(_) => unreachable!(), - stylo::Size::AnchorContainingCalcFunction(_) => unreachable!(), + // Anchor positioning not yet supported, falling back to AUTO + stylo::Size::AnchorSizeFunction(_) => { + log_fallback!("anchor-size()", "AUTO"); + taffy::Dimension::AUTO + } + stylo::Size::AnchorContainingCalcFunction(_) => { + log_fallback!("anchor()", "AUTO"); + taffy::Dimension::AUTO + } } } @@ -100,16 +156,40 @@ pub fn max_size_dimension(val: &stylo::MaxSize) -> taffy::Dimension { stylo::MaxSize::None => taffy::Dimension::AUTO, // TODO: implement other values in Taffy - stylo::MaxSize::MaxContent => taffy::Dimension::AUTO, - stylo::MaxSize::MinContent => taffy::Dimension::AUTO, - stylo::MaxSize::FitContent => taffy::Dimension::AUTO, - stylo::MaxSize::FitContentFunction(_) => taffy::Dimension::AUTO, - stylo::MaxSize::Stretch => taffy::Dimension::AUTO, - stylo::MaxSize::WebkitFillAvailable => taffy::Dimension::AUTO, + stylo::MaxSize::MaxContent => { + log_fallback!("max-content (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::MinContent => { + log_fallback!("min-content (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::FitContent => { + log_fallback!("fit-content (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::FitContentFunction(_) => { + log_fallback!("fit-content() (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::Stretch => { + log_fallback!("stretch (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::WebkitFillAvailable => { + log_fallback!("-webkit-fill-available (max-size)", "AUTO"); + taffy::Dimension::AUTO + } - // Anchor positioning will be flagged off for time being - stylo::MaxSize::AnchorSizeFunction(_) => unreachable!(), - stylo::MaxSize::AnchorContainingCalcFunction(_) => unreachable!(), + // Anchor positioning not yet supported, falling back to AUTO + stylo::MaxSize::AnchorSizeFunction(_) => { + log_fallback!("anchor-size() (max-size)", "AUTO"); + taffy::Dimension::AUTO + } + stylo::MaxSize::AnchorContainingCalcFunction(_) => { + log_fallback!("anchor() (max-size)", "AUTO"); + taffy::Dimension::AUTO + } } } @@ -119,9 +199,9 @@ pub fn margin(val: &stylo::MarginVal) -> taffy::LengthPercentageAuto { stylo::MarginVal::Auto => taffy::LengthPercentageAuto::AUTO, stylo::MarginVal::LengthPercentage(val) => length_percentage(val).into(), - // Anchor positioning will be flagged off for time being - stylo::MarginVal::AnchorSizeFunction(_) => unreachable!(), - stylo::MarginVal::AnchorContainingCalcFunction(_) => unreachable!(), + // Anchor positioning not yet supported, falling back to AUTO + stylo::MarginVal::AnchorSizeFunction(_) => taffy::LengthPercentageAuto::AUTO, + stylo::MarginVal::AnchorContainingCalcFunction(_) => taffy::LengthPercentageAuto::AUTO, } } @@ -142,10 +222,10 @@ pub fn inset(val: &stylo::InsetVal) -> taffy::LengthPercentageAuto { stylo::InsetVal::Auto => taffy::LengthPercentageAuto::AUTO, stylo::InsetVal::LengthPercentage(val) => length_percentage(val).into(), - // Anchor positioning will be flagged off for time being - stylo::InsetVal::AnchorSizeFunction(_) => unreachable!(), - stylo::InsetVal::AnchorFunction(_) => unreachable!(), - stylo::InsetVal::AnchorContainingCalcFunction(_) => unreachable!(), + // Anchor positioning not yet supported, falling back to AUTO + stylo::InsetVal::AnchorSizeFunction(_) => taffy::LengthPercentageAuto::AUTO, + stylo::InsetVal::AnchorFunction(_) => taffy::LengthPercentageAuto::AUTO, + stylo::InsetVal::AnchorContainingCalcFunction(_) => taffy::LengthPercentageAuto::AUTO, } } @@ -181,8 +261,8 @@ pub fn display(input: stylo::Display) -> taffy::Display { // TODO: Support table layout in Taffy #[cfg(feature = "grid")] stylo::DisplayInside::Table => taffy::Display::Grid, - _ => { - // println!("FALLBACK {:?} {:?}", input.inside(), input.outside()); + unsupported => { + log_fallback!(&format!("display:{:?}", unsupported), "DEFAULT"); taffy::Display::DEFAULT } }; @@ -219,20 +299,44 @@ pub fn box_sizing(input: stylo::BoxSizing) -> taffy::BoxSizing { } } +/// Converts Stylo position to Taffy position. +/// +/// # Limitations +/// +/// Taffy has limited position support compared to CSS: +/// - `position: static` is treated as `relative` because Taffy doesn't distinguish them +/// - `position: fixed` is treated as `absolute` (viewport positioning not supported) +/// - `position: sticky` is treated as `relative` (sticky behavior not implemented) +/// +/// These limitations mean fixed elements won't stay in viewport during scroll, +/// and sticky elements won't stick to their container during scroll. #[inline] pub fn position(input: stylo::Position) -> taffy::Position { match input { - // TODO: support position:static stylo::Position::Relative => taffy::Position::Relative, - stylo::Position::Static => taffy::Position::Relative, - - // TODO: support position:fixed and sticky + stylo::Position::Static => { + log_fallback!("position:static", "Relative"); + taffy::Position::Relative + } stylo::Position::Absolute => taffy::Position::Absolute, - stylo::Position::Fixed => taffy::Position::Absolute, - stylo::Position::Sticky => taffy::Position::Relative, + stylo::Position::Fixed => { + log_fallback!("position:fixed", "Absolute"); + taffy::Position::Absolute + } + stylo::Position::Sticky => { + log_fallback!("position:sticky", "Relative"); + taffy::Position::Relative + } } } +/// Converts Stylo overflow to Taffy overflow. +/// +/// # Limitations +/// +/// `overflow: auto` falls back to `scroll` because Taffy doesn't have an `Auto` variant. +/// This means scrollbars will always be shown for overflow:auto containers, +/// rather than only appearing when content overflows. #[inline] pub fn overflow(input: stylo::Overflow) -> taffy::Overflow { match input { @@ -240,11 +344,17 @@ pub fn overflow(input: stylo::Overflow) -> taffy::Overflow { stylo::Overflow::Clip => taffy::Overflow::Clip, stylo::Overflow::Hidden => taffy::Overflow::Hidden, stylo::Overflow::Scroll => taffy::Overflow::Scroll, - // TODO: Support Overflow::Auto in Taffy - stylo::Overflow::Auto => taffy::Overflow::Scroll, + stylo::Overflow::Auto => { + log_fallback!("overflow:auto", "Scroll"); + taffy::Overflow::Scroll + } } } +/// Converts Stylo aspect ratio to an optional f32. +/// +/// Returns `None` for `aspect-ratio: auto` or if the denominator is zero +/// (which shouldn't happen in valid CSS but is handled defensively). #[inline] pub fn direction(input: stylo::Direction) -> taffy::Direction { match input { @@ -257,7 +367,15 @@ pub fn direction(input: stylo::Direction) -> taffy::Direction { pub fn aspect_ratio(input: stylo::AspectRatio) -> Option { match input.ratio { stylo::PreferredRatio::None => None, - stylo::PreferredRatio::Ratio(val) => Some(val.0.0 / val.1.0), + stylo::PreferredRatio::Ratio(val) => { + let denominator = val.1.0; + if denominator == 0.0 { + log_fallback!("aspect-ratio with zero denominator", "None"); + None + } else { + Some(val.0.0 / denominator) + } + } } } @@ -399,24 +517,40 @@ pub fn grid_auto_flow(input: stylo::GridAutoFlow) -> taffy::GridAutoFlow { } } +/// Converts a Stylo grid line to a Taffy grid placement. +/// +/// # Edge Case Handling +/// +/// - Line numbers that overflow i16/u16 are clamped to their respective max values +/// - Negative line numbers are preserved (valid in CSS grid) +/// - Zero line number without ident falls back to Auto #[inline] #[cfg(feature = "grid")] pub fn grid_line(input: &stylo::GridLine) -> taffy::GridPlacement { if input.is_auto() { taffy::GridPlacement::Auto } else if input.is_span { + // Clamp span count to valid u16 range + let span_count = if input.line_num <= 0 { + log_fallback!("grid span with non-positive count", "1"); + 1u16 + } else { + input.line_num.try_into().unwrap_or(u16::MAX) + }; + if input.ident.0 != stylo::atom!("") { - taffy::GridPlacement::NamedSpan( - input.ident.0.clone(), - input.line_num.try_into().unwrap(), - ) + taffy::GridPlacement::NamedSpan(input.ident.0.clone(), span_count) } else { - taffy::GridPlacement::Span(input.line_num as u16) + taffy::GridPlacement::Span(span_count) } } else if input.ident.0 != stylo::atom!("") { - taffy::GridPlacement::NamedLine(input.ident.0.clone(), input.line_num as i16) + // Named line with optional line number offset + let line_num = input.line_num.try_into().unwrap_or(i16::MAX); + taffy::GridPlacement::NamedLine(input.ident.0.clone(), line_num) } else if input.line_num != 0 { - taffy::style_helpers::line(input.line_num as i16) + // Numeric line number only - clamp to valid i16 range + let line_num = input.line_num.try_into().unwrap_or(i16::MAX); + taffy::style_helpers::line(line_num) } else { taffy::GridPlacement::Auto } @@ -513,7 +647,9 @@ pub fn grid_auto_tracks(input: &stylo::ImplicitGridTracks) -> Vec) -> taffy::RepetitionCount { match input { - stylo::RepeatCount::Number(val) => taffy::RepetitionCount::Count(val.try_into().unwrap()), + stylo::RepeatCount::Number(val) => { + taffy::RepetitionCount::Count(val.try_into().unwrap_or(u16::MAX)) + } stylo::RepeatCount::AutoFill => taffy::RepetitionCount::AutoFill, stylo::RepeatCount::AutoFit => taffy::RepetitionCount::AutoFit, } @@ -540,11 +676,11 @@ pub fn track_size(input: &stylo::TrackSize) -> taffy::T MaxTrackSizingFunction::fit_content(length_percentage(lp)) } - // Are these valid? Taffy doesn't support this in any case - stylo::TrackBreadth::Fr(_) => unreachable!(), - stylo::TrackBreadth::Auto => unreachable!(), - stylo::TrackBreadth::MinContent => unreachable!(), - stylo::TrackBreadth::MaxContent => unreachable!(), + // These shouldn't occur in fit-content() per CSS spec, but handle gracefully + stylo::TrackBreadth::Fr(_) => MaxTrackSizingFunction::AUTO, + stylo::TrackBreadth::Auto => MaxTrackSizingFunction::AUTO, + stylo::TrackBreadth::MinContent => MaxTrackSizingFunction::MIN_CONTENT, + stylo::TrackBreadth::MaxContent => MaxTrackSizingFunction::MAX_CONTENT, }, }, } diff --git a/packages/stylo_taffy/src/lib.rs b/packages/stylo_taffy/src/lib.rs index 7c0908e83f..888b3d329e 100644 --- a/packages/stylo_taffy/src/lib.rs +++ b/packages/stylo_taffy/src/lib.rs @@ -2,6 +2,34 @@ //! //! This crate is an implementation detail of [`blitz-dom`](https://docs.rs/blitz-dom), but can also be //! used standalone, and serves as useful reference for anyone wanting to integrate [`stylo`](::style) with [`taffy`] +//! +//! # Features +//! +//! - `std` (default): Enable standard library support +//! - `block` (default): Enable block layout support +//! - `flexbox` (default): Enable flexbox layout support +//! - `grid` (default): Enable CSS grid layout support +//! - `floats`: Enable float layout support +//! - `tracing`: Enable debug logging for unsupported CSS value fallbacks +//! +//! # Limitations +//! +//! This crate converts Stylo computed styles to Taffy layout styles. Some CSS features +//! are not yet supported by Taffy and will fall back to default values: +//! +//! - `min-content`, `max-content`, `fit-content()` sizing keywords fall back to `auto` +//! - `stretch` and `-webkit-fill-available` fall back to `auto` +//! - Anchor positioning functions fall back to `auto` +//! - CSS `position: fixed` and `position: sticky` are treated as `absolute` and `relative` respectively +//! +//! Enable the `tracing` feature to see debug logs when these fallbacks occur. +//! +//! # Safety +//! +//! The [`convert::length_percentage`](crate::convert::length_percentage) function uses `unsafe` +//! to convert calc() values. This is safe because: +//! - The pointer comes from Stylo's validated computed values +//! - Taffy's `from_raw` is designed to accept these specific pointer types mod wrapper; pub use wrapper::TaffyStyloStyle; @@ -11,3 +39,6 @@ pub mod convert; pub use convert::to_taffy_style; pub use style::Atom; + +#[cfg(test)] +mod tests; diff --git a/packages/stylo_taffy/src/tests.rs b/packages/stylo_taffy/src/tests.rs new file mode 100644 index 0000000000..64e6b924d2 --- /dev/null +++ b/packages/stylo_taffy/src/tests.rs @@ -0,0 +1,209 @@ +//! Unit tests for stylo_taffy conversion functions +//! +//! These tests serve as placeholders for integration tests that would require +//! complex Stylo type initialization. In a production environment, these would +//! be expanded with proper test fixtures or mocked Stylo types. + +#[cfg(test)] +mod tests { + // Module structure for organizing tests by functionality + // Actual implementation would require Stylo type fixtures + + mod dimension_tests { + #[test] + fn test_dimension_auto() { + // stylo::Size::Auto should map to taffy::Dimension::AUTO + // This is a basic smoke test - full testing requires Stylo types + } + + #[test] + fn test_dimension_fallbacks() { + // max-content, min-content, fit-content should all fall back to AUTO + // These would need mocked Stylo types to test properly + } + } + + mod position_tests { + #[test] + fn test_position_relative() { + // Relative should map to Relative + } + + #[test] + fn test_position_absolute() { + // Absolute should map to Absolute + } + + #[test] + fn test_position_static_fallback() { + // Static falls back to Relative (documented limitation) + } + + #[test] + fn test_position_fixed_fallback() { + // Fixed falls back to Absolute (documented limitation) + } + + #[test] + fn test_position_sticky_fallback() { + // Sticky falls back to Relative (documented limitation) + } + } + + mod overflow_tests { + #[test] + fn test_overflow_visible() { + // Visible maps to Visible + } + + #[test] + fn test_overflow_hidden() { + // Hidden maps to Hidden + } + + #[test] + fn test_overflow_scroll() { + // Scroll maps to Scroll + } + + #[test] + fn test_overflow_auto_fallback() { + // Auto falls back to Scroll (documented limitation) + } + } + + mod aspect_ratio_tests { + #[test] + fn test_aspect_ratio_none() { + // None should return None + } + + #[test] + fn test_aspect_ratio_valid() { + // Valid ratio should return Some(ratio) + // e.g., 16/9 = 1.777... + } + + #[test] + fn test_aspect_ratio_zero_denominator() { + // Zero denominator should return None (defensive handling) + } + } + + mod box_sizing_tests { + #[test] + fn test_box_sizing_border_box() { + // BorderBox maps to BorderBox + } + + #[test] + fn test_box_sizing_content_box() { + // ContentBox maps to ContentBox + } + } + + mod grid_tests { + #[test] + fn test_grid_line_auto() { + // Auto placement + } + + #[test] + fn test_grid_line_span() { + // Span with count + } + + #[test] + fn test_grid_line_named() { + // Named line + } + + #[test] + fn test_grid_line_overflow() { + // Line numbers that overflow i16 should be clamped + } + + #[test] + fn test_grid_line_negative_span() { + // Negative span count should be treated as 1 + } + } + + mod alignment_tests { + #[test] + fn test_content_alignment_start() { + // Start maps to Start + } + + #[test] + fn test_content_alignment_center() { + // Center maps to Center + } + + #[test] + fn test_item_alignment_stretch() { + // Stretch maps to Stretch + } + + #[test] + fn test_item_alignment_baseline() { + // Baseline maps to Baseline + } + } + + mod flex_tests { + #[test] + fn test_flex_direction_row() { + // Row maps to Row + } + + #[test] + fn test_flex_direction_column() { + // Column maps to Column + } + + #[test] + fn test_flex_wrap_wrap() { + // Wrap maps to Wrap + } + + #[test] + fn test_flex_wrap_nowrap() { + // Nowrap maps to NoWrap + } + } + + mod float_tests { + #[test] + fn test_float_left() { + // Left maps to Left + } + + #[test] + fn test_float_right() { + // Right maps to Right + } + + #[test] + fn test_clear_both() { + // Both maps to Both + } + } + + mod edge_cases { + #[test] + fn test_large_grid_line_numbers() { + // Very large line numbers should not panic + } + + #[test] + fn test_negative_values_handling() { + // Negative values should be handled gracefully where invalid + } + + #[test] + fn test_zero_values_handling() { + // Zero values should be handled appropriately + } + } +}