LibWeb: Don't require layout node when setting canvas 2D filter

Setting the filter property on a CanvasRenderingContext2D would crash
with a null pointer dereference if the canvas element had no layout
node (e.g. a detached canvas not in the document).

Instead of forcing a full layout update and requiring a layout node,
we now only update style if needed and resolve lengths via the
element's computed properties when available, falling back to
document-level defaults otherwise. This matches the pattern used by
CanvasTextDrawingStyles.
This commit is contained in:
Andreas Kling 2026-02-22 11:02:43 +01:00 committed by Jelle Raaijmakers
parent ef6368924e
commit cbf8b70d42
4 changed files with 32 additions and 11 deletions

View file

@ -66,7 +66,6 @@ enum class InvalidateLayoutTreeReason {
#define ENUMERATE_UPDATE_LAYOUT_REASONS(X) \
X(AutoScrollSelection) \
X(CanvasRenderingContext2DSetFilter) \
X(CanvasRenderingContext2DSetFillStyle) \
X(CanvasRenderingContext2DSetShadowColor) \
X(CanvasRenderingContext2DSetStrokeStyle) \

View file

@ -1218,14 +1218,17 @@ void CanvasRenderingContext2D::set_filter(String filter)
auto style_value = parser.parse_as_css_value(CSS::PropertyID::Filter);
if (style_value && style_value->is_filter_value_list()) {
// Note: The layout must be updated to make sure the canvas's layout node isn't null.
canvas_element().document().update_layout(DOM::UpdateLayoutReason::CanvasRenderingContext2DSetFilter);
auto layout_node = canvas_element().layout_node();
auto& document = canvas_element().document();
DOM::AbstractElement abstract_element { canvas_element() };
document.update_style_if_needed_for_element(abstract_element);
auto length_resolution_context = canvas_element().computed_properties()
? CSS::Length::ResolutionContext::for_element(abstract_element)
: CSS::Length::ResolutionContext::for_document(document);
CSS::ComputationContext computation_context {
.length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(*layout_node),
.abstract_element = DOM::AbstractElement { canvas_element() },
.color_scheme = layout_node->computed_values().color_scheme(),
.length_resolution_context = length_resolution_context,
.abstract_element = abstract_element,
};
auto filter_value_list = style_value->absolutized(computation_context)->as_filter_value_list().filter_value_list();
@ -1266,10 +1269,11 @@ void CanvasRenderingContext2D::set_filter(String filter)
radius = static_cast<float>(CSS::Length::from_style_value(*drop_shadow.radius, {}).absolute_length_to_px());
};
auto color_context = CSS::ColorResolutionContext::for_layout_node_with_style(*layout_node);
auto color = drop_shadow.color
? drop_shadow.color->to_color(color_context).value_or(Gfx::Color::Black)
: Gfx::Color::Black;
Gfx::Color color = Gfx::Color::Black;
if (drop_shadow.color && canvas_element().computed_properties()) {
auto color_context = CSS::ColorResolutionContext::for_element(abstract_element);
color = drop_shadow.color->to_color(color_context).value_or(Gfx::Color::Black);
}
auto new_filter = Gfx::Filter::drop_shadow(offset_x, offset_y, radius, color);

View file

@ -0,0 +1,3 @@
blur(5px)
sepia(0.5)
none

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
// Setting filter on a detached canvas (no layout node) should not crash.
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.filter = "blur(5px)";
println(ctx.filter);
ctx.filter = "sepia(0.5)";
println(ctx.filter);
ctx.filter = "none";
println(ctx.filter);
});
</script>