mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
Implement drawing shadows in canvas.
This commit is contained in:
parent
24af4c4ec6
commit
465cea8db5
30 changed files with 134 additions and 204 deletions
|
@ -54,40 +54,6 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
image_data
|
image_data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// It writes image data to the canvas
|
|
||||||
/// source_rect: the area of the image data to be written
|
|
||||||
/// dest_rect: The area of the canvas where the imagedata will be copied
|
|
||||||
/// smoothing_enabled: if smoothing is applied to the copied pixels
|
|
||||||
fn write_pixels(&self, imagedata: &[u8],
|
|
||||||
image_size: Size2D<f64>,
|
|
||||||
source_rect: Rect<f64>,
|
|
||||||
dest_rect: Rect<f64>,
|
|
||||||
smoothing_enabled: bool) {
|
|
||||||
// From spec https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
|
|
||||||
// When scaling up, if the imageSmoothingEnabled attribute is set to true, the user agent should attempt
|
|
||||||
// to apply a smoothing algorithm to the image data when it is scaled.
|
|
||||||
// Otherwise, the image must be rendered using nearest-neighbor interpolation.
|
|
||||||
let filter = if smoothing_enabled {
|
|
||||||
Filter::Linear
|
|
||||||
} else {
|
|
||||||
Filter::Point
|
|
||||||
};
|
|
||||||
// azure_hl operates with integers. We need to cast the image size
|
|
||||||
let image_size = image_size.to_i32();
|
|
||||||
|
|
||||||
let source_surface = self.drawtarget.create_source_surface_from_data(
|
|
||||||
&imagedata,
|
|
||||||
image_size, image_size.width * 4, SurfaceFormat::B8G8R8A8);
|
|
||||||
|
|
||||||
let draw_surface_options = DrawSurfaceOptions::new(filter, true);
|
|
||||||
let draw_options = DrawOptions::new(self.state.draw_options.alpha, CompositionOp::Over, AntialiasMode::None);
|
|
||||||
|
|
||||||
self.drawtarget.draw_surface(source_surface,
|
|
||||||
dest_rect.to_azfloat(),
|
|
||||||
source_rect.to_azfloat(),
|
|
||||||
draw_surface_options, draw_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// dirty_rect: original dirty_rect provided by the putImageData call
|
/// dirty_rect: original dirty_rect provided by the putImageData call
|
||||||
/// image_data_rect: the area of the image to be copied
|
/// image_data_rect: the area of the image to be copied
|
||||||
/// Result: It retuns the modified dirty_rect by the rules described in
|
/// Result: It retuns the modified dirty_rect by the rules described in
|
||||||
|
@ -136,23 +102,6 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
|
|
||||||
dirty_rect
|
dirty_rect
|
||||||
}
|
}
|
||||||
|
|
||||||
/// It writes an image to the destination canvas
|
|
||||||
/// imagedata: Pixel information of the image to be written. It takes RGBA8
|
|
||||||
/// image_size: The size of the image to be written
|
|
||||||
/// dest_rect: Area of the destination canvas where the pixels will be copied
|
|
||||||
/// smoothing_enabled: It determines if smoothing is applied to the image result
|
|
||||||
fn write_image(&self, mut imagedata: Vec<u8>,
|
|
||||||
image_size: Size2D<f64>, dest_rect: Rect<f64>, smoothing_enabled: bool) {
|
|
||||||
if imagedata.len() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let image_rect = Rect::new(Point2D::new(0f64, 0f64), image_size);
|
|
||||||
// rgba -> bgra
|
|
||||||
byte_swap(&mut imagedata);
|
|
||||||
self.write_pixels(&imagedata, image_size, image_rect, dest_rect, smoothing_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CanvasPaintTask<'a> {
|
pub struct CanvasPaintTask<'a> {
|
||||||
|
@ -316,9 +265,16 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.need_to_draw_shadow() {
|
||||||
|
self.draw_with_shadow(&draw_rect, |new_draw_target: &DrawTarget| {
|
||||||
|
new_draw_target.fill_rect(&draw_rect, self.state.fill_style.to_pattern_ref(),
|
||||||
|
Some(&self.state.draw_options));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
self.drawtarget.fill_rect(&draw_rect, self.state.fill_style.to_pattern_ref(),
|
self.drawtarget.fill_rect(&draw_rect, self.state.fill_style.to_pattern_ref(),
|
||||||
Some(&self.state.draw_options));
|
Some(&self.state.draw_options));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_rect(&self, rect: &Rect<f32>) {
|
fn clear_rect(&self, rect: &Rect<f32>) {
|
||||||
self.drawtarget.clear_rect(rect);
|
self.drawtarget.clear_rect(rect);
|
||||||
|
@ -376,7 +332,19 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
let source_rect = source_rect.ceil();
|
let source_rect = source_rect.ceil();
|
||||||
// It discards the extra pixels (if any) that won't be painted
|
// It discards the extra pixels (if any) that won't be painted
|
||||||
let image_data = crop_image(image_data, image_size, source_rect);
|
let image_data = crop_image(image_data, image_size, source_rect);
|
||||||
self.write_image(image_data, source_rect.size, dest_rect, smoothing_enabled);
|
|
||||||
|
if self.need_to_draw_shadow() {
|
||||||
|
let rect = Rect::new(Point2D::new(dest_rect.origin.x as f32, dest_rect.origin.y as f32),
|
||||||
|
Size2D::new(dest_rect.size.width as f32, dest_rect.size.height as f32));
|
||||||
|
|
||||||
|
self.draw_with_shadow(&rect, |new_draw_target: &DrawTarget| {
|
||||||
|
write_image(&new_draw_target, image_data, source_rect.size, dest_rect,
|
||||||
|
smoothing_enabled, self.state.draw_options.alpha);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
write_image(&self.drawtarget, image_data, source_rect.size, dest_rect,
|
||||||
|
smoothing_enabled, self.state.draw_options.alpha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_image_self(&self, image_size: Size2D<f64>,
|
fn draw_image_self(&self, image_size: Size2D<f64>,
|
||||||
|
@ -384,9 +352,21 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
smoothing_enabled: bool) {
|
smoothing_enabled: bool) {
|
||||||
// Reads pixels from source image
|
// Reads pixels from source image
|
||||||
// In this case source and target are the same canvas
|
// In this case source and target are the same canvas
|
||||||
let imagedata = self.read_pixels(source_rect, image_size);
|
let image_data = self.read_pixels(source_rect, image_size);
|
||||||
|
|
||||||
|
if self.need_to_draw_shadow() {
|
||||||
|
let rect = Rect::new(Point2D::new(dest_rect.origin.x as f32, dest_rect.origin.y as f32),
|
||||||
|
Size2D::new(dest_rect.size.width as f32, dest_rect.size.height as f32));
|
||||||
|
|
||||||
|
self.draw_with_shadow(&rect, |new_draw_target: &DrawTarget| {
|
||||||
|
write_image(&new_draw_target, image_data, source_rect.size, dest_rect,
|
||||||
|
smoothing_enabled, self.state.draw_options.alpha);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
// Writes on target canvas
|
// Writes on target canvas
|
||||||
self.write_image(imagedata, image_size, dest_rect, smoothing_enabled);
|
write_image(&self.drawtarget, image_data, image_size, dest_rect,
|
||||||
|
smoothing_enabled, self.state.draw_options.alpha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_to(&self, point: &Point2D<AzFloat>) {
|
fn move_to(&self, point: &Point2D<AzFloat>) {
|
||||||
|
@ -577,7 +557,7 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
// rgba -> bgra
|
// rgba -> bgra
|
||||||
byte_swap(&mut imagedata);
|
byte_swap(&mut imagedata);
|
||||||
|
|
||||||
let image_rect = Rect::new(Point2D::new(0f64, 0f64),
|
let image_rect = Rect::new(Point2D::zero(),
|
||||||
Size2D::new(image_data_rect.size.width, image_data_rect.size.height));
|
Size2D::new(image_data_rect.size.width, image_data_rect.size.height));
|
||||||
|
|
||||||
// Dirty rectangle defines the area of the source image to be copied
|
// Dirty rectangle defines the area of the source image to be copied
|
||||||
|
@ -607,7 +587,8 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
image_data_rect.origin.y + source_rect.origin.y),
|
image_data_rect.origin.y + source_rect.origin.y),
|
||||||
Size2D::new(source_rect.size.width, source_rect.size.height));
|
Size2D::new(source_rect.size.width, source_rect.size.height));
|
||||||
|
|
||||||
self.write_pixels(&imagedata, image_data_rect.size, source_rect, dest_rect, true)
|
write_pixels(&self.drawtarget, &imagedata, image_data_rect.size, source_rect,
|
||||||
|
dest_rect, true, self.state.draw_options.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_shadow_offset_x(&mut self, value: f64) {
|
fn set_shadow_offset_x(&mut self, value: f64) {
|
||||||
|
@ -625,6 +606,41 @@ impl<'a> CanvasPaintTask<'a> {
|
||||||
fn set_shadow_color(&mut self, value: AzColor) {
|
fn set_shadow_color(&mut self, value: AzColor) {
|
||||||
self.state.shadow_color = value;
|
self.state.shadow_color = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/#when-shadows-are-drawn
|
||||||
|
fn need_to_draw_shadow(&self) -> bool {
|
||||||
|
self.state.shadow_color.a != 0.0f32 &&
|
||||||
|
(self.state.shadow_offset_x != 0.0f64 ||
|
||||||
|
self.state.shadow_offset_y != 0.0f64 ||
|
||||||
|
self.state.shadow_blur != 0.0f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_draw_target_for_shadow(&self, source_rect: &Rect<f32>) -> DrawTarget {
|
||||||
|
let draw_target = self.drawtarget.create_similar_draw_target(&Size2D::new(source_rect.size.width as i32,
|
||||||
|
source_rect.size.height as i32),
|
||||||
|
self.drawtarget.get_format());
|
||||||
|
let matrix = Matrix2D::identity().translate(-source_rect.origin.x as AzFloat,
|
||||||
|
-source_rect.origin.y as AzFloat)
|
||||||
|
.mul(&self.state.transform);
|
||||||
|
draw_target.set_transform(&matrix);
|
||||||
|
draw_target
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_with_shadow<F>(&self, rect: &Rect<f32>, draw_shadow_source: F)
|
||||||
|
where F: FnOnce(&DrawTarget)
|
||||||
|
{
|
||||||
|
let shadow_src_rect = self.state.transform.transform_rect(rect);
|
||||||
|
let new_draw_target = self.create_draw_target_for_shadow(&shadow_src_rect);
|
||||||
|
draw_shadow_source(&new_draw_target);
|
||||||
|
self.drawtarget.draw_surface_with_shadow(new_draw_target.snapshot(),
|
||||||
|
&Point2D::new(shadow_src_rect.origin.x as AzFloat,
|
||||||
|
shadow_src_rect.origin.y as AzFloat),
|
||||||
|
&self.state.shadow_color,
|
||||||
|
&Point2D::new(self.state.shadow_offset_x as AzFloat,
|
||||||
|
self.state.shadow_offset_y as AzFloat),
|
||||||
|
(self.state.shadow_blur / 2.0f64) as AzFloat,
|
||||||
|
self.state.draw_options.composition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used by drawImage to get rid of the extra pixels of the image data that
|
/// Used by drawImage to get rid of the extra pixels of the image data that
|
||||||
|
@ -658,6 +674,65 @@ fn crop_image(image_data: Vec<u8>,
|
||||||
new_image_data
|
new_image_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// It writes an image to the destination target
|
||||||
|
/// draw_target: the destination target where the image_data will be copied
|
||||||
|
/// image_data: Pixel information of the image to be written. It takes RGBA8
|
||||||
|
/// image_size: The size of the image to be written
|
||||||
|
/// dest_rect: Area of the destination target where the pixels will be copied
|
||||||
|
/// smoothing_enabled: It determines if smoothing is applied to the image result
|
||||||
|
fn write_image(draw_target: &DrawTarget,
|
||||||
|
mut image_data: Vec<u8>,
|
||||||
|
image_size: Size2D<f64>,
|
||||||
|
dest_rect: Rect<f64>,
|
||||||
|
smoothing_enabled: bool,
|
||||||
|
global_alpha: f32) {
|
||||||
|
if image_data.len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let image_rect = Rect::new(Point2D::zero(), image_size);
|
||||||
|
// rgba -> bgra
|
||||||
|
byte_swap(&mut image_data);
|
||||||
|
write_pixels(&draw_target, &image_data, image_size, image_rect, dest_rect, smoothing_enabled, global_alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// It writes image data to the target
|
||||||
|
/// draw_target: the destination target where the imagedata will be copied
|
||||||
|
/// source_rect: the area of the image data to be written
|
||||||
|
/// dest_rect: The area of the target where the imagedata will be copied
|
||||||
|
/// smoothing_enabled: if smoothing is applied to the copied pixels
|
||||||
|
fn write_pixels(draw_target: &DrawTarget,
|
||||||
|
image_data: &[u8],
|
||||||
|
image_size: Size2D<f64>,
|
||||||
|
source_rect: Rect<f64>,
|
||||||
|
dest_rect: Rect<f64>,
|
||||||
|
smoothing_enabled: bool,
|
||||||
|
global_alpha: f32) {
|
||||||
|
// From spec https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
|
||||||
|
// When scaling up, if the imageSmoothingEnabled attribute is set to true, the user agent should attempt
|
||||||
|
// to apply a smoothing algorithm to the image data when it is scaled.
|
||||||
|
// Otherwise, the image must be rendered using nearest-neighbor interpolation.
|
||||||
|
let filter = if smoothing_enabled {
|
||||||
|
Filter::Linear
|
||||||
|
} else {
|
||||||
|
Filter::Point
|
||||||
|
};
|
||||||
|
// azure_hl operates with integers. We need to cast the image size
|
||||||
|
let image_size = image_size.to_i32();
|
||||||
|
|
||||||
|
let source_surface = draw_target.create_source_surface_from_data(
|
||||||
|
&image_data,
|
||||||
|
image_size, image_size.width * 4, SurfaceFormat::B8G8R8A8);
|
||||||
|
|
||||||
|
let draw_surface_options = DrawSurfaceOptions::new(filter, true);
|
||||||
|
let draw_options = DrawOptions::new(global_alpha, CompositionOp::Over, AntialiasMode::None);
|
||||||
|
|
||||||
|
draw_target.draw_surface(source_surface,
|
||||||
|
dest_rect.to_azfloat(),
|
||||||
|
source_rect.to_azfloat(),
|
||||||
|
draw_surface_options,
|
||||||
|
draw_options);
|
||||||
|
}
|
||||||
|
|
||||||
pub trait SizeToi32 {
|
pub trait SizeToi32 {
|
||||||
fn to_i32(&self) -> Size2D<i32>;
|
fn to_i32(&self) -> Size2D<i32>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.fillRect.shadow.html]
|
|
||||||
type: testharness
|
|
||||||
[fillRect draws shadows]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.alpha.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadow colour alpha components are used]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.alpha.3.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are affected by globalAlpha]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.alpha.4.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows with alpha components are correctly affected by globalAlpha]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.alpha.5.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows of shapes with alpha components are drawn correctly]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.canvas.alpha.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn correctly for partially-transparent canvases]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.canvas.basic.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn for canvases]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.canvas.transparent.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are not drawn for transparent parts of canvases]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.clip.1.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows of clipped shapes are still drawn within the clipping region]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.clip.3.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows of clipped shapes are still drawn within the clipping region]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.composite.1.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn using globalCompositeOperation]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.composite.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn using globalCompositeOperation]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.gradient.alpha.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn correctly for partially-transparent gradient fills]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.gradient.basic.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn for gradient fills]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.gradient.transparent.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are not drawn for transparent parts of gradient fills]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.image.alpha.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn correctly for partially-transparent images]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.image.basic.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn for images]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.image.scale.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn correctly for scaled images]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.image.transparent.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are not drawn for transparent parts of images]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.offset.negativeX.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows can be offset with negative x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.offset.negativeY.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows can be offset with negative y]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.offset.positiveX.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows can be offset with positive x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.offset.positiveY.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows can be offset with positive y]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.outside.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows of shapes outside the visible area can be offset onto the visible area]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.pattern.alpha.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn correctly for partially-transparent fill patterns]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.pattern.basic.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are drawn for fill patterns]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.pattern.transparent.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows are not drawn for transparent parts of fill patterns]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.transform.1.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadows take account of transformations]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[2d.shadow.transform.2.html]
|
|
||||||
type: testharness
|
|
||||||
[Shadow offsets are not affected by transformations]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue