canvas: pop many clips on restore (#38496)

When restoring context/state we need to pop all clips from current
state, before we just poped one (even if there was none).

Testing: Added new WPT tests

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
sagudev 2025-08-07 10:23:09 +02:00 committed by GitHub
parent c055e8b456
commit c0cc8484f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 161 additions and 11 deletions

View file

@ -749,8 +749,10 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
} }
} }
pub(crate) fn pop_clip(&mut self) { pub(crate) fn pop_clips(&mut self, clips: usize) {
self.drawtarget.pop_clip(); for _ in 0..clips {
self.drawtarget.pop_clip();
}
} }
} }

View file

@ -282,7 +282,7 @@ impl CanvasPaintThread {
self.canvas(canvas_id).update_image_rendering(); self.canvas(canvas_id).update_image_rendering();
sender.send(()).unwrap(); sender.send(()).unwrap();
}, },
Canvas2dMsg::PopClip => self.canvas(canvas_id).pop_clip(), Canvas2dMsg::PopClips(clips) => self.canvas(canvas_id).pop_clips(clips),
} }
} }
@ -348,14 +348,14 @@ impl Canvas {
} }
} }
fn pop_clip(&mut self) { fn pop_clips(&mut self, clips: usize) {
match self { match self {
#[cfg(feature = "raqote")] #[cfg(feature = "raqote")]
Canvas::Raqote(canvas_data) => canvas_data.pop_clip(), Canvas::Raqote(canvas_data) => canvas_data.pop_clips(clips),
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.pop_clip(), Canvas::Vello(canvas_data) => canvas_data.pop_clips(clips),
#[cfg(feature = "vello_cpu")] #[cfg(feature = "vello_cpu")]
Canvas::VelloCPU(canvas_data) => canvas_data.pop_clip(), Canvas::VelloCPU(canvas_data) => canvas_data.pop_clips(clips),
_ => unreachable!(), _ => unreachable!(),
} }
} }

View file

