diff --git a/components/layout_2020/replaced.rs b/components/layout_2020/replaced.rs index f7c44046717..fc07b6da746 100644 --- a/components/layout_2020/replaced.rs +++ b/components/layout_2020/replaced.rs @@ -14,6 +14,7 @@ use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder}; use pixels::Image; use serde::Serialize; use servo_arc::Arc as ServoArc; +use style::logical_geometry::Direction; use style::properties::ComputedValues; use style::servo::url::ComputedUrl; use style::values::computed::image::Image as ComputedImage; @@ -359,12 +360,18 @@ impl ReplacedContent { ) -> LogicalVec2 { let mode = style.writing_mode; let intrinsic_size = self.flow_relative_intrinsic_size(style); - let intrinsic_ratio = self.inline_size_over_block_size_intrinsic_ratio(style); + let intrinsic_ratio = style.preferred_aspect_ratio( + self.inline_size_over_block_size_intrinsic_ratio(style), + containing_block, + ); let box_size = box_size.unwrap_or( style .content_box_size(containing_block, pbm) - .map(|v| v.map(Au::from)), + // We need to clamp to zero here to obtain the proper aspect + // ratio when box-sizing is border-box and the inner box size + // would otherwise be negative. + .map(|v| v.map(|v| Au::from(v).max(Au::zero()))), ); let max_box_size = style .content_max_box_size(containing_block, pbm) @@ -393,8 +400,8 @@ impl ReplacedContent { LogicalVec2 { inline, block } }, (AuOrAuto::LengthPercentage(inline), AuOrAuto::Auto) => { - let block = if let Some(i_over_b) = intrinsic_ratio { - inline.scale_by(1.0 / i_over_b) + let block = if let Some(ratio) = intrinsic_ratio { + ratio.compute_dependent_size(Direction::Block, inline) } else if let Some(block) = intrinsic_size.block { block } else { @@ -403,8 +410,8 @@ impl ReplacedContent { LogicalVec2 { inline, block } }, (AuOrAuto::Auto, AuOrAuto::LengthPercentage(block)) => { - let inline = if let Some(i_over_b) = intrinsic_ratio { - block.scale_by(i_over_b) + let inline = if let Some(ratio) = intrinsic_ratio { + ratio.compute_dependent_size(Direction::Inline, block) } else if let Some(inline) = intrinsic_size.inline { inline } else { @@ -416,10 +423,10 @@ impl ReplacedContent { let inline_size = match (intrinsic_size.inline, intrinsic_size.block, intrinsic_ratio) { (Some(inline), _, _) => inline, - (None, Some(block), Some(i_over_b)) => { + (None, Some(block), Some(ratio)) => { // “used height” in CSS 2 is always gonna be the intrinsic one, // since it is available. - block.scale_by(i_over_b) + ratio.compute_dependent_size(Direction::Inline, block) }, // FIXME // @@ -442,9 +449,9 @@ impl ReplacedContent { }; let block_size = if let Some(block) = intrinsic_size.block { block - } else if let Some(i_over_b) = intrinsic_ratio { + } else if let Some(ratio) = intrinsic_ratio { // “used width” in CSS 2 is what we just computed above - inline_size.scale_by(1.0 / i_over_b) + ratio.compute_dependent_size(Direction::Block, inline_size) } else { default_object_size().block }; @@ -459,7 +466,7 @@ impl ReplacedContent { // https://drafts.csswg.org/css2/visudet.html#min-max-widths // “However, for replaced elements with an intrinsic ratio and both // 'width' and 'height' specified as 'auto', the algorithm is as follows” - if let (AuOrAuto::Auto, AuOrAuto::Auto, Some(i_over_b)) = + if let (AuOrAuto::Auto, AuOrAuto::Auto, Some(ratio)) = (box_size.inline, box_size.block, intrinsic_ratio) { let LogicalVec2 { @@ -495,26 +502,28 @@ impl ReplacedContent { // Row 2. (Violation::Above(max_inline_size), Violation::None) => LogicalVec2 { inline: max_inline_size, - block: max_inline_size - .scale_by(1.0 / i_over_b) + block: ratio + .compute_dependent_size(Direction::Block, max_inline_size) .max(min_box_size.block), }, // Row 3. (Violation::Below(min_inline_size), Violation::None) => LogicalVec2 { inline: min_inline_size, - block: min_inline_size - .scale_by(1.0 / i_over_b) + block: ratio + .compute_dependent_size(Direction::Block, min_inline_size) .clamp_below_max(max_box_size.block), }, // Row 4. (Violation::None, Violation::Above(max_block_size)) => LogicalVec2 { - inline: max_block_size.scale_by(i_over_b).max(min_box_size.inline), + inline: ratio + .compute_dependent_size(Direction::Inline, max_block_size) + .max(min_box_size.inline), block: max_block_size, }, // Row 5. (Violation::None, Violation::Below(min_block_size)) => LogicalVec2 { - inline: min_block_size - .scale_by(i_over_b) + inline: ratio + .compute_dependent_size(Direction::Inline, min_block_size) .clamp_below_max(max_box_size.inline), block: min_block_size, }, @@ -524,14 +533,16 @@ impl ReplacedContent { // Row 6. LogicalVec2 { inline: max_inline_size, - block: max_inline_size - .scale_by(1.0 / i_over_b) + block: ratio + .compute_dependent_size(Direction::Block, max_inline_size) .max(min_box_size.block), } } else { // Row 7. LogicalVec2 { - inline: max_block_size.scale_by(i_over_b).max(min_box_size.inline), + inline: ratio + .compute_dependent_size(Direction::Inline, max_block_size) + .max(min_box_size.inline), block: max_block_size, } } @@ -541,8 +552,8 @@ impl ReplacedContent { if min_inline_size.0 * block_size.0 <= min_block_size.0 * inline_size.0 { // Row 8. LogicalVec2 { - inline: min_block_size - .scale_by(i_over_b) + inline: ratio + .compute_dependent_size(Direction::Inline, min_block_size) .clamp_below_max(max_box_size.inline), block: min_block_size, } @@ -550,8 +561,8 @@ impl ReplacedContent { // Row 9. LogicalVec2 { inline: min_inline_size, - block: min_inline_size - .scale_by(1.0 / i_over_b) + block: ratio + .compute_dependent_size(Direction::Block, min_inline_size) .clamp_below_max(max_box_size.block), } } diff --git a/components/layout_2020/style_ext.rs b/components/layout_2020/style_ext.rs index da3f94f88c1..20a89bedf28 100644 --- a/components/layout_2020/style_ext.rs +++ b/components/layout_2020/style_ext.rs @@ -6,7 +6,7 @@ use app_units::Au; use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode; use style::computed_values::position::T as ComputedPosition; use style::computed_values::transform_style::T as ComputedTransformStyle; -use style::logical_geometry::WritingMode; +use style::logical_geometry::{Direction, WritingMode}; use style::properties::longhands::backface_visibility::computed_value::T as BackfaceVisiblity; use style::properties::longhands::box_sizing::computed_value::T as BoxSizing; use style::properties::longhands::column_span::computed_value::T as ColumnSpan; @@ -15,7 +15,9 @@ use style::values::computed::image::Image as ComputedImageLayer; use style::values::computed::{Length, LengthPercentage, NonNegativeLengthPercentage, Size}; use style::values::generics::box_::Perspective; use style::values::generics::length::MaxSize; +use style::values::generics::position::{GenericAspectRatio, PreferredRatio}; use style::values::specified::{box_ as stylo, Overflow}; +use style::values::CSSFloat; use style::Zero; use webrender_api as wr; @@ -143,6 +145,43 @@ impl PaddingBorderMargin { } } +/// Resolved `aspect-ratio` property with respect to a specific element. Depends +/// on that element's `box-sizing` (and padding and border, if that `box-sizing` +/// is `border-box`). +#[derive(Clone, Copy, Debug)] +pub(crate) struct AspectRatio { + /// If the element that this aspect ratio belongs to uses box-sizing: + /// border-box, and the aspect-ratio property does not contain "auto", then + /// the aspect ratio is in respect to the border box. This will then contain + /// the summed sizes of the padding and border. Otherwise, it's 0. + box_sizing_adjustment: LogicalVec2, + /// The ratio itself (inline over block). + i_over_b: CSSFloat, +} + +impl AspectRatio { + /// Given one side length, compute the other one. + pub(crate) fn compute_dependent_size( + &self, + ratio_dependent_axis: Direction, + ratio_determining_size: Au, + ) -> Au { + match ratio_dependent_axis { + // Calculate the inline size from the block size + Direction::Inline => { + (ratio_determining_size + self.box_sizing_adjustment.block).scale_by(self.i_over_b) - + self.box_sizing_adjustment.inline + }, + // Calculate the block size from the inline size + Direction::Block => { + (ratio_determining_size + self.box_sizing_adjustment.inline) + .scale_by(1.0 / self.i_over_b) - + self.box_sizing_adjustment.block + }, + } + } +} + pub(crate) trait ComputedValuesExt { fn box_offsets( &self, @@ -198,6 +237,11 @@ pub(crate) trait ComputedValuesExt { &self, fragment_flags: FragmentFlags, ) -> bool; + fn preferred_aspect_ratio( + &self, + natural_aspect_ratio: Option, + containing_block: &ContainingBlock, + ) -> Option; fn background_is_transparent(&self) -> bool; fn get_webrender_primitive_flags(&self) -> wr::PrimitiveFlags; } @@ -535,6 +579,75 @@ impl ComputedValuesExt for ComputedValues { false } + /// Resolve the preferred aspect ratio according to the given natural aspect + /// ratio and the `aspect-ratio` property. + /// See . + fn preferred_aspect_ratio( + &self, + natural_aspect_ratio: Option, + containing_block: &ContainingBlock, + ) -> Option { + let GenericAspectRatio { + auto, + ratio: mut preferred_ratio, + } = self.clone_aspect_ratio(); + + // For all cases where a ratio is specified: + // "If the is degenerate, the property instead behaves as auto." + if matches!(preferred_ratio, PreferredRatio::Ratio(ratio) if ratio.is_degenerate()) { + preferred_ratio = PreferredRatio::None; + } + + match (auto, preferred_ratio) { + // The value `auto`. Either the ratio was not specified, or was + // degenerate and set to PreferredRatio::None above. + // + // "Replaced elements with a natural aspect ratio use that aspect + // ratio; otherwise the box has no preferred aspect ratio. Size + // calculations involving the aspect ratio work with the content box + // dimensions always." + (_, PreferredRatio::None) => natural_aspect_ratio.map(|natural_ratio| AspectRatio { + i_over_b: natural_ratio, + box_sizing_adjustment: LogicalVec2::zero(), + }), + // "If both auto and a are specified together, the preferred + // aspect ratio is the specified ratio of width / height unless it + // is a replaced element with a natural aspect ratio, in which case + // that aspect ratio is used instead. In all cases, size + // calculations involving the aspect ratio work with the content box + // dimensions always." + (true, PreferredRatio::Ratio(preferred_ratio)) => match natural_aspect_ratio { + Some(natural_ratio) => Some(AspectRatio { + i_over_b: natural_ratio, + box_sizing_adjustment: LogicalVec2::zero(), + }), + None => Some(AspectRatio { + i_over_b: (preferred_ratio.0).0 / (preferred_ratio.1).0, + box_sizing_adjustment: LogicalVec2::zero(), + }), + }, + + // "The box’s preferred aspect ratio is the specified ratio of width + // / height. Size calculations involving the aspect ratio work with + // the dimensions of the box specified by box-sizing." + (false, PreferredRatio::Ratio(preferred_ratio)) => { + // If the `box-sizing` is `border-box`, use the padding and + // border when calculating the aspect ratio. + let box_sizing_adjustment = match self.clone_box_sizing() { + BoxSizing::ContentBox => LogicalVec2::zero(), + BoxSizing::BorderBox => { + self.padding_border_margin(containing_block) + .padding_border_sums + }, + }; + Some(AspectRatio { + i_over_b: (preferred_ratio.0).0 / (preferred_ratio.1).0, + box_sizing_adjustment, + }) + }, + } + } + /// Whether or not this style specifies a non-transparent background. fn background_is_transparent(&self) -> bool { let background = self.get_background(); diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index 2d309d489bf..de68405fbd8 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -556187,6 +556187,20 @@ ] }, "aspect-ratio": { + "box-sizing-dimensions.html": [ + "126c3e30583edc1fb61ed5dab62187e64b597500", + [ + null, + {} + ] + ], + "box-sizing-squashed.html": [ + "542dabe1b5c11784257018b6352151d9940f59bf", + [ + null, + {} + ] + ], "fractional-aspect-ratio.html": [ "7186f35f8a2e62d6bae515ab0453c2af5041c23b", [ diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-dimensions.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-dimensions.html.ini new file mode 100644 index 00000000000..ea6d2711aa0 --- /dev/null +++ b/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-dimensions.html.ini @@ -0,0 +1,12 @@ +[box-sizing-dimensions.html] + [.item 5] + expected: FAIL + + [.item 6] + expected: FAIL + + [.item 7] + expected: FAIL + + [.item 8] + expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-squashed.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-squashed.html.ini new file mode 100644 index 00000000000..2746cd42a2e --- /dev/null +++ b/tests/wpt/meta/css/css-sizing/aspect-ratio/box-sizing-squashed.html.ini @@ -0,0 +1,18 @@ +[box-sizing-squashed.html] + [.item 10] + expected: FAIL + + [.item 12] + expected: FAIL + + [.item 13] + expected: FAIL + + [.item 14] + expected: FAIL + + [.item 15] + expected: FAIL + + [.item 16] + expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-015.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-015.html.ini deleted file mode 100644 index 3b6708cbdb8..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-015.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[flex-aspect-ratio-015.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-016.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-016.html.ini deleted file mode 100644 index 5e23eeb1b9b..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/flex-aspect-ratio-016.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[flex-aspect-ratio-016.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-003.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-003.html.ini deleted file mode 100644 index 6110268aa54..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-003.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[grid-aspect-ratio-003.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-004.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-004.html.ini deleted file mode 100644 index 85a65b2259f..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/grid-aspect-ratio-004.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[grid-aspect-ratio-004.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-001.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-001.html.ini deleted file mode 100644 index 5c831df5045..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-001.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-001.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-002.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-002.html.ini deleted file mode 100644 index 9be1a75b5c6..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-002.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-002.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-003.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-003.html.ini deleted file mode 100644 index ed3f1adefc3..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-003.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-003.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-009.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-009.html.ini deleted file mode 100644 index b094fe452e8..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-009.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-009.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-012.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-012.html.ini deleted file mode 100644 index f0f4f86b8fd..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-012.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-012.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-024.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-024.html.ini new file mode 100644 index 00000000000..df3ff064f31 --- /dev/null +++ b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-024.html.ini @@ -0,0 +1,2 @@ +[replaced-element-024.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-025.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-025.html.ini deleted file mode 100644 index 6e5eadfede8..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-025.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-025.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-026.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-026.html.ini new file mode 100644 index 00000000000..7fb39b59914 --- /dev/null +++ b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-026.html.ini @@ -0,0 +1,2 @@ +[replaced-element-026.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-027.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-027.html.ini deleted file mode 100644 index 3207d66a97f..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-027.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-027.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-028.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-028.html.ini new file mode 100644 index 00000000000..4003e429144 --- /dev/null +++ b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-028.html.ini @@ -0,0 +1,3 @@ +[replaced-element-028.html] + [CSS aspect-ratio: img] + expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-031.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-031.html.ini deleted file mode 100644 index bd6a2a25263..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-031.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-031.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-032.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-032.html.ini deleted file mode 100644 index cbb89ffd95a..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-032.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-032.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-033.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-033.html.ini deleted file mode 100644 index 26c172e3a52..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-033.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-033.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-036.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-036.html.ini deleted file mode 100644 index 453560a3f33..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-036.html.ini +++ /dev/null @@ -1,6 +0,0 @@ -[replaced-element-036.html] - [display:block img should be 200px high] - expected: FAIL - - [display:inline-block img should be 200px high] - expected: FAIL diff --git a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-dynamic-aspect-ratio.html.ini b/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-dynamic-aspect-ratio.html.ini deleted file mode 100644 index 919fb85dac8..00000000000 --- a/tests/wpt/meta/css/css-sizing/aspect-ratio/replaced-element-dynamic-aspect-ratio.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[replaced-element-dynamic-aspect-ratio.html] - expected: FAIL diff --git a/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-dimensions.html b/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-dimensions.html new file mode 100644 index 00000000000..126c3e30583 --- /dev/null +++ b/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-dimensions.html @@ -0,0 +1,51 @@ + + +CSS aspect-ratio: uses content box when "auto" is present and box-sizing dimensions when it is absent + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
diff --git a/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-squashed.html b/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-squashed.html new file mode 100644 index 00000000000..542dabe1b5c --- /dev/null +++ b/tests/wpt/tests/css/css-sizing/aspect-ratio/box-sizing-squashed.html @@ -0,0 +1,62 @@ + + +CSS aspect-ratio: correct ratio maintained when box-sizing: border-box and one axis is clamped to 0 + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +