Skip to content

Commit

Permalink
Rounded rectangle drawing
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Nov 18, 2023
1 parent 74db2e6 commit ca138e7
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 7 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ image = { version = "0.24.6", features = ["png"] }
# [patch."https://github.com/khonsulabs/appit"]
# appit = { path = "../appit" }

# [patch."https://github.com/khonsulabs/figures"]
# figures = { path = "../figures" }
[patch."https://github.com/khonsulabs/figures"]
figures = { path = "../figures" }

# [patch."https://github.com/khonsulabs/shelf-packer"]
# shelf-packer = { path = "../shelf-packer" }
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ impl Kludgine {
view_formats: &[],
});

let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..wgpu::SamplerDescriptor::default()
});
let default_bindings = pipeline::bind_group(
device,
&binding_layout,
Expand Down
242 changes: 239 additions & 3 deletions src/shapes.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::ops::{Add, Neg};
use std::fmt::Debug;
use std::ops::{Add, Div, Mul, Neg, Sub};

use figures::units::{Lp, Px, UPx};
use figures::{FloatConversion, Point, Rect, ScreenScale};
use figures::{FloatConversion, IsZero, Point, Rect, ScreenScale};
use lyon_tessellation::{
FillGeometryBuilder, FillOptions, FillTessellator, FillVertex, FillVertexConstructor,
GeometryBuilder, GeometryBuilderError, StrokeGeometryBuilder, StrokeTessellator, StrokeVertex,
Expand Down Expand Up @@ -103,6 +104,51 @@ impl<Unit> Shape<Unit, false> {
path.stroke(color, options)
}

/// Returns a rounded rectangle with the specified corner radii that is
/// filled solid with `color`.
pub fn filled_round_rect(
rect: Rect<Unit>,
corner_radius: impl Into<CornerRadii<Unit>>,
color: Color,
) -> Shape<Unit, false>
where
Unit: Add<Output = Unit>
+ Sub<Output = Unit>
+ Div<Output = Unit>
+ Mul<f32, Output = Unit>
+ TryFrom<i32>
+ Ord
+ FloatConversion<Float = f32>
+ Copy,
Unit::Error: Debug,
{
let path = Path::round_rect(rect, corner_radius);
path.fill(color)
}

/// Returns a rounded rectangle with the specified corner radii that has its
/// outline stroked with `color` and `options`.
pub fn stroked_round_rect(
rect: Rect<Unit>,
corner_radius: impl Into<CornerRadii<Unit>>,
color: Color,
options: StrokeOptions<Unit>,
) -> Shape<Unit, false>
where
Unit: Add<Output = Unit>
+ Sub<Output = Unit>
+ Div<Output = Unit>
+ Mul<f32, Output = Unit>
+ TryFrom<i32>
+ Ord
+ FloatConversion<Float = f32>
+ Copy,
Unit::Error: Debug,
{
let path = Path::round_rect(rect, corner_radius);
path.stroke(color, options)
}

/// Uploads the shape to the GPU.
#[must_use]
pub fn prepare(&self, graphics: &Graphics<'_>) -> PreparedGraphic<Unit>
Expand Down Expand Up @@ -340,6 +386,70 @@ pub struct Path<Unit, const TEXTURED: bool> {
events: SmallVec<[PathEvent<Unit>; 7]>,
}

impl<Unit> Path<Unit, false> {
pub fn round_rect(
rect: Rect<Unit>,
corner_radius: impl Into<CornerRadii<Unit>>,
) -> Path<Unit, false>
where
Unit: Add<Output = Unit>
+ Sub<Output = Unit>
+ Div<Output = Unit>
+ Mul<f32, Output = Unit>
+ TryFrom<i32>
+ Ord
+ FloatConversion<Float = f32>
+ Copy,
Unit::Error: Debug,
{
const C: f32 = 0.551_915_02; // https://spencermortensen.com/articles/bezier-circle/
let (top_left, bottom_right) = rect.extents();
let top = top_left.y;
let bottom = bottom_right.y;
let left = top_left.x;
let right = bottom_right.x;

let min_dimension = if rect.size.width > rect.size.height {
rect.size.height
} else {
rect.size.width
};
let radii = corner_radius
.into()
.clamped(min_dimension / Unit::try_from(2).expect("two is always convertable"));

let drift = radii.map(|r| r * C);

let start = Point::new(left + radii.top_left, top);
PathBuilder::new(start)
.line_to(Point::new(right - radii.top_right, top))
.cubic_curve_to(
Point::new(right + drift.top_right - radii.top_right, top),
Point::new(right, top + radii.top_right - drift.top_right),
Point::new(right, top + radii.top_right),
)
.line_to(Point::new(right, bottom - radii.bottom_right))
.cubic_curve_to(
Point::new(right, bottom + drift.bottom_right - radii.bottom_right),
Point::new(right + drift.bottom_right - radii.bottom_right, bottom),
Point::new(right - radii.bottom_right, bottom),
)
.line_to(Point::new(left + radii.bottom_left, bottom))
.cubic_curve_to(
Point::new(left + radii.bottom_left - drift.bottom_left, bottom),
Point::new(left, bottom + drift.bottom_left - radii.bottom_left),
Point::new(left, bottom - radii.bottom_left),
)
.line_to(Point::new(left, top + radii.top_left))
.cubic_curve_to(
Point::new(left, top + radii.top_left - drift.top_left),
Point::new(left + radii.top_left - drift.top_left, top),
start,
)
.close()
}
}

#[test]
fn path_size() {
assert_eq!(std::mem::size_of::<PathEvent<i32>>(), 36);
Expand Down Expand Up @@ -703,7 +813,7 @@ where
start_cap: lyon_tessellation::StrokeOptions::DEFAULT_LINE_CAP,
end_cap: lyon_tessellation::StrokeOptions::DEFAULT_LINE_CAP,
miter_limit: lyon_tessellation::StrokeOptions::DEFAULT_MITER_LIMIT,
tolerance: Default::default(),
tolerance: 1.0,
}
}
}
Expand Down Expand Up @@ -848,3 +958,129 @@ where
.with_tolerance(tolerance)
}
}

