2021-09-17 23:12:16 +02:00
/*
2023-04-18 19:32:00 +02:00
* Copyright ( c ) 2021 - 2023 , Andreas Kling < kling @ serenityos . org >
2022-02-16 16:32:18 +00:00
* Copyright ( c ) 2022 , Sam Atkins < atkinssj @ serenityos . org >
2022-02-27 21:00:04 +01:00
* Copyright ( c ) 2022 , Tobias Christiansen < tobyase @ serenityos . org >
2023-04-10 12:28:55 +01:00
* Copyright ( c ) 2023 , MacDue < macdue @ dueutil . tech >
2021-09-17 23:12:16 +02:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2023-05-31 11:48:29 +02:00
# include <AK/Debug.h>
2022-11-10 13:54:57 +01:00
# include <LibWeb/Layout/BlockFormattingContext.h>
2021-09-17 23:12:16 +02:00
# include <LibWeb/Layout/SVGFormattingContext.h>
2022-02-11 12:37:22 +00:00
# include <LibWeb/Layout/SVGGeometryBox.h>
2023-04-18 19:32:00 +02:00
# include <LibWeb/Layout/SVGSVGBox.h>
2023-06-08 21:32:33 +03:00
# include <LibWeb/Layout/SVGTextBox.h>
2022-11-10 13:54:57 +01:00
# include <LibWeb/SVG/SVGForeignObjectElement.h>
2022-02-27 21:00:04 +01:00
# include <LibWeb/SVG/SVGSVGElement.h>
2021-09-17 23:12:16 +02:00
namespace Web : : Layout {
2022-07-16 23:30:32 +02:00
SVGFormattingContext : : SVGFormattingContext ( LayoutState & state , Box const & box , FormattingContext * parent )
2022-02-19 20:13:47 +01:00
: FormattingContext ( Type : : SVG , state , box , parent )
2021-09-17 23:12:16 +02:00
{
}
2022-03-14 13:21:51 -06:00
SVGFormattingContext : : ~ SVGFormattingContext ( ) = default ;
2021-09-17 23:12:16 +02:00
2023-03-19 09:57:31 +01:00
CSSPixels SVGFormattingContext : : automatic_content_width ( ) const
{
return 0 ;
}
2022-11-23 17:46:10 +00:00
CSSPixels SVGFormattingContext : : automatic_content_height ( ) const
2022-09-24 13:39:43 +02:00
{
return 0 ;
}
2023-04-17 01:20:24 +01:00
struct ViewBoxTransform {
CSSPixelPoint offset ;
2023-05-24 10:50:57 +02:00
double scale_factor ;
2023-04-17 01:20:24 +01:00
} ;
// https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute
static ViewBoxTransform scale_and_align_viewbox_content ( SVG : : PreserveAspectRatio const & preserve_aspect_ratio ,
SVG : : ViewBox const & view_box , Gfx : : FloatSize viewbox_scale , auto const & svg_box_state )
{
ViewBoxTransform viewbox_transform { } ;
switch ( preserve_aspect_ratio . meet_or_slice ) {
case SVG : : PreserveAspectRatio : : MeetOrSlice : : Meet :
// meet (the default) - Scale the graphic such that:
// - aspect ratio is preserved
// - the entire ‘ viewBox’ is visible within the SVG viewport
// - the ‘ viewBox’ is scaled up as much as possible, while still meeting the other criteria
viewbox_transform . scale_factor = min ( viewbox_scale . width ( ) , viewbox_scale . height ( ) ) ;
break ;
case SVG : : PreserveAspectRatio : : MeetOrSlice : : Slice :
// slice - Scale the graphic such that:
// aspect ratio is preserved
// the entire SVG viewport is covered by the ‘ viewBox’
// the ‘ viewBox’ is scaled down as much as possible, while still meeting the other criteria
viewbox_transform . scale_factor = max ( viewbox_scale . width ( ) , viewbox_scale . height ( ) ) ;
break ;
default :
VERIFY_NOT_REACHED ( ) ;
}
// Handle X alignment:
if ( svg_box_state . has_definite_width ( ) ) {
switch ( preserve_aspect_ratio . align ) {
case SVG : : PreserveAspectRatio : : Align : : xMinYMin :
case SVG : : PreserveAspectRatio : : Align : : xMinYMid :
case SVG : : PreserveAspectRatio : : Align : : xMinYMax :
// Align the <min-x> of the element's ‘ viewBox’ with the smallest X value of the SVG viewport.
viewbox_transform . offset . translate_by ( 0 , 0 ) ;
break ;
case SVG : : PreserveAspectRatio : : Align : : None : {
// Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
// if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
// FIXME: None is unimplemented (treat as xMidYMid)
[[fallthrough]] ;
}
case SVG : : PreserveAspectRatio : : Align : : xMidYMin :
case SVG : : PreserveAspectRatio : : Align : : xMidYMid :
case SVG : : PreserveAspectRatio : : Align : : xMidYMax :
// Align the midpoint X value of the element's ‘ viewBox’ with the midpoint X value of the SVG viewport.
viewbox_transform . offset . translate_by ( ( svg_box_state . content_width ( ) - ( view_box . width * viewbox_transform . scale_factor ) ) / 2 , 0 ) ;
break ;
case SVG : : PreserveAspectRatio : : Align : : xMaxYMin :
case SVG : : PreserveAspectRatio : : Align : : xMaxYMid :
case SVG : : PreserveAspectRatio : : Align : : xMaxYMax :
// Align the <min-x>+<width> of the element's ‘ viewBox’ with the maximum X value of the SVG viewport.
viewbox_transform . offset . translate_by ( ( svg_box_state . content_width ( ) - ( view_box . width * viewbox_transform . scale_factor ) ) , 0 ) ;
break ;
default :
VERIFY_NOT_REACHED ( ) ;
}
}
if ( svg_box_state . has_definite_width ( ) ) {
switch ( preserve_aspect_ratio . align ) {
case SVG : : PreserveAspectRatio : : Align : : xMinYMin :
case SVG : : PreserveAspectRatio : : Align : : xMidYMin :
case SVG : : PreserveAspectRatio : : Align : : xMaxYMin :
// Align the <min-y> of the element's ‘ viewBox’ with the smallest Y value of the SVG viewport.
viewbox_transform . offset . translate_by ( 0 , 0 ) ;
break ;
case SVG : : PreserveAspectRatio : : Align : : None : {
// Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
// if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
// FIXME: None is unimplemented (treat as xMidYMid)
[[fallthrough]] ;
}
case SVG : : PreserveAspectRatio : : Align : : xMinYMid :
case SVG : : PreserveAspectRatio : : Align : : xMidYMid :
case SVG : : PreserveAspectRatio : : Align : : xMaxYMid :
// Align the midpoint Y value of the element's ‘ viewBox’ with the midpoint Y value of the SVG viewport.
viewbox_transform . offset . translate_by ( 0 , ( svg_box_state . content_height ( ) - ( view_box . height * viewbox_transform . scale_factor ) ) / 2 ) ;
break ;
case SVG : : PreserveAspectRatio : : Align : : xMinYMax :
case SVG : : PreserveAspectRatio : : Align : : xMidYMax :
case SVG : : PreserveAspectRatio : : Align : : xMaxYMax :
// Align the <min-y>+<height> of the element's ‘ viewBox’ with the maximum Y value of the SVG viewport.
viewbox_transform . offset . translate_by ( 0 , ( svg_box_state . content_height ( ) - ( view_box . height * viewbox_transform . scale_factor ) ) ) ;
break ;
default :
VERIFY_NOT_REACHED ( ) ;
}
}
return viewbox_transform ;
}
2023-04-18 19:32:00 +02:00
void SVGFormattingContext : : run ( Box const & box , LayoutMode layout_mode , AvailableSpace const & available_space )
2021-09-17 23:12:16 +02:00
{
2022-07-11 00:27:54 +02:00
auto & svg_svg_element = verify_cast < SVG : : SVGSVGElement > ( * box . dom_node ( ) ) ;
2023-04-10 12:28:55 +01:00
auto svg_box_state = m_state . get ( box ) ;
auto root_offset = svg_box_state . offset ;
2022-11-10 13:54:57 +01:00
box . for_each_child_of_type < BlockContainer > ( [ & ] ( BlockContainer const & child_box ) {
if ( is < SVG : : SVGForeignObjectElement > ( child_box . dom_node ( ) ) ) {
Layout : : BlockFormattingContext bfc ( m_state , child_box , this ) ;
bfc . run ( child_box , LayoutMode : : Normal , available_space ) ;
auto & child_state = m_state . get_mutable ( child_box ) ;
child_state . set_content_offset ( child_state . offset . translated ( root_offset ) ) ;
}
return IterationDecision : : Continue ;
} ) ;
2023-04-18 19:32:00 +02:00
box . for_each_in_subtree_of_type < Box > ( [ & ] ( Box const & descendant ) {
2022-02-11 12:37:22 +00:00
if ( is < SVGGeometryBox > ( descendant ) ) {
2022-02-20 15:51:24 +01:00
auto const & geometry_box = static_cast < SVGGeometryBox const & > ( descendant ) ;
2022-02-27 21:00:04 +01:00
auto & geometry_box_state = m_state . get_mutable ( geometry_box ) ;
auto & dom_node = const_cast < SVGGeometryBox & > ( geometry_box ) . dom_node ( ) ;
auto & path = dom_node . get_path ( ) ;
2023-04-17 01:20:24 +01:00
auto path_transform = dom_node . get_transform ( ) ;
2022-02-16 15:17:50 +00:00
2023-05-24 10:50:57 +02:00
double viewbox_scale = 1 ;
2023-07-31 18:37:44 +02:00
auto maybe_view_box = dom_node . view_box ( ) ;
2022-02-27 21:00:04 +01:00
if ( maybe_view_box . has_value ( ) ) {
2023-04-10 12:28:55 +01:00
// FIXME: This should allow just one of width or height to be specified.
// E.g. We should be able to layout <svg width="100%"> where height is unspecified/auto.
if ( ! svg_box_state . has_definite_width ( ) | | ! svg_box_state . has_definite_height ( ) ) {
2023-05-31 11:48:29 +02:00
dbgln_if ( LIBWEB_CSS_DEBUG , " FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work! " ) ;
2023-04-10 12:28:55 +01:00
}
2023-04-17 01:20:24 +01:00
auto view_box = maybe_view_box . value ( ) ;
2023-06-12 21:37:35 +03:00
auto scale_width = svg_box_state . has_definite_width ( ) ? svg_box_state . content_width ( ) / view_box . width : 1 ;
auto scale_height = svg_box_state . has_definite_height ( ) ? svg_box_state . content_height ( ) / view_box . height : 1 ;
2023-04-10 12:28:55 +01:00
2023-04-17 01:20:24 +01:00
// The initial value for preserveAspectRatio is xMidYMid meet.
auto preserve_aspect_ratio = svg_svg_element . preserve_aspect_ratio ( ) . value_or ( SVG : : PreserveAspectRatio { } ) ;
2023-07-23 01:09:39 +02:00
auto viewbox_transform = scale_and_align_viewbox_content ( preserve_aspect_ratio , view_box , { scale_width , scale_height } , svg_box_state ) ;
2023-08-07 22:12:21 +01:00
path_transform = Gfx : : AffineTransform { } . translate ( viewbox_transform . offset . to_type < float > ( ) ) . scale ( viewbox_transform . scale_factor , viewbox_transform . scale_factor ) . translate ( { - view_box . min_x , - view_box . min_y } ) . multiply ( path_transform ) ;
2023-04-17 01:20:24 +01:00
viewbox_scale = viewbox_transform . scale_factor ;
2022-02-27 21:00:04 +01:00
}
2023-04-10 12:28:55 +01:00
// Stroke increases the path's size by stroke_width/2 per side.
2023-04-17 01:20:24 +01:00
auto path_bounding_box = path_transform . map ( path . bounding_box ( ) ) . to_type < CSSPixels > ( ) ;
2023-05-24 10:50:57 +02:00
CSSPixels stroke_width = static_cast < double > ( geometry_box . dom_node ( ) . visible_stroke_width ( ) ) * viewbox_scale ;
2023-04-10 12:28:55 +01:00
path_bounding_box . inflate ( stroke_width , stroke_width ) ;
2023-04-17 01:20:24 +01:00
geometry_box_state . set_content_offset ( path_bounding_box . top_left ( ) ) ;
2022-11-04 20:32:50 +00:00
geometry_box_state . set_content_width ( path_bounding_box . width ( ) ) ;
geometry_box_state . set_content_height ( path_bounding_box . height ( ) ) ;
2023-04-18 19:32:00 +02:00
} else if ( is < SVGSVGBox > ( descendant ) ) {
SVGFormattingContext nested_context ( m_state , descendant , this ) ;
nested_context . run ( descendant , layout_mode , available_space ) ;
2023-06-08 21:32:33 +03:00
} else if ( is < SVGTextBox > ( descendant ) ) {
auto const & svg_text_box = static_cast < SVGTextBox const & > ( descendant ) ;
// NOTE: This hack creates a layout state to ensure the existence of a paintable box node in LayoutState::commit(), even when none of the values from UsedValues impact the SVG text.
m_state . get_mutable ( svg_text_box ) ;
2023-07-01 21:41:00 +01:00
} else if ( is < SVGGraphicsBox > ( descendant ) ) {
// Same hack as above.
// FIXME: This should be sized based on its children.
auto const & svg_graphics_box = static_cast < SVGGraphicsBox const & > ( descendant ) ;
m_state . get_mutable ( svg_graphics_box ) ;
2021-09-18 00:06:16 +02:00
}
return IterationDecision : : Continue ;
} ) ;
2021-09-17 23:12:16 +02:00
}
}