@ -119,6 +119,9 @@ pub(crate) struct CanvasContextState {
text_baseline: TextBaseline, text_baseline: TextBaseline,
#[no_trace] #[no_trace]
direction: Direction, direction: Direction,
/// The number of clips pushed onto the context while in this state.
/// When restoring old state, same number of clips will be popped to restore state.
clips_pushed: usize,
} }
impl CanvasContextState { impl CanvasContextState {
@ -146,6 +149,7 @@ impl CanvasContextState {
direction: Default::default(), direction: Default::default(),
line_dash: Vec::new(), line_dash: Vec::new(),
line_dash_offset: 0.0, line_dash_offset: 0.0,
clips_pushed: 0,
} }
} }
@ -1320,12 +1324,15 @@ impl CanvasState {
} }
#[cfg_attr(crown, allow(crown::unrooted_must_root))] #[cfg_attr(crown, allow(crown::unrooted_must_root))]
// https://html.spec.whatwg.org/multipage/#dom-context-2d-restore /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-restore>
pub(crate) fn restore(&self) { pub(crate) fn restore(&self) {
let mut saved_states = self.saved_states.borrow_mut(); let mut saved_states = self.saved_states.borrow_mut();
if let Some(state) = saved_states.pop() { if let Some(state) = saved_states.pop() {
let clips_to_pop = self.state.borrow().clips_pushed;
if clips_to_pop != 0 {
self.send_canvas_2d_msg(Canvas2dMsg::PopClips(clips_to_pop));
}
self.state.borrow_mut().clone_from(&state); self.state.borrow_mut().clone_from(&state);
self.send_canvas_2d_msg(Canvas2dMsg::PopClip);
} }
} }
@ -1940,6 +1947,7 @@ impl CanvasState {
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip // https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
pub(crate) fn clip_(&self, path: Path, fill_rule: CanvasFillRule) { pub(crate) fn clip_(&self, path: Path, fill_rule: CanvasFillRule) {
self.state.borrow_mut().clips_pushed += 1;
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath( self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(
path, path,
fill_rule.convert(), fill_rule.convert(),

View file

@ -483,7 +483,7 @@ pub enum Canvas2dMsg {
), ),
ClearRect(Rect<f32>, Transform2D<f32>), ClearRect(Rect<f32>, Transform2D<f32>),
ClipPath(Path, FillRule, Transform2D<f32>), ClipPath(Path, FillRule, Transform2D<f32>),
PopClip, PopClips(usize),
FillPath( FillPath(
FillOrStrokeStyle, FillOrStrokeStyle,
Path, Path,

View file

@ -480322,7 +480322,7 @@
[] []
], ],
"the-canvas-state.yaml": [ "the-canvas-state.yaml": [
"230e45f80a5901cae395ed1c4c848556e333073d", "c3f382ef92929aa86f26f5462a7bc6d0e8731854",
[] []
], ],
"the-canvas.yaml": [ "the-canvas.yaml": [
@ -711259,6 +711259,13 @@
{} {}
] ]
], ],
"2d.state.saverestore.clip.2.html": [
"1f1372c2c34847a224434118d864b2431b054e01",
[
null,
{}
]
],
"2d.state.saverestore.clip.html": [ "2d.state.saverestore.clip.html": [
"b95a9a38c02740a4025850771c3bb8de5bb68403", "b95a9a38c02740a4025850771c3bb8de5bb68403",
[ [
@ -725464,6 +725471,20 @@
{} {}
] ]
], ],
"2d.state.saverestore.clip.2.html": [
"31805b52bc1ca2a0a3303762bf4d46e068a45e4d",
[
null,
{}
]
],
"2d.state.saverestore.clip.2.worker.js": [
"f3d49f9511a1c099795aa8fe93955881b761b2a9",
[
"html/canvas/offscreen/the-canvas-state/2d.state.saverestore.clip.2.worker.html",
{}
]
],
"2d.state.saverestore.clip.html": [ "2d.state.saverestore.clip.html": [
"08ddd216a8f90c9b4bf81ef04c8cf8829f6ef5e6", "08ddd216a8f90c9b4bf81ef04c8cf8829f6ef5e6",
[ [

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>Canvas test: 2d.state.saverestore.clip.2</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">
<h1>2d.state.saverestore.clip.2</h1>
<p class="desc">save()/restore() affects the clipping path</p>
<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
<p class="output expectedtext">Expected output:<p><img src="/images/green-100x50.png" class="output expected" id="expected" alt="">
<ul id="d"></ul>
<script>
var t = async_test("save()/restore() affects the clipping path");
_addTest(function(canvas, ctx) {
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 100, 50);
ctx.save();
ctx.rect(0, 0, 1, 1);
ctx.clip();
ctx.clip();
ctx.restore();
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
_assertPixel(canvas, 50,25, 0,255,0,255);
});
</script>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<meta charset="UTF-8">
<title>OffscreenCanvas test: 2d.state.saverestore.clip.2</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<h1>2d.state.saverestore.clip.2</h1>
<p class="desc">save()/restore() affects the clipping path</p>
<script>
var t = async_test("save()/restore() affects the clipping path");
var t_pass = t.done.bind(t);
var t_fail = t.step_func(function(reason) {
throw reason;
});
t.step(function() {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 100, 50);
ctx.save();
ctx.rect(0, 0, 1, 1);
ctx.clip();
ctx.clip();
ctx.restore();
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
_assertPixel(canvas, 50,25, 0,255,0,255);
t.done();
});
</script>

View file

@ -0,0 +1,31 @@
// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
// OffscreenCanvas test in a worker:2d.state.saverestore.clip.2
// Description:save()/restore() affects the clipping path
// Note:
importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");
var t = async_test("save()/restore() affects the clipping path");
var t_pass = t.done.bind(t);
var t_fail = t.step_func(function(reason) {
throw reason;
});
t.step(function() {
var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 100, 50);
ctx.save();
ctx.rect(0, 0, 1, 1);
ctx.clip();
ctx.clip();
ctx.restore();
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
_assertPixel(canvas, 50,25, 0,255,0,255);
t.done();
});
done();

View file

@ -25,6 +25,21 @@
@assert pixel 50,25 == 0,255,0,255; @assert pixel 50,25 == 0,255,0,255;
expected: green expected: green
- name: 2d.state.saverestore.clip.2
desc: save()/restore() affects the clipping path
code: |
ctx.fillStyle = '#f00';
ctx.fillRect(0, 0, 100, 50);
ctx.save();
ctx.rect(0, 0, 1, 1);
ctx.clip();
ctx.clip();
ctx.restore();
ctx.fillStyle = '#0f0';
ctx.fillRect(0, 0, 100, 50);
@assert pixel 50,25 == 0,255,0,255;
expected: green
- name: 2d.state.saverestore.path - name: 2d.state.saverestore.path
desc: save()/restore() does not affect the current path desc: save()/restore() does not affect the current path
code: | code: |