diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 067eb2775..f5401dfb8 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -16,9 +16,9 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset::subset; use crate::font::{find_name, FaceId, FontStore}; -use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; +use crate::frame::{rect_path, rect_paths, Element, Frame, Geometry, Group, Shape, Text}; use crate::geom::{ - self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform, + self, Color, Em, Length, Numeric, Paint, Point, Sides, Size, Stroke, Transform, }; use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::Context; @@ -499,16 +499,16 @@ impl<'a> PageExporter<'a> { } fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { - if shape.fill.is_none() && shape.stroke.is_none() { + if shape.fill.is_none() && shape.stroke.iter().all(Option::is_none) { return; } match shape.geometry { - Geometry::Rect(size) => { + Geometry::Rect(size, radius) => { let w = size.x.to_f32(); let h = size.y.to_f32(); if w > 0.0 && h > 0.0 { - self.content.rect(x, y, w, h); + self.write_path(x, y, &rect_path(size, radius)); } } Geometry::Ellipse(size) => { @@ -530,16 +530,37 @@ impl<'a> PageExporter<'a> { self.set_fill(fill); } - if let Some(stroke) = shape.stroke { - self.set_stroke(stroke); + // The stroke does not exist or is non-uniform. + let mut use_stroke = false; + if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) { + if let Some(stroke) = shape.stroke.top { + self.set_stroke(stroke); + use_stroke = true; + } } - match (shape.fill, shape.stroke) { - (None, None) => unreachable!(), - (Some(_), None) => self.content.fill_nonzero(), - (None, Some(_)) => self.content.stroke(), - (Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(), + match (shape.fill, use_stroke) { + (None, false) => self.content.end_path(), + (Some(_), false) => self.content.fill_nonzero(), + (None, true) => self.content.stroke(), + (Some(_), true) => self.content.fill_nonzero_and_stroke(), }; + + if let Geometry::Rect(size, radius) = shape.geometry { + if !use_stroke { + for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) { + if let Some(stroke) = stroke { + self.write_shape(x, y, &Shape { + geometry: Geometry::Path(path), + fill: None, + stroke: Sides::splat(Some(stroke)), + }); + } else { + continue; + } + } + } + } } fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) { diff --git a/src/export/render.rs b/src/export/render.rs index c3b92d315..9c674acb9 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -8,8 +8,9 @@ use ttf_parser::{GlyphId, OutlineBuilder}; use usvg::FitTo; use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; -use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform}; +use crate::geom::{self, Length, Paint, PathElement, Sides, Size, Stroke, Transform}; use crate::image::{Image, RasterImage, Svg}; +use crate::library::prelude::{rect_path, rect_paths}; use crate::Context; /// Export a frame into a rendered image. @@ -298,12 +299,7 @@ fn render_shape( shape: &Shape, ) -> Option<()> { let path = match shape.geometry { - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?; - sk::PathBuilder::from_rect(rect) - } + Geometry::Rect(size, radius) => convert_path(&rect_path(size, radius))?, Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?, Geometry::Line(target) => { let mut builder = sk::PathBuilder::new(); @@ -315,7 +311,7 @@ fn render_shape( if let Some(fill) = shape.fill { let mut paint: sk::Paint = fill.into(); - if matches!(shape.geometry, Geometry::Rect(_)) { + if matches!(shape.geometry, Geometry::Rect(_, _)) { paint.anti_alias = false; } @@ -323,11 +319,27 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - if let Some(Stroke { paint, thickness }) = shape.stroke { - let paint = paint.into(); - let mut stroke = sk::Stroke::default(); - stroke.width = thickness.to_f32(); - canvas.stroke_path(&path, &paint, &stroke, ts, mask); + if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) { + if let Some(Stroke { paint, thickness }) = shape.stroke.top { + let paint = paint.into(); + let mut stroke = sk::Stroke::default(); + stroke.width = thickness.to_f32(); + canvas.stroke_path(&path, &paint, &stroke, ts, mask); + } + } else { + if let Geometry::Rect(size, radius) = shape.geometry { + for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) { + if let Some(stroke) = stroke { + render_shape(canvas, ts, mask, &Shape { + geometry: Geometry::Path(path), + fill: None, + stroke: Sides::splat(Some(stroke)), + })?; + } else { + continue; + } + } + } } Some(()) diff --git a/src/frame.rs b/src/frame.rs index 5ee6e77e4..f889601eb 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -1,11 +1,13 @@ //! Finished layouts. use std::fmt::{self, Debug, Formatter, Write}; +use std::mem; use std::sync::Arc; use crate::font::FaceId; use crate::geom::{ - Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform, + Align, Angle, Em, Get, Length, Numeric, Paint, Path, Point, Side, Sides, Size, Spec, + Stroke, Transform, }; use crate::image::ImageId; use crate::util::{EcoString, MaybeShared}; @@ -306,7 +308,7 @@ pub struct Shape { /// The shape's background fill. pub fill: Option, /// The shape's border stroke. - pub stroke: Option, + pub stroke: Sides>, } /// A shape's geometry. @@ -314,8 +316,8 @@ pub struct Shape { pub enum Geometry { /// A line to a point (relative to its position). Line(Point), - /// A rectangle with its origin in the topleft corner. - Rect(Size), + /// A rectangle with its origin in the topleft corner and a border radius. + Rect(Size, Sides), /// A ellipse with its origin in the topleft corner. Ellipse(Size), /// A bezier path. @@ -328,7 +330,7 @@ impl Geometry { Shape { geometry: self, fill: Some(fill), - stroke: None, + stroke: Sides::splat(None), } } @@ -337,7 +339,170 @@ impl Geometry { Shape { geometry: self, fill: None, - stroke: Some(stroke), + stroke: Sides::splat(Some(stroke)), } } } + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Connection { + None, + Left, + Right, + Both, +} + +impl Connection { + pub fn advance(self, right: bool) -> Self { + match self { + Self::Right | Self::Both => { + if right { + Self::Both + } else { + Self::Left + } + } + Self::Left | Self::None => { + if right { + Self::Right + } else { + Self::None + } + } + } + } + + fn left(self) -> bool { + matches!(self, Self::Left | Self::Both) + } + + fn right(self) -> bool { + matches!(self, Self::Right | Self::Both) + } +} + +/// Draws one side of the rounded rectangle. Will always draw the left arc. The +/// right arc will be drawn halfway iff there is no connection. +fn draw_side( + path: &mut Path, + side: Side, + size: Size, + radius_left: Length, + radius_right: Length, + connection: Connection, +) { + let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| { + let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y); + [d, c, b, a] + }; + + let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 }); + let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 }); + + let (arc1, arc2) = match side { + Side::Top => { + let arc1 = reversed(angle_left, radius_left, true, true, false) + .map(|x| x + Point::with_x(radius_left)); + let arc2 = (-angle_right) + .bezier_arc(radius_right, true, true, false) + .map(|x| x + Point::with_x(size.x - radius_right)); + + (arc1, arc2) + } + Side::Right => { + let arc1 = reversed(-angle_left, radius_left, false, false, false) + .map(|x| x + Point::new(size.x, radius_left)); + + let arc2 = angle_right + .bezier_arc(radius_right, false, false, false) + .map(|x| x + Point::new(size.x, size.y - radius_right)); + + (arc1, arc2) + } + Side::Bottom => { + let arc1 = reversed(-angle_left, radius_left, true, false, false) + .map(|x| x + Point::new(size.x - radius_left, size.y)); + + let arc2 = angle_right + .bezier_arc(radius_right, true, false, false) + .map(|x| x + Point::new(radius_right, size.y)); + + (arc1, arc2) + } + Side::Left => { + let arc1 = reversed(angle_left, radius_left, false, false, true) + .map(|x| x + Point::with_y(size.y - radius_left)); + + let arc2 = (-angle_right) + .bezier_arc(radius_right, false, false, true) + .map(|x| x + Point::with_y(radius_right)); + + (arc1, arc2) + } + }; + + if !connection.left() { + path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); + } + + if !radius_left.is_zero() { + path.cubic_to(arc1[1], arc1[2], arc1[3]); + } + + path.line_to(arc2[0]); + + if !connection.right() && !radius_right.is_zero() { + path.cubic_to(arc2[1], arc2[2], arc2[3]); + } +} + +pub fn rect_paths( + size: Size, + radius: Sides, + strokes: Option>>, +) -> Vec<(Path, Option)> { + let strokes = strokes.unwrap_or_else(|| Sides::splat(None)); + let mut res = vec![]; + + let mut connection = Connection::None; + let mut path = Path::new(); + let sides = [Side::Top, Side::Right, Side::Bottom, Side::Left]; + let mut always_continuous = true; + + let radius = [ + radius.left, + radius.top, + radius.right, + radius.bottom, + radius.left, + ]; + + for (side, radius) in sides.into_iter().zip(radius.windows(2)) { + let stroke_continuity = strokes.get(side) == strokes.get(side.clockwise()); + connection = connection.advance(stroke_continuity && side != Side::Left); + always_continuous &= stroke_continuity; + + draw_side(&mut path, side, size, radius[0], radius[1], connection); + + if !stroke_continuity { + res.push((mem::take(&mut path), strokes.get(side))); + } + } + + if always_continuous { + path.close_path(); + } + + if !path.0.is_empty() { + res.push((path, strokes.left)); + } + + res +} + +pub fn rect_path(size: Size, radius: Sides) -> Path { + let mut paths = rect_paths(size, radius, None); + assert_eq!(paths.len(), 1); + + paths.pop().unwrap().0 +} diff --git a/src/geom/angle.rs b/src/geom/angle.rs index 888442f7a..65270ebd5 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -64,6 +64,51 @@ impl Angle { pub fn cos(self) -> f64 { self.to_rad().cos() } + + /// Get the control points for a bezier curve that describes a circular arc + /// of this angle with the given radius. + pub fn bezier_arc( + self, + radius: Length, + rotate: bool, + mirror_x: bool, + mirror_y: bool, + ) -> [Point; 4] { + let end = Point::new(self.cos() * radius - radius, self.sin() * radius); + let center = Point::new(-radius, Length::zero()); + + let mut ts = if mirror_y { + Transform::mirror_y() + } else { + Transform::identity() + }; + + if mirror_x { + ts = ts.pre_concat(Transform::mirror_x()); + } + + if rotate { + ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0))); + } + + let a = center * -1.0; + let b = end - center; + + let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); + let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); + let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) + / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); + + let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); + let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); + + [ + Point::zero(), + control_1.transform(ts), + control_2.transform(ts), + end.transform(ts), + ] + } } impl Numeric for Angle { diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 3584a1ce9..255c21eef 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -31,6 +31,32 @@ impl Sides { bottom: value, } } + + /// Maps the individual fields with `f`. + pub fn map(self, mut f: F) -> Sides + where + F: FnMut(T) -> U, + { + Sides { + left: f(self.left), + top: f(self.top), + right: f(self.right), + bottom: f(self.bottom), + } + } + + /// Returns an iterator over the sides. + pub fn iter(&self) -> impl Iterator { + [&self.left, &self.top, &self.right, &self.bottom].into_iter() + } + + /// Returns whether all sides are equal. + pub fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.left == self.top && self.top == self.right && self.right == self.bottom + } } impl Sides @@ -100,4 +126,24 @@ impl Side { Self::Bottom => Self::Top, } } + + /// The next side, clockwise. + pub fn clockwise(self) -> Self { + match self { + Self::Left => Self::Top, + Self::Top => Self::Right, + Self::Right => Self::Bottom, + Self::Bottom => Self::Left, + } + } + + /// The next side, counter-clockwise. + pub fn counter_clockwise(self) -> Self { + match self { + Self::Left => Self::Bottom, + Self::Top => Self::Left, + Self::Right => Self::Top, + Self::Bottom => Self::Right, + } + } } diff --git a/src/geom/transform.rs b/src/geom/transform.rs index 28a1af809..de2a97818 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -24,6 +24,30 @@ impl Transform { } } + /// Transform by mirroring along the x-axis. + pub fn mirror_x() -> Self { + Self { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: -Ratio::one(), + tx: Length::zero(), + ty: Length::zero(), + } + } + + /// Transform by mirroring along the y-axis. + pub fn mirror_y() -> Self { + Self { + sx: -Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::one(), + tx: Length::zero(), + ty: Length::zero(), + } + } + /// A translate transform. pub const fn translate(tx: Length, ty: Length) -> Self { Self { tx, ty, ..Self::identity() } diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 49c74c2f0..7a1bfb1f3 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -5,36 +5,46 @@ use crate::library::text::TextNode; /// Place a node into a sizable and fillable shape. #[derive(Debug, Hash)] -pub struct ShapeNode(pub Option); +pub struct AngularNode(pub Option); /// Place a node into a square. -pub type SquareNode = ShapeNode; +pub type SquareNode = AngularNode; /// Place a node into a rectangle. -pub type RectNode = ShapeNode; +pub type RectNode = AngularNode; -/// Place a node into a circle. -pub type CircleNode = ShapeNode; +// /// Place a node into a sizable and fillable shape. +// #[derive(Debug, Hash)] +// pub struct RoundNode(pub Option); -/// Place a node into an ellipse. -pub type EllipseNode = ShapeNode; +// /// Place a node into a circle. +// pub type CircleNode = RoundNode; + +// /// Place a node into an ellipse. +// pub type EllipseNode = RoundNode; #[node] -impl ShapeNode { +impl AngularNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. #[property(resolve, fold)] - pub const STROKE: Smart> = Smart::Auto; + pub const STROKE: Smart>> = Smart::Auto; + /// How much to pad the shape's content. - pub const PADDING: Relative = Relative::zero(); + #[property(resolve, fold)] + pub const INSET: Sides>> = Sides::splat(Relative::zero()); + + /// How much to extend the shape's dimensions beyond the allocated space. + #[property(resolve, fold)] + pub const OUTSET: Sides>> = Sides::splat(Relative::zero()); + + /// How much to round the shape's corners. + #[property(resolve, fold)] + pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let size = match S { - SQUARE => args.named::("size")?.map(Relative::from), - CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), - _ => None, - }; + let size = args.named::("size")?.map(Relative::from); let width = match size { None => args.named("width")?, @@ -52,7 +62,50 @@ impl ShapeNode { } } -impl Layout for ShapeNode { +castable! { + Sides>, + Expected: "stroke, dictionary with strokes for each side", + Value::None => { + Sides::splat(None) + }, + Value::Dict(values) => { + let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); + Sides { + top: get("top"), + right: get("right"), + bottom: get("bottom"), + left: get("left"), + } + }, + Value::Length(thickness) => Sides::splat(Some(RawStroke { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + })), + Value::Color(color) => Sides::splat(Some(RawStroke { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + })), + @stroke: RawStroke => Sides::splat(Some(*stroke)), +} + +castable! { + Sides>>, + Expected: "length or dictionary of lengths for each side", + Value::None => Sides::splat(None), + Value::Dict(values) => { + let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None); + Sides { + top: get("top"), + right: get("right"), + bottom: get("bottom"), + left: get("left"), + } + }, + Value::Length(l) => Sides::splat(Some(l.into())), + Value::Relative(r) => Sides::splat(Some(r)), +} + +impl Layout for AngularNode { fn layout( &self, ctx: &mut Context, @@ -61,50 +114,43 @@ impl Layout for ShapeNode { ) -> TypResult>> { let mut frames; if let Some(child) = &self.0 { - let mut padding = styles.get(Self::PADDING); - if is_round(S) { - padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0); - } + let inset = styles.get(Self::INSET); // Pad the child. - let child = child.clone().padded(Sides::splat(padding)); + let child = child + .clone() + .padded(inset.map(|side| side.map(|abs| RawLength::from(abs)))); let mut pod = Regions::one(regions.first, regions.base, regions.expand); frames = child.layout(ctx, &pod, styles)?; // Relayout with full expansion into square region to make sure // the result is really a square or circle. - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - let size = frames[0].size; - let desired = size.x.max(size.y); - desired.min(regions.first.x).min(regions.first.y) - }; + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + let size = frames[0].size; + let desired = size.x.max(size.y); + desired.min(regions.first.x).min(regions.first.y) + }; - pod.first = Size::splat(length); - pod.expand = Spec::splat(true); - frames = child.layout(ctx, &pod, styles)?; - } + pod.first = Size::splat(length); + pod.expand = Spec::splat(true); + frames = child.layout(ctx, &pod, styles)?; } else { // The default size that a shape takes on if it has no child and // enough space. let mut size = Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first); - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - size.x.min(size.y) - }; - size = Size::splat(length); + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) } else { - size = regions.expand.select(regions.first, size); - } + size.x.min(size.y) + }; + size = Size::splat(length); frames = vec![Arc::new(Frame::new(size))]; } @@ -114,18 +160,28 @@ impl Layout for ShapeNode { // Add fill and/or stroke. let fill = styles.get(Self::FILL); let stroke = match styles.get(Self::STROKE) { - Smart::Auto => fill.is_none().then(Stroke::default), - Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default), + Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), + Smart::Auto => Sides::splat(None), + Smart::Custom(strokes) => strokes.map(|s| Some(s.unwrap_or_default())), }; - if fill.is_some() || stroke.is_some() { - let geometry = if is_round(S) { - Geometry::Ellipse(frame.size) - } else { - Geometry::Rect(frame.size) - }; + let radius = { + let radius = styles.get(Self::RADIUS); - let shape = Shape { geometry, fill, stroke }; + Sides { + left: radius.left.relative_to(frame.size.x / 2.0), + top: radius.top.relative_to(frame.size.y / 2.0), + right: radius.right.relative_to(frame.size.x / 2.0), + bottom: radius.bottom.relative_to(frame.size.y / 2.0), + } + }; + + if fill.is_some() || stroke.iter().any(Option::is_some) { + let shape = Shape { + geometry: Geometry::Rect(frame.size, radius), + fill, + stroke, + }; frame.prepend(Point::zero(), Element::Shape(shape)); } @@ -152,13 +208,3 @@ const CIRCLE: ShapeKind = 2; /// A curve around two focal points. const ELLIPSE: ShapeKind = 3; - -/// Whether a shape kind is curvy. -fn is_round(kind: ShapeKind) -> bool { - matches!(kind, CIRCLE | ELLIPSE) -} - -/// Whether a shape kind has equal side length. -fn is_quadratic(kind: ShapeKind) -> bool { - matches!(kind, SQUARE | CIRCLE) -} diff --git a/src/model/layout.rs b/src/model/layout.rs index 511542862..63e8f0882 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -353,7 +353,8 @@ impl Layout for FillNode { ) -> TypResult>> { let mut frames = self.child.layout(ctx, regions, styles)?; for frame in &mut frames { - let shape = Geometry::Rect(frame.size).filled(self.fill); + let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) + .filled(self.fill); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames) @@ -378,7 +379,8 @@ impl Layout for StrokeNode { ) -> TypResult>> { let mut frames = self.child.layout(ctx, regions, styles)?; for frame in &mut frames { - let shape = Geometry::Rect(frame.size).stroked(self.stroke); + let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero())) + .stroked(self.stroke); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } Ok(frames) diff --git a/src/model/styles.rs b/src/model/styles.rs index eb7a70537..2e752625b 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -459,6 +459,22 @@ where } } +impl Fold for Sides> +where + T: Default, +{ + type Output = Sides; + + fn fold(self, outer: Self::Output) -> Self::Output { + Sides { + left: self.left.unwrap_or(outer.left), + right: self.right.unwrap_or(outer.right), + top: self.top.unwrap_or(outer.top), + bottom: self.bottom.unwrap_or(outer.bottom), + } + } +} + /// A scoped property barrier. /// /// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style