/// A description of the size to use for each corner radius measurement when
/// rendering a rounded rectangle.
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct CornerRadii<Unit> {
/// The radius of the top left rounded corner.
pub top_left: Unit,
/// The radius of the top right rounded corner.
pub top_right: Unit,
/// The radius of the bottom right rounded corner.
pub bottom_right: Unit,
/// The radius of the bottom left rounded corner.
pub bottom_left: Unit,
}

impl<Unit> CornerRadii<Unit> {
/// Passes each radius definition to `map` and returns a new set of radii
/// with the results.
#[must_use]
pub fn map<UnitB>(self, mut map: impl FnMut(Unit) -> UnitB) -> CornerRadii<UnitB> {
CornerRadii {
top_left: map(self.top_left),
top_right: map(self.top_right),
bottom_right: map(self.bottom_right),
bottom_left: map(self.bottom_left),
}
}
}

impl<Unit> ScreenScale for CornerRadii<Unit>
where
Unit: ScreenScale<Lp = Lp, Px = Px, UPx = UPx>,
{
type Lp = CornerRadii<Lp>;
type Px = CornerRadii<Px>;
type UPx = CornerRadii<UPx>;

fn into_px(self, scale: figures::Fraction) -> Self::Px {
self.map(|size| size.into_px(scale))
}

fn from_px(px: Self::Px, scale: figures::Fraction) -> Self {
Self {
top_left: Unit::from_px(px.top_left, scale),
top_right: Unit::from_px(px.top_right, scale),
bottom_right: Unit::from_px(px.bottom_right, scale),
bottom_left: Unit::from_px(px.bottom_left, scale),
}
}

fn into_upx(self, scale: figures::Fraction) -> Self::UPx {
self.map(|size| size.into_upx(scale))
}

fn from_upx(px: Self::UPx, scale: figures::Fraction) -> Self {
Self {
top_left: Unit::from_upx(px.top_left, scale),
top_right: Unit::from_upx(px.top_right, scale),
bottom_right: Unit::from_upx(px.bottom_right, scale),
bottom_left: Unit::from_upx(px.bottom_left, scale),
}
}

fn into_lp(self, scale: figures::Fraction) -> Self::Lp {
self.map(|size| size.into_lp(scale))
}

fn from_lp(lp: Self::Lp, scale: figures::Fraction) -> Self {
Self {
top_left: Unit::from_lp(lp.top_left, scale),
top_right: Unit::from_lp(lp.top_right, scale),
bottom_right: Unit::from_lp(lp.bottom_right, scale),
bottom_left: Unit::from_lp(lp.bottom_left, scale),
}
}
}

impl<Unit> IsZero for CornerRadii<Unit>
where
Unit: IsZero,
{
fn is_zero(&self) -> bool {
self.top_left.is_zero()
&& self.top_right.is_zero()
&& self.bottom_right.is_zero()
&& self.bottom_left.is_zero()
}
}

impl<Unit> CornerRadii<Unit>
where
Unit: PartialOrd + Copy,
{
fn clamp_size(size: Unit, clamp: Unit) -> Unit {
if size > clamp {
clamp
} else {
size
}
}

/// Returns this set of radii clamped so that no corner radius has a width
/// or height larger than `size`'s.
#[must_use]
pub fn clamped(mut self, size: Unit) -> Self {
self.top_left = Self::clamp_size(self.top_left, size);
self.top_right = Self::clamp_size(self.top_right, size);
self.bottom_right = Self::clamp_size(self.bottom_right, size);
self.bottom_left = Self::clamp_size(self.bottom_left, size);
self
}
}

impl<Unit> From<Unit> for CornerRadii<Unit>
where
Unit: Copy,
{
fn from(radii: Unit) -> Self {
Self {
top_left: radii,
top_right: radii,
bottom_right: radii,
bottom_left: radii,
}
}
}

0 comments on commit ca138e7

Please sign in to comment.