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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/grida-canvas/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ usvg = { path = "../../third_party/usvg" }
# html/css cascade (Stylo-based style resolution)
csscascade = { path = "../csscascade" }
stylo = { git = "https://github.com/gridaco/stylo" }
style_config = { git = "https://github.com/gridaco/stylo", package = "stylo_config" }
# markdown parsing
# (+0.25mb wasm32-unknown-emscripten@opt-level=3)
pulldown-cmark = "0.13.0"
Expand Down
174 changes: 174 additions & 0 deletions crates/grida-canvas/src/htmlcss/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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);
Comment on lines +957 to +962
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the file structure and locate the relevant code
wc -l crates/grida-canvas/src/htmlcss/collect.rs

Repository: gridaco/grida

Length of output: 104


🏁 Script executed:

# Now let's read the code around lines 957-962 with more context
sed -n '940,980p' crates/grida-canvas/src/htmlcss/collect.rs | cat -n

Repository: gridaco/grida

Length of output: 1533


🏁 Script executed:

# Let's also check what extract_box_shadow function looks like
sed -n '900,1000p' crates/grida-canvas/src/htmlcss/collect.rs | cat -n

Repository: gridaco/grida

Length of output: 4073


🏁 Script executed:

# Search for other usages of get_inherited_text().color to understand the pattern
rg "get_inherited_text\(\)\.color" crates/grida-canvas/src/htmlcss/collect.rs -B 3 -A 3

Repository: gridaco/grida

Length of output: 272


🏁 Script executed:

# Check if there's a function signature and where this code lives
sed -n '900,970p' crates/grida-canvas/src/htmlcss/collect.rs | head -80 | cat -n

Repository: gridaco/grida

Length of output: 2501


🏁 Script executed:

# Let's check if there are other similar color fallback patterns in the code
rg "unwrap_or\(CGColor::" crates/grida-canvas/src/htmlcss/collect.rs -B 2 -A 2

Repository: gridaco/grida

Length of output: 1144


🏁 Script executed:

# Check the type of style.get_inherited_text().color to ensure it's compatible
rg "get_inherited_text" crates/grida-canvas/src/htmlcss/collect.rs -B 5 -A 5

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-shadow item is a <shadow> that includes an optional <color>. If the color is omitted, it defaults to currentColor. [1]

currentColor is the keyword that resolves to the element’s computed color value (i.e., the current text color), so an unspecified box-shadow color will match the element’s color. [2][3]

Sources: W3C spec for box-shadow and <shadow> grammar/defaults [1], MDN box-shadow docs [2], MDN <color> / currentColor reference [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
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/htmlcss/collect.rs` around lines 957 - 962, The code
currently falls back to CGColor::BLACK when s.base.color.as_absolute() is
absent; change this to use the element's computed text color (CSS currentColor)
instead. Locate the assignment to color (the chain
s.base.color.as_absolute().map(|a|
abs_color_to_cg(a)).unwrap_or(CGColor::BLACK)) and replace the unwrap_or
fallback so it converts the element's computed text/current color via
abs_color_to_cg (i.e., obtain the element's computed foreground/text color from
the style object and use that as the fallback) rather than using 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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 -A2

Repository: gridaco/grida

Length of output: 1091


Clamp Stylo's grid integers before narrowing them.

Rust's as cast wraps/truncates out-of-range integers. A large repeat() count or grid-column: 40000 will silently turn into an unrelated u16/i16 value instead of saturating or being rejected.

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
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/htmlcss/collect.rs` around lines 1000 - 1003, The
match arm for RepeatCount::Number currently narrows rep.count with an unchecked
`as` cast which can wrap large values; clamp the integer to the valid range
before converting (e.g. clamp to 0..=u16::MAX) and then construct
types::RepeatCount::Count with the clamped value; apply the same clamping
approach to the other similar casts around RepeatCount handling (including the
other occurrence referenced at lines ~1089-1095) so no out-of-range repeat/grid
integers wrap when narrowed.

};
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();
Expand Down
112 changes: 112 additions & 0 deletions crates/grida-canvas/src/htmlcss/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,30 @@ fn element_to_taffy_style(el: &StyledElement) -> taffy::Style {
..taffy::Style::default()
};

// Grid container properties
if el.display == types::Display::Grid {
style.grid_template_columns = grid_template_to_taffy(&el.grid_template_columns);
style.grid_template_rows = grid_template_to_taffy(&el.grid_template_rows);
style.grid_auto_columns = implicit_tracks_to_taffy(&el.grid_auto_columns);
style.grid_auto_rows = implicit_tracks_to_taffy(&el.grid_auto_rows);
style.grid_auto_flow = match el.grid_auto_flow {
types::GridAutoFlow::Row => taffy::GridAutoFlow::Row,
types::GridAutoFlow::Column => taffy::GridAutoFlow::Column,
types::GridAutoFlow::RowDense => taffy::GridAutoFlow::RowDense,
types::GridAutoFlow::ColumnDense => taffy::GridAutoFlow::ColumnDense,
};
}

