Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: per-side border widths #836

Merged
merged 28 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e39b4cf
feat: per-side border widths
Tropix126 Aug 31, 2024
8605390
refactor: make `BorderWidth::is_all_zero` more sane
Tropix126 Sep 1, 2024
ffa1098
fix: wrong operator in `is_all_zero`
Tropix126 Sep 1, 2024
702046b
Merge remote-tracking branch 'upstream/main' into feat/per-side-borde…
Tropix126 Sep 13, 2024
9cc9a0c
implement parsing for per-side widths
Tropix126 Sep 13, 2024
efd883e
feat: impl `Display` for `Border`
Tropix126 Sep 13, 2024
7eced0c
fix lints
Tropix126 Sep 13, 2024
b4bbb1c
fix another lint
Tropix126 Sep 13, 2024
6129adb
chore: fmt
Tropix126 Sep 13, 2024
097ae71
Merge remote-tracking branch 'upstream/main' into feat/per-side-borde…
Tropix126 Sep 13, 2024
bbe4828
fix tests
Tropix126 Sep 13, 2024
c621f5d
fix: add `RRect::rect` to mocked engine
Tropix126 Sep 14, 2024
508a862
chore: improve border test coverage
Tropix126 Sep 14, 2024
d81ac26
fix: border test assertion
Tropix126 Sep 14, 2024
7568069
fix: radius offset calculations for different border alignments
Tropix126 Sep 15, 2024
705c88d
clean up border drawing code
Tropix126 Sep 15, 2024
c199d70
fmt
Tropix126 Sep 15, 2024
ba41652
revert needless `get_rounded_rect` name change
Tropix126 Sep 15, 2024
2053486
format
Tropix126 Sep 15, 2024
f375183
add `PathFillType` to mocked engine
Tropix126 Sep 15, 2024
689180d
mock `Path::set_fill_type`
Tropix126 Sep 15, 2024
8d0df4a
update docs
Tropix126 Sep 15, 2024
8490de3
Merge branch 'main' into feat/per-side-border-width
marc2332 Sep 15, 2024
1742537
fix jagged antialiasing on borders
Tropix126 Sep 15, 2024
aef3410
lint
Tropix126 Sep 15, 2024
0f2e889
mock `Canvas::draw_drrect` and `RRect::bounds`
Tropix126 Sep 15, 2024
2c8d092
simplify border visibility check
Tropix126 Sep 28, 2024
42a6593
Merge branch 'main' into feat/per-side-border-width
marc2332 Sep 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 223 additions & 79 deletions crates/core/src/elements/rect.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use freya_engine::prelude::*;
use freya_native_core::real_dom::NodeImmutable;
use freya_node_state::{
Border,
BorderAlignment,
BorderStyle,
CanvasRunnerContext,
CornerRadius,
Fill,
ReferencesState,
ShadowPosition,
Expand All @@ -12,7 +13,6 @@ use freya_node_state::{
use torin::{
prelude::{
Area,
AreaModel,
CursorPoint,
LayoutNode,
Point2D,
Expand Down Expand Up @@ -48,6 +48,203 @@ impl RectElement {
],
)
}

fn outer_border_path_corner_radius(
alignment: BorderAlignment,
corner_radius: f32,
width_1: f32,
width_2: f32,
) -> f32 {
if alignment == BorderAlignment::Inner || corner_radius == 0.0 {
return corner_radius;
}

let mut offset = if width_1 == 0.0 {
width_2
} else if width_2 == 0.0 {
width_1
} else {
width_1.min(width_2)
};

if alignment == BorderAlignment::Center {
offset *= 0.5;
}

corner_radius + offset
}

fn inner_border_path_corner_radius(
alignment: BorderAlignment,
corner_radius: f32,
width_1: f32,
width_2: f32,
) -> f32 {
if alignment == BorderAlignment::Outer || corner_radius == 0.0 {
return corner_radius;
}

let mut offset = if width_1 == 0.0 {
width_2
} else if width_2 == 0.0 {
width_1
} else {
width_1.min(width_2)
};

if alignment == BorderAlignment::Center {
offset *= 0.5;
}

corner_radius - offset
}

/// Returns a `Path` that will draw a [`Border`] around a base rectangle.
///
/// We don't use Skia's stroking API here, since we might need different widths for each side.
fn border_path(base_rect: Rect, base_corner_radius: CornerRadius, border: &Border) -> Path {
let border_alignment = border.alignment;
let border_width = border.width;

let mut path = Path::new();
path.set_fill_type(PathFillType::EvenOdd);

// First we create a path that is outset from the rect by a certain amount on each side.
//
// Let's call this the outer border path.
{
// Calculuate the outer corner radius for the border.
let corner_radius = CornerRadius {
top_left: Self::outer_border_path_corner_radius(
border_alignment,
base_corner_radius.top_left,
border_width.top,
border_width.left,
),
top_right: Self::outer_border_path_corner_radius(
border_alignment,
base_corner_radius.top_right,
border_width.top,
border_width.right,
),
bottom_left: Self::outer_border_path_corner_radius(
border_alignment,
base_corner_radius.bottom_left,
border_width.bottom,
border_width.left,
),
bottom_right: Self::outer_border_path_corner_radius(
border_alignment,
base_corner_radius.bottom_right,
border_width.bottom,
border_width.right,
),
smoothing: base_corner_radius.smoothing,
};

let rrect = RRect::new_rect_radii(
{
let mut rect = base_rect;
let alignment_scale = match border_alignment {
BorderAlignment::Outer => 1.0,
BorderAlignment::Center => 0.5,
BorderAlignment::Inner => 0.0,
};

rect.left -= border_width.left * alignment_scale;
rect.top -= border_width.top * alignment_scale;
rect.right += border_width.right * alignment_scale;
rect.bottom += border_width.bottom * alignment_scale;

rect
},
&[
(corner_radius.top_left, corner_radius.top_left).into(),
(corner_radius.top_right, corner_radius.top_right).into(),
(corner_radius.bottom_right, corner_radius.bottom_right).into(),
(corner_radius.bottom_left, corner_radius.bottom_left).into(),
],
);

if corner_radius.smoothing > 0.0 {
path.add_path(
&corner_radius.smoothed_path(rrect),
Point::new(rrect.rect().x(), rrect.rect().y()),
None,
);
} else {
path.add_rrect(rrect, None);
}
}

// After the outer path, we will then move to the inner bounds of the border.
{
// Calculuate the inner corner radius for the border.
let corner_radius = CornerRadius {
top_left: Self::inner_border_path_corner_radius(
border_alignment,
base_corner_radius.top_left,
border_width.top,
border_width.left,
),
top_right: Self::inner_border_path_corner_radius(
border_alignment,
base_corner_radius.top_right,
border_width.top,
border_width.right,
),
bottom_left: Self::inner_border_path_corner_radius(
border_alignment,
base_corner_radius.bottom_left,
border_width.bottom,
border_width.left,
),
bottom_right: Self::inner_border_path_corner_radius(
border_alignment,
base_corner_radius.bottom_right,
border_width.bottom,
border_width.right,
),
smoothing: base_corner_radius.smoothing,
};

let rrect = RRect::new_rect_radii(
{
let mut rect = base_rect;
let alignment_scale = match border_alignment {
BorderAlignment::Outer => 0.0,
BorderAlignment::Center => 0.5,
BorderAlignment::Inner => 1.0,
};

rect.left += border_width.left * alignment_scale;
rect.top += border_width.top * alignment_scale;
rect.right -= border_width.right * alignment_scale;
rect.bottom -= border_width.bottom * alignment_scale;

rect
},
&[
(corner_radius.top_left, corner_radius.top_left).into(),
(corner_radius.top_right, corner_radius.top_right).into(),
(corner_radius.bottom_right, corner_radius.bottom_right).into(),
(corner_radius.bottom_left, corner_radius.bottom_left).into(),
],
);

if corner_radius.smoothing > 0.0 {
path.add_path(
&corner_radius.smoothed_path(rrect),
Point::new(rrect.rect().x(), rrect.rect().y()),
None,
);
} else {
path.add_rrect(rrect, None);
}
};

path
}
}

impl ElementUtils for RectElement {
Expand Down Expand Up @@ -109,22 +306,22 @@ impl ElementUtils for RectElement {
}
}

let mut radius = node_style.corner_radius;
radius.scale(scale_factor);
let mut corner_radius = node_style.corner_radius;
corner_radius.scale(scale_factor);

let rounded_rect = RRect::new_rect_radii(
Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
&[
(radius.top_left, radius.top_left).into(),
(radius.top_right, radius.top_right).into(),
(radius.bottom_right, radius.bottom_right).into(),
(radius.bottom_left, radius.bottom_left).into(),
(corner_radius.top_left, corner_radius.top_left).into(),
(corner_radius.top_right, corner_radius.top_right).into(),
(corner_radius.bottom_right, corner_radius.bottom_right).into(),
(corner_radius.bottom_left, corner_radius.bottom_left).into(),
],
);

if radius.smoothing > 0.0 {
if corner_radius.smoothing > 0.0 {
path.add_path(
&radius.smoothed_path(rounded_rect),
&corner_radius.smoothed_path(rounded_rect),
(area.min_x(), area.min_y()),
None,
);
Expand Down Expand Up @@ -181,7 +378,7 @@ impl ElementUtils for RectElement {
}

// Add either the RRect or smoothed path based on whether smoothing is used.
if radius.smoothing > 0.0 {
if corner_radius.smoothing > 0.0 {
shadow_path.add_path(
&node_style
.corner_radius
Expand Down Expand Up @@ -212,17 +409,14 @@ impl ElementUtils for RectElement {
}

// Borders
if node_style.border.width > 0.0 && node_style.border.style != BorderStyle::None {
let mut border_width = node_style.border.width;
border_width *= scale_factor;
if node_style.border.is_visible() {
let mut border = node_style.border.clone();
border.scale(scale_factor);

// Create a new paint and path
// Create a new paint
let mut border_paint = paint.clone();
let mut border_path = Path::new();

// Setup paint params
border_paint.set_anti_alias(true);
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_style(PaintStyle::Fill);
match &node_style.border.fill {
Fill::Color(color) => {
border_paint.set_color(*color);
Expand All @@ -237,31 +431,11 @@ impl ElementUtils for RectElement {
border_paint.set_shader(gradient.into_shader(area));
}
}
border_paint.set_stroke_width(border_width);

// Skia draws strokes centered on the edge of the path. This means that half of the stroke is inside the path, and half outside.
// For Inner and Outer borders, we need to grow or shrink the stroke path by half the border width.
let outset = Point::new(border_width / 2.0, border_width / 2.0)
* match node_style.border.alignment {
BorderAlignment::Center => 0.0,
BorderAlignment::Inner => -1.0,
BorderAlignment::Outer => 1.0,
};

// Add either the RRect or smoothed path based on whether smoothing is used.
if radius.smoothing > 0.0 {
border_path.add_path(
&node_style
.corner_radius
.smoothed_path(rounded_rect.with_outset(outset)),
Point::new(area.min_x(), area.min_y()) - outset,
None,
);
} else {
border_path.add_rrect(rounded_rect.with_outset(outset), None);
}

canvas.draw_path(&border_path, &border_paint);
canvas.draw_path(
&Self::border_path(*rounded_rect.rect(), corner_radius, &border),
&border_paint,
);
}

let references = node_ref.get::<ReferencesState>().unwrap();
Expand Down Expand Up @@ -370,50 +544,20 @@ impl ElementUtils for RectElement {
}
}

if node_style.border.width > 0.0 && node_style.border.style != BorderStyle::None {
let mut border_width = node_style.border.width;
border_width *= scale_factor;
if node_style.border.is_visible() {
let mut border = node_style.border.clone();
border.scale(scale_factor);

// Create a new paint and path
let mut border_path = Path::new();

// Skia draws strokes centered on the edge of the path. This means that half of the stroke is inside the path, and half outside.
// For Inner and Outer borders, we need to grow or shrink the stroke path by half the border width.
let outset = Point::new(border_width / 2.0, border_width / 2.0)
* match node_style.border.alignment {
BorderAlignment::Center => 0.0,
BorderAlignment::Inner => -1.0,
BorderAlignment::Outer => 1.0,
};

// Add either the RRect or smoothed path based on whether smoothing is used.
if radius.smoothing > 0.0 {
border_path.add_path(
&node_style
.corner_radius
.smoothed_path(rounded_rect.with_outset(outset)),
Point::new(area.min_x(), area.min_y()) - outset,
None,
);
} else {
border_path.add_rrect(rounded_rect.with_outset(outset), None);
}
let border_path =
Self::border_path(*rounded_rect.rect(), node_style.corner_radius, &border);

let border_bounds = border_path.bounds();
let border_area = Area::new(
Point2D::new(border_bounds.x(), border_bounds.y()),
Size2D::new(border_bounds.width(), border_bounds.height()),
);
area = area.union(&border_area.round_out());

match node_style.border.alignment {
BorderAlignment::Outer => area.expand(&Size2D::new(
node_style.border.width,
node_style.border.width,
)),
BorderAlignment::Center => area.expand(&Size2D::new(border_width, border_width)),
_ => {}
}
area = area.union(&border_area.round_out());
}

area
Expand Down
Loading