-
Notifications
You must be signed in to change notification settings - Fork 131
feat(htmlcss): CSS Grid layout + box-shadow support #635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,12 @@ pub fn collect_styled_tree(html: &str) -> Result<Option<StyledElement>, String> | |
|
|
||
| let _ = thread_state::initialize(ThreadState::LAYOUT); | ||
|
|
||
| // Enable CSS Grid support in Stylo's servo mode (one-time). | ||
| // Without this, `display: grid` is not parsed (gated behind a pref). | ||
| use std::sync::Once; | ||
| static GRID_PREF: Once = Once::new(); | ||
| GRID_PREF.call_once(|| style_config::set_bool("layout.grid.enabled", true)); | ||
|
|
||
| let dom = | ||
| DemoDom::parse_from_bytes(html.as_bytes()).map_err(|e| format!("HTML parse error: {e}"))?; | ||
| let mut driver = CascadeDriver::new(&dom); | ||
|
|
@@ -509,6 +515,9 @@ fn extract_style(tag: &str, style: &ComputedValues) -> StyledElement { | |
| // Font properties (inherited) | ||
| el.font = extract_font(style); | ||
|
|
||
| // Box shadow | ||
| el.box_shadow = extract_box_shadow(style); | ||
|
|
||
| // Blend mode | ||
| el.blend_mode = extract_blend_mode(style); | ||
|
|
||
|
|
@@ -572,6 +581,21 @@ fn extract_style(tag: &str, style: &ComputedValues) -> StyledElement { | |
| el.flex_grow = style.clone_flex_grow().0; | ||
| el.flex_shrink = style.clone_flex_shrink().0; | ||
|
|
||
| // Grid container | ||
| if el.display == types::Display::Grid { | ||
| el.grid_template_columns = extract_grid_template(&style.clone_grid_template_columns()); | ||
| el.grid_template_rows = extract_grid_template(&style.clone_grid_template_rows()); | ||
| el.grid_auto_columns = extract_implicit_tracks(&style.clone_grid_auto_columns()); | ||
| el.grid_auto_rows = extract_implicit_tracks(&style.clone_grid_auto_rows()); | ||
| el.grid_auto_flow = extract_grid_auto_flow(&style.clone_grid_auto_flow()); | ||
| } | ||
|
|
||
| // Grid child | ||
| el.grid_column_start = extract_grid_placement(&style.clone_grid_column_start()); | ||
| el.grid_column_end = extract_grid_placement(&style.clone_grid_column_end()); | ||
| el.grid_row_start = extract_grid_placement(&style.clone_grid_row_start()); | ||
| el.grid_row_end = extract_grid_placement(&style.clone_grid_row_end()); | ||
|
|
||
| el | ||
| } | ||
|
|
||
|
|
@@ -924,6 +948,156 @@ fn auto_distribute_stops(raw: &mut [(Option<f32>, CGColor)]) { | |
| } | ||
| } | ||
|
|
||
| fn extract_box_shadow(style: &ComputedValues) -> Vec<BoxShadow> { | ||
| let shadows = style.clone_box_shadow(); | ||
| shadows | ||
| .0 | ||
| .iter() | ||
| .map(|s| { | ||
| let color = s | ||
| .base | ||
| .color | ||
| .as_absolute() | ||
| .map(|a| abs_color_to_cg(a)) | ||
| .unwrap_or(CGColor::BLACK); | ||
| BoxShadow { | ||
| offset_x: s.base.horizontal.px(), | ||
| offset_y: s.base.vertical.px(), | ||
| blur: s.base.blur.0.px(), | ||
| spread: s.spread.px(), | ||
| color, | ||
| inset: s.inset, | ||
| } | ||
| }) | ||
| .collect() | ||
| } | ||
|
|
||
| // ─── Grid property extraction ─────────────────────────────────────── | ||
|
|
||
| /// Convert a Stylo `GridTemplateComponent` (computed) to our IR. | ||
| /// | ||
| /// Stylo's computed grid-template uses `CSSInteger` (= `i32`) for repeat counts. | ||
| fn extract_grid_template( | ||
| tpl: &style::values::generics::grid::GenericGridTemplateComponent< | ||
| style::values::computed::LengthPercentage, | ||
| i32, | ||
| >, | ||
| ) -> Vec<types::GridTemplateEntry> { | ||
| use style::values::generics::grid::GenericGridTemplateComponent; | ||
| match tpl { | ||
| GenericGridTemplateComponent::None | GenericGridTemplateComponent::Masonry => Vec::new(), | ||
| GenericGridTemplateComponent::Subgrid(_) => Vec::new(), // subgrid not supported | ||
| GenericGridTemplateComponent::TrackList(track_list) => { | ||
| use style::values::generics::grid::GenericTrackListValue; | ||
| let mut entries = Vec::new(); | ||
| for value in track_list.values.iter() { | ||
| match value { | ||
| GenericTrackListValue::TrackSize(ts) => { | ||
| entries.push(types::GridTemplateEntry::Track(stylo_track_size(ts))); | ||
| } | ||
| GenericTrackListValue::TrackRepeat(rep) => { | ||
| use style::values::generics::grid::RepeatCount; | ||
| let count = match rep.count { | ||
| RepeatCount::Number(n) => types::RepeatCount::Count(n as u16), | ||
| RepeatCount::AutoFill => types::RepeatCount::AutoFill, | ||
| RepeatCount::AutoFit => types::RepeatCount::AutoFit, | ||
|
Comment on lines
+1000
to
+1003
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's check if the file exists and examine the relevant lines
wc -l crates/grida-canvas/src/htmlcss/collect.rsRepository: gridaco/grida Length of output: 104 🏁 Script executed: # Read the code around the first issue (lines 1000-1003)
sed -n '995,1010p' crates/grida-canvas/src/htmlcss/collect.rsRepository: gridaco/grida Length of output: 1034 🏁 Script executed: # Read the code around the second issue (lines 1089-1095)
sed -n '1080,1100p' crates/grida-canvas/src/htmlcss/collect.rsRepository: gridaco/grida Length of output: 790 🏁 Script executed: # Search for other similar unsafe casts in the file
rg 'as (u16|i16|u32|i32)' crates/grida-canvas/src/htmlcss/collect.rs -n -B2 -A2Repository: gridaco/grida Length of output: 1091 Clamp Stylo's grid integers before narrowing them. Rust's Suggested fix- RepeatCount::Number(n) => types::RepeatCount::Count(n as u16),
+ RepeatCount::Number(n) => {
+ types::RepeatCount::Count(n.clamp(1, u16::MAX as i32) as u16)
+ }
...
- let n = line.line_num.unsigned_abs() as u16;
+ let n = line.line_num.unsigned_abs().min(u16::MAX as u32) as u16;
return types::GridPlacement::Span(if n == 0 { 1 } else { n });
}
let num = line.line_num;
if num != 0 {
- return types::GridPlacement::Line(num as i16);
+ return types::GridPlacement::Line(num.clamp(i16::MIN as i32, i16::MAX as i32) as i16);
}Also applies to: 1089-1095 🤖 Prompt for AI Agents |
||
| }; | ||
| let tracks: Vec<types::TrackSize> = rep | ||
| .track_sizes | ||
| .iter() | ||
| .map(|ts| stylo_track_size(ts)) | ||
| .collect(); | ||
| entries.push(types::GridTemplateEntry::Repeat(count, tracks)); | ||
| } | ||
| } | ||
| } | ||
| entries | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Convert Stylo `ImplicitGridTracks` (grid-auto-columns/rows) to our IR. | ||
| fn extract_implicit_tracks( | ||
| tracks: &style::values::generics::grid::GenericImplicitGridTracks< | ||
| style::values::generics::grid::GenericTrackSize<style::values::computed::LengthPercentage>, | ||
| >, | ||
| ) -> Vec<types::TrackSize> { | ||
| tracks.0.iter().map(|ts| stylo_track_size(ts)).collect() | ||
| } | ||
|
|
||
| /// Convert a single Stylo `TrackSize` to our IR. | ||
| fn stylo_track_size( | ||
| ts: &style::values::generics::grid::GenericTrackSize<style::values::computed::LengthPercentage>, | ||
| ) -> types::TrackSize { | ||
| use style::values::generics::grid::GenericTrackSize; | ||
| match ts { | ||
| GenericTrackSize::Breadth(b) => types::TrackSize::Single(stylo_track_breadth(b)), | ||
| GenericTrackSize::Minmax(min_b, max_b) => { | ||
| types::TrackSize::MinMax(stylo_track_breadth(min_b), stylo_track_breadth(max_b)) | ||
| } | ||
| GenericTrackSize::FitContent(b) => types::TrackSize::FitContent(stylo_track_breadth(b)), | ||
| } | ||
| } | ||
|
|
||
| /// Convert a single Stylo `TrackBreadth` to our IR. | ||
| fn stylo_track_breadth( | ||
| b: &style::values::generics::grid::GenericTrackBreadth< | ||
| style::values::computed::LengthPercentage, | ||
| >, | ||
| ) -> types::TrackBreadth { | ||
| use style::values::generics::grid::GenericTrackBreadth; | ||
| match b { | ||
| GenericTrackBreadth::Breadth(lp) => { | ||
| if let Some(len) = lp.to_length() { | ||
| types::TrackBreadth::Px(len.px()) | ||
| } else if let Some(pct) = lp.to_percentage() { | ||
| types::TrackBreadth::Percent(pct.0) | ||
| } else { | ||
| types::TrackBreadth::Auto | ||
| } | ||
| } | ||
| GenericTrackBreadth::Fr(fr) => types::TrackBreadth::Fr(*fr), | ||
| GenericTrackBreadth::Auto => types::TrackBreadth::Auto, | ||
| GenericTrackBreadth::MinContent => types::TrackBreadth::MinContent, | ||
| GenericTrackBreadth::MaxContent => types::TrackBreadth::MaxContent, | ||
| } | ||
| } | ||
|
|
||
| /// Convert Stylo `GridAutoFlow` to our IR. | ||
| fn extract_grid_auto_flow( | ||
| flow: &style::values::specified::position::GridAutoFlow, | ||
| ) -> types::GridAutoFlow { | ||
| let is_column = flow.contains(style::values::specified::position::GridAutoFlow::COLUMN); | ||
| let is_dense = flow.contains(style::values::specified::position::GridAutoFlow::DENSE); | ||
| match (is_column, is_dense) { | ||
| (false, false) => types::GridAutoFlow::Row, | ||
| (false, true) => types::GridAutoFlow::RowDense, | ||
| (true, false) => types::GridAutoFlow::Column, | ||
| (true, true) => types::GridAutoFlow::ColumnDense, | ||
| } | ||
| } | ||
|
|
||
| /// Convert a Stylo `GridLine` (grid-column-start/end, grid-row-start/end) to our IR. | ||
| /// | ||
| /// Stylo's computed grid-line uses `CSSInteger` (= `i32`) for line numbers. | ||
| fn extract_grid_placement( | ||
| line: &style::values::generics::grid::GenericGridLine<i32>, | ||
| ) -> types::GridPlacement { | ||
| if line.is_auto() { | ||
| return types::GridPlacement::Auto; | ||
| } | ||
| if line.is_span { | ||
| let n = line.line_num.unsigned_abs() as u16; | ||
| return types::GridPlacement::Span(if n == 0 { 1 } else { n }); | ||
| } | ||
| let num = line.line_num; | ||
| if num != 0 { | ||
| return types::GridPlacement::Line(num as i16); | ||
| } | ||
| // line_num == 0 with ident only → treat as auto (named lines not supported) | ||
| types::GridPlacement::Auto | ||
| } | ||
|
|
||
| fn extract_font(style: &ComputedValues) -> FontProps { | ||
| let font = style.get_font(); | ||
| let inherited_text = style.get_inherited_text(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's see the file structure and locate the relevant code wc -l crates/grida-canvas/src/htmlcss/collect.rsRepository: gridaco/grida
Length of output: 104
🏁 Script executed:
Repository: gridaco/grida
Length of output: 1533
🏁 Script executed:
Repository: gridaco/grida
Length of output: 4073
🏁 Script executed:
Repository: gridaco/grida
Length of output: 272
🏁 Script executed:
Repository: gridaco/grida
Length of output: 2501
🏁 Script executed:
Repository: gridaco/grida
Length of output: 1144
🏁 Script executed:
Repository: gridaco/grida
Length of output: 650
🌐 Web query:
CSS box-shadow color default currentColor specification💡 Result:
In the CSS Backgrounds and Borders Module Level 3 spec, each
box-shadowitem is a<shadow>that includes an optional<color>. If the color is omitted, it defaults tocurrentColor. [1]currentColoris the keyword that resolves to the element’s computedcolorvalue (i.e., the current text color), so an unspecifiedbox-shadowcolor will match the element’scolor. [2][3]Sources: W3C spec for
box-shadowand<shadow>grammar/defaults [1], MDNbox-shadowdocs [2], MDN<color>/currentColorreference [3].Change box-shadow color fallback from black to currentColor.
Per the CSS Backgrounds and Borders spec, box-shadow color defaults to
currentColor(the element's text color) when omitted or unresolved. Falling back to black renders these shadows with incorrect color.Suggested fix
let color = s .base .color .as_absolute() .map(|a| abs_color_to_cg(a)) - .unwrap_or(CGColor::BLACK); + .unwrap_or_else(|| abs_color_to_cg(&style.get_inherited_text().color));🤖 Prompt for AI Agents