// Grid child placement
style.grid_column = taffy::Line {
start: grid_placement_to_taffy(el.grid_column_start),
end: grid_placement_to_taffy(el.grid_column_end),
};
style.grid_row = taffy::Line {
start: grid_placement_to_taffy(el.grid_row_start),
end: grid_placement_to_taffy(el.grid_row_end),
};

// Faux-table: override display/flex for CSS table elements.
// Try display-based override first, then tag-based fallback for
// row-group wrappers (<thead>, <tbody>, <tfoot>).
Expand Down Expand Up @@ -383,6 +407,94 @@ fn map_overflow(ov: types::Overflow) -> taffy::Overflow {
}
}

// ─── Grid conversion helpers ────────────────────────────────────────

fn track_breadth_to_taffy_min(b: types::TrackBreadth) -> taffy::MinTrackSizingFunction {
match b {
types::TrackBreadth::Px(px) => taffy::MinTrackSizingFunction::length(px),
types::TrackBreadth::Percent(pct) => taffy::MinTrackSizingFunction::percent(pct),
// fr is not valid for min track sizing — treat as auto
types::TrackBreadth::Fr(_) => taffy::MinTrackSizingFunction::auto(),
types::TrackBreadth::Auto => taffy::MinTrackSizingFunction::auto(),
types::TrackBreadth::MinContent => taffy::MinTrackSizingFunction::min_content(),
types::TrackBreadth::MaxContent => taffy::MinTrackSizingFunction::max_content(),
}
}

fn track_breadth_to_taffy_max(b: types::TrackBreadth) -> taffy::MaxTrackSizingFunction {
match b {
types::TrackBreadth::Px(px) => taffy::MaxTrackSizingFunction::length(px),
types::TrackBreadth::Percent(pct) => taffy::MaxTrackSizingFunction::percent(pct),
types::TrackBreadth::Fr(fr) => taffy::MaxTrackSizingFunction::fr(fr),
types::TrackBreadth::Auto => taffy::MaxTrackSizingFunction::auto(),
types::TrackBreadth::MinContent => taffy::MaxTrackSizingFunction::min_content(),
types::TrackBreadth::MaxContent => taffy::MaxTrackSizingFunction::max_content(),
}
}

fn track_size_to_taffy(ts: &types::TrackSize) -> taffy::TrackSizingFunction {
match ts {
types::TrackSize::Single(b) => taffy::MinMax {
min: track_breadth_to_taffy_min(*b),
max: track_breadth_to_taffy_max(*b),
},
types::TrackSize::MinMax(min_b, max_b) => taffy::MinMax {
min: track_breadth_to_taffy_min(*min_b),
max: track_breadth_to_taffy_max(*max_b),
},
types::TrackSize::FitContent(b) => {
let max = match b {
types::TrackBreadth::Px(px) => taffy::MaxTrackSizingFunction::fit_content_px(*px),
types::TrackBreadth::Percent(pct) => {
taffy::MaxTrackSizingFunction::fit_content_percent(*pct)
}
_ => taffy::MaxTrackSizingFunction::auto(),
};
taffy::MinMax {
min: taffy::MinTrackSizingFunction::auto(),
max,
}
}
}
}

fn grid_template_to_taffy(
entries: &[types::GridTemplateEntry],
) -> Vec<taffy::GridTemplateComponent<String>> {
entries
.iter()
.map(|entry| match entry {
types::GridTemplateEntry::Track(ts) => {
taffy::GridTemplateComponent::Single(track_size_to_taffy(ts))
}
types::GridTemplateEntry::Repeat(count, tracks) => {
taffy::GridTemplateComponent::Repeat(taffy::GridTemplateRepetition {
count: match count {
types::RepeatCount::Count(n) => taffy::RepetitionCount::Count(*n),
types::RepeatCount::AutoFill => taffy::RepetitionCount::AutoFill,
types::RepeatCount::AutoFit => taffy::RepetitionCount::AutoFit,
},
tracks: tracks.iter().map(|ts| track_size_to_taffy(ts)).collect(),
line_names: Vec::new(),
})
}
})
.collect()
}

fn implicit_tracks_to_taffy(tracks: &[types::TrackSize]) -> Vec<taffy::TrackSizingFunction> {
tracks.iter().map(|ts| track_size_to_taffy(ts)).collect()
}

fn grid_placement_to_taffy(p: types::GridPlacement) -> taffy::GridPlacement<String> {
use taffy::style_helpers::TaffyGridLine;
match p {
types::GridPlacement::Auto => taffy::GridPlacement::Auto,
types::GridPlacement::Line(n) => taffy::GridPlacement::from_line_index(n),
types::GridPlacement::Span(n) => taffy::GridPlacement::Span(n),
}
}

// ─── Layout extraction ───────────────────────────────────────────────

fn extract_layout<'a>(
Expand Down
Loading
Loading