diff --git a/core/src/border.rs b/core/src/border.rs index 232211db73..46217c9502 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -12,6 +12,60 @@ pub struct Border { /// The [`Radius`] of the border. pub radius: Radius, + + /// Overrides for the top edge of the border. + pub top: Side, + + /// Overrides for the right edge of the border. + pub right: Side, + + /// Overrides for the bottom edge of the border. + pub bottom: Side, + + /// Overrides for the left edge of the border. + pub left: Side, +} + +/// Optional overrides for one side of a [`Border`]. +/// +/// A [`Side`] inherits the border's [`Border::color`] and [`Border::width`] +/// when its corresponding value is `None`. +/// +/// This is useful for patterns such as a collapsible header, where the open +/// state can keep only a bottom divider: +/// +/// ``` +/// # use iced_core::{border, Color}; +/// let header = border::color(Color::BLACK) +/// .width(1) +/// .bottom(border::Side::default().color(Color::from_rgb(0.2, 0.4, 1.0))); +/// let collapsed = header.bottom(border::Side::default().width(0)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Side { + /// An optional color override for this side. + pub color: Option, + + /// An optional width override for this side. + pub width: Option, +} + +impl Side { + /// Sets the color override for this side. + pub fn color(self, color: impl Into) -> Self { + Self { + color: Some(color.into()), + ..self + } + } + + /// Sets the width override for this side. + pub fn width(self, width: impl Into) -> Self { + Self { + width: Some(width.into().0), + ..self + } + } } /// Creates a new [`Border`] with the given [`Radius`]. @@ -73,6 +127,26 @@ impl Border { ..self } } + + /// Sets the overrides for the top edge of the [`Border`]. + pub fn top(self, top: Side) -> Self { + Self { top, ..self } + } + + /// Sets the overrides for the right edge of the [`Border`]. + pub fn right(self, right: Side) -> Self { + Self { right, ..self } + } + + /// Sets the overrides for the bottom edge of the [`Border`]. + pub fn bottom(self, bottom: Side) -> Self { + Self { bottom, ..self } + } + + /// Sets the overrides for the left edge of the [`Border`]. + pub fn left(self, left: Side) -> Self { + Self { left, ..self } + } } /// The border radii for the corners of a graphics primitive in the order: @@ -276,3 +350,55 @@ impl std::ops::Mul for Radius { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sides_fall_back_to_the_uniform_values() { + let border = Border::default().color(Color::BLACK).width(2); + + assert_eq!(border.top.color.unwrap_or(border.color), Color::BLACK); + assert_eq!(border.right.width.unwrap_or(border.width), 2.0); + } + + #[test] + fn sides_override_color_and_width_independently() { + let border = Border::default() + .color(Color::BLACK) + .width(2) + .left(Side::default().color(Color::WHITE)) + .right(Side::default().width(4)); + + assert_eq!(border.left.color, Some(Color::WHITE)); + assert_eq!(border.left.width, None); + assert_eq!(border.right.color, None); + assert_eq!(border.right.width, Some(4.0)); + } + + #[test] + fn uniform_builders_do_not_replace_side_overrides() { + let side = Side::default().color(Color::WHITE).width(4); + let before = Border::default().left(side).color(Color::BLACK).width(2); + let after = Border::default().color(Color::BLACK).width(2).left(side); + + assert_eq!(before, after); + } + + #[test] + fn replacing_a_side_with_default_clears_its_overrides() { + let border = Border::default() + .top(Side::default().color(Color::WHITE).width(3)) + .top(Side::default()); + + assert_eq!(border.top, Side::default()); + } + + #[test] + fn zero_width_is_a_valid_side_override() { + let border = Border::default().width(2).bottom(Side::default().width(0)); + + assert_eq!(border.bottom.width.unwrap_or(border.width), 0.0); + } +} diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index f9c9c4b6d2..3f602dafbe 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -131,7 +131,7 @@ mod quad { use iced::advanced::widget::{self, Widget}; use iced::border; use iced::mouse; - use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size}; + use iced::{Color, Element, Length, Rectangle, Shadow, Size}; pub struct CustomQuad { size: f32, @@ -192,11 +192,27 @@ mod quad { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border: Border { - radius: self.radius, - width: self.border_width, - color: Color::from_rgb(1.0, 0.0, 0.0), - }, + // Side overrides demonstrate a collapsible-header-like + // divider: zeroing a side width cleanly removes it. + border: border::color(Color::from_rgb(1.0, 0.0, 0.0)) + .rounded(self.radius) + .width(self.border_width) + .top(border::Side::default().color(Color::from_rgb(1.0, 0.2, 0.2))) + .right( + border::Side::default() + .color(Color::from_rgb(0.2, 1.0, 0.2)) + .width(self.border_width * 0.5), + ) + .bottom( + border::Side::default() + .color(Color::from_rgb(0.2, 0.4, 1.0)) + .width(0), + ) + .left( + border::Side::default() + .color(Color::from_rgb(1.0, 0.8, 0.2)) + .width(self.border_width * 1.5), + ), shadow: self.shadow, snap: self.snap, }, diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index bb8dcad36e..b91159f779 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -42,12 +42,65 @@ impl Engine { let transform = into_transform(transformation); - // Make sure the border radius is not larger than the bounds - let border_width = quad - .border - .width - .min(quad.bounds.width / 2.0) - .min(quad.bounds.height / 2.0); + let sides = [ + quad.border.top, + quad.border.right, + quad.border.bottom, + quad.border.left, + ]; + let has_border_overrides = sides + .iter() + .any(|side| side.color.is_some() || side.width.is_some()); + + let (border_colors, border_widths, uniform_border) = if has_border_overrides { + let border_colors = sides.map(|side| side.color.unwrap_or(quad.border.color)); + let resolved_widths = + sides.map(|side| side.width.unwrap_or(quad.border.width).max(0.0)); + let border_widths = if resolved_widths + .iter() + .all(|width| *width == resolved_widths[0]) + { + let width = resolved_widths[0] + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + [width; 4] + } else { + let mut widths = resolved_widths; + let horizontal = widths[1] + widths[3]; + if horizontal > quad.bounds.width { + let factor = quad.bounds.width / horizontal; + widths[1] *= factor; + widths[3] *= factor; + } + + let vertical = widths[0] + widths[2]; + if vertical > quad.bounds.height { + let factor = quad.bounds.height / vertical; + widths[0] *= factor; + widths[2] *= factor; + } + + widths + }; + + let uniform = border_colors.iter().all(|color| *color == border_colors[0]) + && border_widths.iter().all(|width| *width == border_widths[0]); + + (border_colors, border_widths, uniform) + } else { + // Keep the common case on the original uniform rendering path. + let border_width = quad + .border + .width + .max(0.0) + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + ([quad.border.color; 4], [border_width; 4], true) + }; + let border_width = border_widths[0]; + let border_color = border_colors[0]; let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); @@ -181,7 +234,7 @@ impl Engine { clip_mask, ); - if border_width > 0.0 { + if uniform_border && border_width > 0.0 { // Border path is offset by half the border width let border_bounds = Rectangle { x: quad.bounds.x + border_width / 2.0, @@ -215,7 +268,7 @@ impl Engine { pixels.stroke_path( &border_path, &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(quad.border.color)), + shader: tiny_skia::Shader::SolidColor(into_color(border_color)), anti_alias: true, ..tiny_skia::Paint::default() }, @@ -258,7 +311,7 @@ impl Engine { temp_pixmap.stroke_path( &border_radius_path, &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(quad.border.color)), + shader: tiny_skia::Shader::SolidColor(into_color(border_color)), anti_alias: true, ..tiny_skia::Paint::default() }, @@ -280,6 +333,19 @@ impl Engine { ); } } + + if !uniform_border && border_widths.into_iter().any(|width| width > 0.0) { + draw_asymmetric_border( + quad.bounds, + physical_bounds, + fill_border_radius, + border_widths, + border_colors, + transform, + pixels, + clip_mask, + ); + } } pub fn draw_text( @@ -621,6 +687,210 @@ fn into_transform(transformation: Transformation) -> tiny_skia::Transform { } } +fn draw_asymmetric_border( + bounds: Rectangle, + physical_bounds: Rectangle, + outer_radius: [f32; 4], + widths: [f32; 4], + colors: [Color; 4], + transform: tiny_skia::Transform, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, +) { + let first_side = widths + .iter() + .position(|width| *width > 0.0) + .expect("Draw a non-empty asymmetric border"); + + if outer_radius.iter().all(|radius| *radius == 0.0) { + for side in 0..4 { + if widths[side] == 0.0 { + continue; + } + + pixels.fill_path( + &border_side_path(bounds, widths, side), + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color(colors[side])), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::Winding, + transform, + clip_mask, + ); + } + + return; + } + + if widths + .iter() + .enumerate() + .all(|(side, width)| *width == 0.0 || colors[side] == colors[first_side]) + { + pixels.fill_path( + &asymmetric_border_path(bounds, outer_radius, widths), + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color(colors[first_side])), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + return; + } + + // Render only the transformed quad bounds. Allocating masks at the size of + // the entire destination surface made every asymmetric border temporarily + // consume a full render target. + let x = (physical_bounds.x.floor() - 1.0) + .max(0.0) + .min(pixels.width() as f32) as u32; + let y = (physical_bounds.y.floor() - 1.0) + .max(0.0) + .min(pixels.height() as f32) as u32; + let right = (physical_bounds.x + physical_bounds.width).ceil() + 1.0; + let bottom = (physical_bounds.y + physical_bounds.height).ceil() + 1.0; + let right = right.max(0.0).min(pixels.width() as f32) as u32; + let bottom = bottom.max(0.0).min(pixels.height() as f32) as u32; + + if x >= right || y >= bottom { + return; + } + + let width = right - x; + let height = bottom - y; + let transform = tiny_skia::Transform { + tx: transform.tx - x as f32, + ty: transform.ty - y as f32, + ..transform + }; + let mut ring_mask = tiny_skia::Mask::new(width, height).expect("Create border mask"); + ring_mask.fill_path( + &asymmetric_border_path(bounds, outer_radius, widths), + tiny_skia::FillRule::EvenOdd, + true, + transform, + ); + + let mut border = tiny_skia::Pixmap::new(width, height).expect("Create border pixmap"); + for side in 0..4 { + if widths[side] == 0.0 { + continue; + } + + border.fill_path( + &border_side_path(bounds, widths, side), + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color(colors[side])), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::Winding, + transform, + Some(&ring_mask), + ); + } + + pixels.draw_pixmap( + x as i32, + y as i32, + border.as_ref(), + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::identity(), + clip_mask, + ); +} + +fn asymmetric_border_path( + bounds: Rectangle, + outer_radius: [f32; 4], + widths: [f32; 4], +) -> tiny_skia::Path { + let outer_path = rounded_rectangle(bounds, outer_radius); + let [top, right, bottom, left] = widths; + let inner_bounds = Rectangle { + x: bounds.x + left, + y: bounds.y + top, + width: bounds.width - left - right, + height: bounds.height - top - bottom, + }; + + if inner_bounds.width <= 0.0 || inner_bounds.height <= 0.0 { + return outer_path; + } + + let [top_left, top_right, bottom_right, bottom_left] = outer_radius; + let inner_radius = [ + ((top_left - left).max(0.0), (top_left - top).max(0.0)), + ((top_right - right).max(0.0), (top_right - top).max(0.0)), + ( + (bottom_right - right).max(0.0), + (bottom_right - bottom).max(0.0), + ), + ( + (bottom_left - left).max(0.0), + (bottom_left - bottom).max(0.0), + ), + ]; + + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_path(&outer_path); + builder.push_path(&rounded_rectangle_elliptical(inner_bounds, inner_radius)); + builder.finish().expect("Build asymmetric border") +} + +fn border_side_path(bounds: Rectangle, widths: [f32; 4], side: usize) -> tiny_skia::Path { + let [top, right, bottom, left] = widths; + let x = bounds.x; + let y = bounds.y; + let far_x = bounds.x + bounds.width; + let far_y = bounds.y + bounds.height; + let points = match side { + 0 => [ + (x, y), + (far_x, y), + (far_x - right, y + top), + (x + left, y + top), + ], + 1 => [ + (far_x, y), + (far_x, far_y), + (far_x - right, far_y - bottom), + (far_x - right, y + top), + ], + 2 => [ + (far_x, far_y), + (x, far_y), + (x + left, far_y - bottom), + (far_x - right, far_y - bottom), + ], + 3 => [ + (x, far_y), + (x, y), + (x + left, y + top), + (x + left, far_y - bottom), + ], + _ => unreachable!("Border side index"), + }; + + quadrilateral_path(points) +} + +fn quadrilateral_path(points: [(f32, f32); 4]) -> tiny_skia::Path { + let mut builder = tiny_skia::PathBuilder::new(); + builder.move_to(points[0].0, points[0].1); + for (x, y) in points.into_iter().skip(1) { + builder.line_to(x, y); + } + builder.close(); + builder.finish().expect("Build border side") +} + fn rounded_rectangle(bounds: Rectangle, border_radius: [f32; 4]) -> tiny_skia::Path { let [top_left, top_right, bottom_right, bottom_left] = border_radius; @@ -711,6 +981,63 @@ fn rounded_rectangle(bounds: Rectangle, border_radius: [f32; 4]) -> tiny_skia::P builder.finish().expect("Build rounded rectangle path") } +fn rounded_rectangle_elliptical( + bounds: Rectangle, + border_radius: [(f32, f32); 4], +) -> tiny_skia::Path { + let [top_left, top_right, bottom_right, bottom_left] = border_radius; + let clamp = |(x, y): (f32, f32)| (x.min(bounds.width / 2.0), y.min(bounds.height / 2.0)); + let (tlx, tly) = clamp(top_left); + let (trx, try_) = clamp(top_right); + let (brx, bry) = clamp(bottom_right); + let (blx, bly) = clamp(bottom_left); + + let mut builder = tiny_skia::PathBuilder::new(); + builder.move_to(bounds.x + tlx, bounds.y); + builder.line_to(bounds.x + bounds.width - trx, bounds.y); + arc_to_ellipse( + &mut builder, + bounds.x + bounds.width - trx, + bounds.y, + bounds.x + bounds.width, + bounds.y + try_, + trx, + try_, + ); + builder.line_to(bounds.x + bounds.width, bounds.y + bounds.height - bry); + arc_to_ellipse( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bry, + bounds.x + bounds.width - brx, + bounds.y + bounds.height, + brx, + bry, + ); + builder.line_to(bounds.x + blx, bounds.y + bounds.height); + arc_to_ellipse( + &mut builder, + bounds.x + blx, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bly, + blx, + bly, + ); + builder.line_to(bounds.x, bounds.y + tly); + arc_to_ellipse( + &mut builder, + bounds.x, + bounds.y + tly, + bounds.x + tlx, + bounds.y, + tlx, + tly, + ); + builder.close(); + builder.finish().expect("Build asymmetric inner border") +} + fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { if path.last_point() != Some(tiny_skia::Point { x, y }) { path.line_to(x, y); @@ -753,6 +1080,44 @@ fn arc_to( } } +fn arc_to_ellipse( + path: &mut tiny_skia::PathBuilder, + x_from: f32, + y_from: f32, + x_to: f32, + y_to: f32, + radius_x: f32, + radius_y: f32, +) { + if radius_x == 0.0 || radius_y == 0.0 { + path.line_to(x_to, y_to); + return; + } + + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), + to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), + radii: kurbo::Vec2::new(f64::from(radius_x), f64::from(radius_y)), + x_rotation: 0.0, + large_arc: false, + sweep: true, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => arc.to_cubic_beziers(0.1, |p1, p2, p| { + path.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }), + None => path.line_to(x_to, y_to), + } +} + fn smoothstep(a: f32, b: f32, x: f32) -> f32 { let x = ((x - a) / (b - a)).clamp(0.0, 1.0); @@ -788,3 +1153,140 @@ pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { tiny_skia::Transform::default(), ); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::border::Side; + use crate::core::{Border, Shadow}; + + fn draw_border(border: Border) -> tiny_skia::Pixmap { + let mut engine = Engine::new(); + let mut pixmap = tiny_skia::Pixmap::new(32, 32).expect("Create pixmap"); + pixmap.fill(tiny_skia::Color::BLACK); + let mut mask = tiny_skia::Mask::new(32, 32).expect("Create clip mask"); + let quad = Quad { + bounds: Rectangle { + x: 4.0, + y: 4.0, + width: 24.0, + height: 24.0, + }, + border, + shadow: Shadow::default(), + snap: false, + }; + + engine.draw_quad( + &quad, + &Background::Color(Color::WHITE), + Transformation::IDENTITY, + &mut pixmap.as_mut(), + &mut mask, + Rectangle { + x: 0.0, + y: 0.0, + width: 32.0, + height: 32.0, + }, + ); + + pixmap + } + + fn pixel(color: Color) -> Option { + Some(into_color(color).to_color_u8().premultiply()) + } + + #[test] + fn asymmetric_border_uses_each_side_and_allows_a_disabled_edge() { + let pixmap = draw_border( + Border::default() + .rounded(5) + .width(3) + .top(Side::default().color(Color::from_rgb(1.0, 0.0, 0.0))) + .right(Side::default().color(Color::from_rgb(0.0, 1.0, 0.0))) + .bottom(Side::default().width(0)) + .left( + Side::default() + .color(Color::from_rgb(0.0, 0.0, 1.0)) + .width(5), + ), + ); + + assert_ne!(pixmap.pixel(16, 4), pixmap.pixel(27, 16)); + assert_eq!(pixmap.pixel(16, 27), pixmap.pixel(16, 16)); + } + + #[test] + fn resolved_uniform_border_uses_the_resolved_side_color() { + let base = Color::from_rgb(1.0, 0.0, 0.0); + let resolved = Color::from_rgb(0.0, 1.0, 0.0); + let side = Side::default().color(resolved); + let pixmap = draw_border( + Border::default() + .color(base) + .width(3) + .top(side) + .right(side) + .bottom(side) + .left(side), + ); + + assert_eq!(pixmap.pixel(16, 5), pixel(resolved)); + assert_ne!(pixmap.pixel(16, 5), pixel(base)); + } + + #[test] + fn zero_radius_one_sided_border_only_paints_the_enabled_side() { + let color = Color::from_rgb(1.0, 0.0, 0.0); + let pixmap = draw_border( + Border::default() + .color(color) + .bottom(Side::default().width(3)), + ); + + assert_eq!(pixmap.pixel(16, 26), pixel(color)); + assert_eq!(pixmap.pixel(16, 5), pixel(Color::WHITE)); + assert_eq!(pixmap.pixel(5, 16), pixel(Color::WHITE)); + } + + #[test] + fn zero_radius_multicolor_border_skips_zero_width_sides() { + let red = Color::from_rgb(1.0, 0.0, 0.0); + let green = Color::from_rgb(0.0, 1.0, 0.0); + let blue = Color::from_rgb(0.0, 0.0, 1.0); + let pixmap = draw_border( + Border::default() + .width(3) + .top(Side::default().color(red)) + .right(Side::default().color(green)) + .bottom(Side::default().width(0)) + .left(Side::default().color(blue)), + ); + + assert_eq!(pixmap.pixel(16, 5), pixel(red)); + assert_eq!(pixmap.pixel(26, 16), pixel(green)); + assert_eq!(pixmap.pixel(5, 16), pixel(blue)); + assert_eq!(pixmap.pixel(16, 26), pixel(Color::WHITE)); + } + + #[test] + fn rounded_same_color_border_preserves_asymmetric_widths() { + let color = Color::from_rgb(0.0, 0.0, 1.0); + let pixmap = draw_border( + Border::default() + .color(color) + .rounded(6) + .width(2) + .right(Side::default().width(5)) + .bottom(Side::default().width(0)) + .left(Side::default().width(4)), + ); + + assert_eq!(pixmap.pixel(16, 5), pixel(color)); + assert_eq!(pixmap.pixel(25, 16), pixel(color)); + assert_eq!(pixmap.pixel(6, 16), pixel(color)); + assert_eq!(pixmap.pixel(16, 26), pixel(Color::WHITE)); + } +} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index f3b243b6a9..63fabab9e5 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -44,15 +44,121 @@ impl Layer { ) { let bounds = quad.bounds * transformation; + let border_color = color::pack(quad.border.color); + let border_colors = if quad.border.top.color.is_none() + && quad.border.right.color.is_none() + && quad.border.bottom.color.is_none() + && quad.border.left.color.is_none() + { + [border_color; 4] + } else { + let top = quad.border.top.color.unwrap_or(quad.border.color); + let right = quad.border.right.color.unwrap_or(quad.border.color); + let bottom = quad.border.bottom.color.unwrap_or(quad.border.color); + let left = quad.border.left.color.unwrap_or(quad.border.color); + + if top == right && top == bottom && top == left { + let color = if top == quad.border.color { + border_color + } else { + color::pack(top) + }; + + [color; 4] + } else { + [ + quad.border + .top + .color + .map(color::pack) + .unwrap_or(border_color), + quad.border + .right + .color + .map(color::pack) + .unwrap_or(border_color), + quad.border + .bottom + .color + .map(color::pack) + .unwrap_or(border_color), + quad.border + .left + .color + .map(color::pack) + .unwrap_or(border_color), + ] + } + }; + + let border_widths = if quad.border.top.width.is_none() + && quad.border.right.width.is_none() + && quad.border.bottom.width.is_none() + && quad.border.left.width.is_none() + { + let border_width = quad + .border + .width + .max(0.0) + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + [border_width; 4] + } else { + let top = quad.border.top.width.unwrap_or(quad.border.width).max(0.0); + let right = quad + .border + .right + .width + .unwrap_or(quad.border.width) + .max(0.0); + let bottom = quad + .border + .bottom + .width + .unwrap_or(quad.border.width) + .max(0.0); + let left = quad.border.left.width.unwrap_or(quad.border.width).max(0.0); + + if top == right && top == bottom && top == left { + let width = top + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + [width; 4] + } else { + let mut widths = [top, right, bottom, left]; + + // Opposing borders meet at the center of the quad rather than spilling + // past each other. Adjacent borders intentionally remain independent. + let horizontal = widths[1] + widths[3]; + if horizontal > quad.bounds.width { + let factor = quad.bounds.width / horizontal; + widths[1] *= factor; + widths[3] *= factor; + } + + let vertical = widths[0] + widths[2]; + if vertical > quad.bounds.height { + let factor = quad.bounds.height / vertical; + widths[0] *= factor; + widths[2] *= factor; + } + + widths + } + }; + let scale = transformation.scale_factor(); + let quad = Quad { position: [bounds.x, bounds.y], size: [bounds.width, bounds.height], - border_color: color::pack(quad.border.color), - border_radius: (quad.border.radius * transformation.scale_factor()).into(), - border_width: quad.border.width * transformation.scale_factor(), + border_colors, + border_radius: (quad.border.radius * scale).into(), + border_widths: border_widths.map(|width| width * scale), shadow_color: color::pack(quad.shadow.color), - shadow_offset: (quad.shadow.offset * transformation.scale_factor()).into(), - shadow_blur_radius: quad.shadow.blur_radius * transformation.scale_factor(), + shadow_offset: (quad.shadow.offset * scale).into(), + shadow_blur_radius: quad.shadow.blur_radius * scale, snap: quad.snap as u32, }; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 8d4e9beaf1..147944676d 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -24,14 +24,15 @@ pub struct Quad { /// The size of the [`Quad`]. pub size: [f32; 2], - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: color::Packed, + /// The border colors of the [`Quad`] in top, right, bottom, left order, + /// in __linear RGB__. + pub border_colors: [color::Packed; 4], /// The border radii of the [`Quad`]. pub border_radius: [f32; 4], - /// The border width of the [`Quad`]. - pub border_width: f32, + /// The border widths of the [`Quad`] in top, right, bottom, left order. + pub border_widths: [f32; 4], /// The shadow color of the [`Quad`]. pub shadow_color: color::Packed, @@ -46,6 +47,44 @@ pub struct Quad { pub snap: u32, } +/// The compact representation used when every border side is identical. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Uniform { + pub position: [f32; 2], + pub size: [f32; 2], + pub border_color: color::Packed, + pub border_radius: [f32; 4], + pub border_width: f32, + pub shadow_color: color::Packed, + pub shadow_offset: [f32; 2], + pub shadow_blur_radius: f32, + pub snap: u32, +} + +impl Quad { + fn uniform(self) -> Option { + let border_color = self.border_colors[0]; + let border_width = self.border_widths[0]; + + if self.border_colors == [border_color; 4] && self.border_widths == [border_width; 4] { + Some(Uniform { + position: self.position, + size: self.size, + border_color, + border_radius: self.border_radius, + border_width, + shadow_color: self.shadow_color, + shadow_offset: self.shadow_offset, + shadow_blur_radius: self.shadow_blur_radius, + snap: self.snap, + }) + } else { + None + } + } +} + #[derive(Debug, Clone)] pub struct Pipeline { solid: solid::Pipeline, @@ -96,11 +135,22 @@ impl State { if let Some(layer) = self.layers.get(layer) { render_pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); + let mut uniform_solid_offset = 0; let mut solid_offset = 0; let mut gradient_offset = 0; for (kind, count) in &quads.order { match kind { + Kind::UniformSolid => { + pipeline.solid.render_uniform( + render_pass, + &layer.constants, + &layer.solid, + uniform_solid_offset..(uniform_solid_offset + count), + ); + + uniform_solid_offset += count; + } Kind::Solid => { pipeline.solid.render( render_pass, @@ -202,8 +252,9 @@ impl Layer { ) { self.update(encoder, belt, transformation, scale); - if !quads.solids.is_empty() { - self.solid.prepare(device, encoder, belt, &quads.solids); + if !quads.uniform_solids.is_empty() || !quads.solids.is_empty() { + self.solid + .prepare(device, encoder, belt, &quads.uniform_solids, &quads.solids); } if !quads.gradients.is_empty() { @@ -235,6 +286,9 @@ impl Layer { /// A group of [`Quad`]s rendered together. #[derive(Default, Debug)] pub struct Batch { + /// The solid quads with uniform borders. + uniform_solids: Vec, + /// The solid quads of the [`Layer`]. solids: Vec, @@ -251,20 +305,30 @@ type Order = Vec<(Kind, usize)>; impl Batch { /// Returns true if there are no quads of any type in [`Quads`]. pub fn is_empty(&self) -> bool { - self.solids.is_empty() && self.gradients.is_empty() + self.uniform_solids.is_empty() && self.solids.is_empty() && self.gradients.is_empty() } /// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`]. pub fn add(&mut self, quad: Quad, background: &Background) { let kind = match background { - Background::Color(color) => { - self.solids.push(Solid { - color: color::pack(*color), - quad, - }); + Background::Color(color) => match quad.uniform() { + Some(quad) => { + self.uniform_solids.push(solid::Uniform { + color: color::pack(*color), + quad, + }); + + Kind::UniformSolid + } + None => { + self.solids.push(Solid { + color: color::pack(*color), + quad, + }); - Kind::Solid - } + Kind::Solid + } + }, Background::Gradient(gradient) => { self.gradients.push(Gradient { gradient: graphics::gradient::pack( @@ -289,12 +353,14 @@ impl Batch { } pub fn clear(&mut self) { + self.uniform_solids.clear(); self.solids.clear(); self.gradients.clear(); self.order.clear(); } pub fn append(&mut self, batch: &mut Batch) { + self.uniform_solids.append(&mut batch.uniform_solids); self.solids.append(&mut batch.solids); self.gradients.append(&mut batch.gradients); self.order.append(&mut batch.order); @@ -304,6 +370,8 @@ impl Batch { #[derive(Debug, Copy, Clone, PartialEq, Eq)] /// The kind of a quad. enum Kind { + /// A solid quad with a uniform border. + UniformSolid, /// A solid quad Solid, /// A gradient quad diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 58c252ba9a..323e80178b 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -117,14 +117,17 @@ impl Pipeline { 5 => Float32x4, // Position & Scale 6 => Float32x4, - // Border color + // Border colors 7 => Float32x4, - // Border radius 8 => Float32x4, - // Border width - 9 => Float32, + 9 => Float32x4, + 10 => Float32x4, + // Border radius + 11 => Float32x4, + // Border widths + 12 => Float32x4, // Snap - 10 => Uint32, + 13 => Uint32, ), }], compilation_options: wgpu::PipelineCompilationOptions::default(), diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 83d47d4d6b..fce7bf7863 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -1,6 +1,6 @@ use crate::Buffer; use crate::graphics::color; -use crate::quad::{self, Quad}; +use crate::quad::{self, Quad, Uniform as UniformQuad}; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -16,14 +16,31 @@ pub struct Solid { pub quad: Quad, } +/// A solid quad with a uniform border. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Uniform { + /// The background color data of the quad. + pub color: color::Packed, + + /// The compact [`Quad`] data of the [`Uniform`]. + pub quad: UniformQuad, +} + #[derive(Debug)] pub struct Layer { + uniform_instances: Buffer, instances: Buffer, - instance_count: usize, } impl Layer { pub fn new(device: &wgpu::Device) -> Self { + let uniform_instances = Buffer::new( + device, + "iced_wgpu.quad.solid.uniform.buffer", + quad::INITIAL_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); let instances = Buffer::new( device, "iced_wgpu.quad.solid.buffer", @@ -32,8 +49,8 @@ impl Layer { ); Self { + uniform_instances, instances, - instance_count: 0, } } @@ -42,17 +59,28 @@ impl Layer { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, + uniform_instances: &[Uniform], instances: &[Solid], ) { - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(encoder, belt, 0, instances); + if !uniform_instances.is_empty() { + let _ = self + .uniform_instances + .resize(device, uniform_instances.len()); + let _ = self + .uniform_instances + .write(encoder, belt, 0, uniform_instances); + } - self.instance_count = instances.len(); + if !instances.is_empty() { + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(encoder, belt, 0, instances); + } } } #[derive(Debug, Clone)] pub struct Pipeline { + uniform_pipeline: wgpu::RenderPipeline, pipeline: wgpu::RenderPipeline, } @@ -81,14 +109,27 @@ impl Pipeline { ))), }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu.quad.solid.pipeline"), + let uniform_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.solid.uniform.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(concat!( + include_str!("../shader/color.wgsl"), + "\n", + include_str!("../shader/quad.wgsl"), + "\n", + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!("../shader/quad/solid_uniform.wgsl"), + ))), + }); + + let uniform_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.solid.uniform.pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("solid_vs_main"), + module: &uniform_shader, + entry_point: Some("uniform_solid_vs_main"), buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as u64, + array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array!( // Color @@ -115,6 +156,64 @@ impl Pipeline { }], compilation_options: wgpu::PipelineCompilationOptions::default(), }, + fragment: Some(wgpu::FragmentState { + module: &uniform_shader, + entry_point: Some("uniform_solid_fs_main"), + targets: &quad::color_target_state(format), + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.solid.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("solid_vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Color + 0 => Float32x4, + // Position + 1 => Float32x2, + // Size + 2 => Float32x2, + // Border colors + 3 => Float32x4, + 4 => Float32x4, + 5 => Float32x4, + 6 => Float32x4, + // Border radius + 7 => Float32x4, + // Border widths + 8 => Float32x4, + // Shadow color + 9 => Float32x4, + // Shadow offset + 10 => Float32x2, + // Shadow blur radius + 11 => Float32, + // Snap + 12 => Uint32, + ), + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("solid_fs_main"), @@ -136,7 +235,10 @@ impl Pipeline { cache: None, }); - Self { pipeline } + Self { + uniform_pipeline, + pipeline, + } } pub fn render<'a>( @@ -152,4 +254,18 @@ impl Pipeline { render_pass.draw(0..6, range.start as u32..range.end as u32); } + + pub fn render_uniform<'a>( + &'a self, + render_pass: &mut wgpu::RenderPass<'a>, + constants: &'a wgpu::BindGroup, + layer: &'a Layer, + range: Range, + ) { + render_pass.set_pipeline(&self.uniform_pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(0, layer.uniform_instances.slice(..)); + + render_pass.draw(0..6, range.start as u32..range.end as u32); + } } diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index f8258a4ec3..d24cc29afa 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -11,3 +11,138 @@ fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 { var q = abs(p) - size + corner; return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner; } + +// Returns the coverage of a border that is inset independently on every edge. +// Widths are ordered top, right, bottom, left. +fn border_coverage( + position: vec2, + size: vec2, + radius: vec4, + widths: vec4 +) -> f32 { + let inner_size = size - vec2(widths.y + widths.w, widths.x + widths.z); + + if inner_size.x <= 0.0 || inner_size.y <= 0.0 { + return select(0.0, 1.0, any(widths > vec4(0.0))); + } + + // Insetting a circle independently on the horizontal and vertical axes + // produces an ellipse. Keep both radii instead of reducing each corner to + // a circle using the largest adjacent border width. + let inner_radius_x = max( + radius - vec4(widths.w, widths.y, widths.y, widths.w), + vec4(0.0) + ); + let inner_radius_y = max( + radius - vec4(widths.x, widths.x, widths.z, widths.z), + vec4(0.0) + ); + let distance = elliptical_rounded_box_sdf( + position, + vec2(widths.w, widths.x), + size - vec2(widths.y, widths.z), + inner_radius_x, + inner_radius_y + ); + + return clamp(0.5 + distance, 0.0, 1.0); +} + +// Returns an approximate signed distance to a rounded box whose corner radii +// may differ on the horizontal and vertical axes. The contour is exact, which +// is the important property for border coverage; the approximation is only +// used for the one-pixel antialiasing transition. +fn elliptical_rounded_box_sdf( + position: vec2, + inset_min: vec2, + inset_max: vec2, + radius_x: vec4, + radius_y: vec4 +) -> f32 { + if ( + radius_x.x > 0.0 && radius_y.x > 0.0 && + position.x < inset_min.x + radius_x.x && + position.y < inset_min.y + radius_y.x + ) { + return elliptical_corner_sdf( + position, + inset_min + vec2(radius_x.x, radius_y.x), + vec2(radius_x.x, radius_y.x) + ); + } + + if ( + radius_x.y > 0.0 && radius_y.y > 0.0 && + position.x > inset_max.x - radius_x.y && + position.y < inset_min.y + radius_y.y + ) { + return elliptical_corner_sdf( + position, + vec2(inset_max.x - radius_x.y, inset_min.y + radius_y.y), + vec2(radius_x.y, radius_y.y) + ); + } + + if ( + radius_x.z > 0.0 && radius_y.z > 0.0 && + position.x > inset_max.x - radius_x.z && + position.y > inset_max.y - radius_y.z + ) { + return elliptical_corner_sdf( + position, + inset_max - vec2(radius_x.z, radius_y.z), + vec2(radius_x.z, radius_y.z) + ); + } + + if ( + radius_x.w > 0.0 && radius_y.w > 0.0 && + position.x < inset_min.x + radius_x.w && + position.y > inset_max.y - radius_y.w + ) { + return elliptical_corner_sdf( + position, + vec2(inset_min.x + radius_x.w, inset_max.y - radius_y.w), + vec2(radius_x.w, radius_y.w) + ); + } + + return max( + max(inset_min.x - position.x, position.x - inset_max.x), + max(inset_min.y - position.y, position.y - inset_max.y) + ); +} + +fn elliptical_corner_sdf( + position: vec2, + center: vec2, + radius: vec2 +) -> f32 { + return (length((position - center) / radius) - 1.0) * min(radius.x, radius.y); +} + +// The smallest normalized distance selects a CSS-like diagonal join at a +// corner, while zero-width sides are never selected. +fn border_color_at( + position: vec2, + size: vec2, + widths: vec4, + top: vec4, + right: vec4, + bottom: vec4, + left: vec4 +) -> vec4 { + let disabled = 1e20; + let distances = vec4( + select(disabled, position.y / widths.x, widths.x > 0.0), + select(disabled, (size.x - position.x) / widths.y, widths.y > 0.0), + select(disabled, (size.y - position.y) / widths.z, widths.z > 0.0), + select(disabled, position.x / widths.w, widths.w > 0.0) + ); + let minimum = min(min(distances.x, distances.y), min(distances.z, distances.w)); + + if minimum == distances.x { return top; } + if minimum == distances.y { return right; } + if minimum == distances.z { return bottom; } + return left; +} diff --git a/wgpu/src/shader/quad/gradient.wgsl b/wgpu/src/shader/quad/gradient.wgsl index 7c7a6e45f0..23dc12a1ab 100644 --- a/wgpu/src/shader/quad/gradient.wgsl +++ b/wgpu/src/shader/quad/gradient.wgsl @@ -7,10 +7,13 @@ struct GradientVertexInput { @location(4) @interpolate(flat) offsets: vec4, @location(5) direction: vec4, @location(6) position_and_scale: vec4, - @location(7) border_color: vec4, - @location(8) border_radius: vec4, - @location(9) border_width: f32, - @location(10) snap: u32, + @location(7) border_top: vec4, + @location(8) border_right: vec4, + @location(9) border_bottom: vec4, + @location(10) border_left: vec4, + @location(11) border_radius: vec4, + @location(12) border_widths: vec4, + @location(13) snap: u32, } struct GradientVertexOutput { @@ -22,9 +25,12 @@ struct GradientVertexOutput { @location(5) @interpolate(flat) offsets: vec4, @location(6) direction: vec4, @location(7) position_and_scale: vec4, - @location(8) border_color: vec4, - @location(9) border_radius: vec4, - @location(10) border_width: f32, + @location(8) border_top: vec4, + @location(9) border_right: vec4, + @location(10) border_bottom: vec4, + @location(11) border_left: vec4, + @location(12) border_radius: vec4, + @location(13) border_widths: vec4, } @vertex @@ -65,9 +71,12 @@ fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { out.offsets = input.offsets; out.direction = input.direction * globals.scale; out.position_and_scale = vec4(pos + pos_snap, scale + scale_snap); - out.border_color = premultiply(input.border_color); + out.border_top = premultiply(input.border_top); + out.border_right = premultiply(input.border_right); + out.border_bottom = premultiply(input.border_bottom); + out.border_left = premultiply(input.border_left); out.border_radius = border_radius * globals.scale; - out.border_width = input.border_width * globals.scale; + out.border_widths = input.border_widths * globals.scale; return out; } @@ -172,11 +181,24 @@ fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4 { input.border_radius * 2.0 ) / 2.0; - if (input.border_width > 0.0) { + if (any(input.border_widths > vec4(0.0))) { mixed_color = mix( mixed_color, - input.border_color, - clamp(0.5 + dist + input.border_width, 0.0, 1.0) + border_color_at( + input.position.xy - pos, + scale, + input.border_widths, + input.border_top, + input.border_right, + input.border_bottom, + input.border_left + ), + border_coverage( + input.position.xy - pos, + scale, + input.border_radius, + input.border_widths + ) ); } diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index 2a07e3fa08..8a8af59b29 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -3,26 +3,32 @@ struct SolidVertexInput { @location(0) color: vec4, @location(1) pos: vec2, @location(2) scale: vec2, - @location(3) border_color: vec4, - @location(4) border_radius: vec4, - @location(5) border_width: f32, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f32, - @location(9) snap: u32, + @location(3) border_top: vec4, + @location(4) border_right: vec4, + @location(5) border_bottom: vec4, + @location(6) border_left: vec4, + @location(7) border_radius: vec4, + @location(8) border_widths: vec4, + @location(9) shadow_color: vec4, + @location(10) shadow_offset: vec2, + @location(11) shadow_blur_radius: f32, + @location(12) snap: u32, } struct SolidVertexOutput { @builtin(position) position: vec4, @location(0) color: vec4, - @location(1) border_color: vec4, - @location(2) pos: vec2, - @location(3) scale: vec2, - @location(4) border_radius: vec4, - @location(5) border_width: f32, - @location(6) shadow_color: vec4, - @location(7) shadow_offset: vec2, - @location(8) shadow_blur_radius: f32, + @location(1) border_top: vec4, + @location(2) border_right: vec4, + @location(3) border_bottom: vec4, + @location(4) border_left: vec4, + @location(5) pos: vec2, + @location(6) scale: vec2, + @location(7) border_radius: vec4, + @location(8) border_widths: vec4, + @location(9) shadow_color: vec4, + @location(10) shadow_offset: vec2, + @location(11) shadow_blur_radius: f32, } @vertex @@ -51,11 +57,14 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); out.color = premultiply(input.color); - out.border_color = premultiply(input.border_color); + out.border_top = premultiply(input.border_top); + out.border_right = premultiply(input.border_right); + out.border_bottom = premultiply(input.border_bottom); + out.border_left = premultiply(input.border_left); out.pos = input.pos * globals.scale + pos_snap; out.scale = input.scale * globals.scale + scale_snap; out.border_radius = border_radius * globals.scale; - out.border_width = input.border_width * globals.scale; + out.border_widths = input.border_widths * globals.scale; out.shadow_color = premultiply(input.shadow_color); out.shadow_offset = input.shadow_offset * globals.scale; out.shadow_blur_radius = input.shadow_blur_radius * globals.scale; @@ -75,11 +84,24 @@ fn solid_fs_main( input.border_radius * 2.0 ) / 2.0; - if (input.border_width > 0.0) { + if (any(input.border_widths > vec4(0.0))) { mixed_color = mix( input.color, - input.border_color, - clamp(0.5 + dist + input.border_width, 0.0, 1.0) + border_color_at( + input.position.xy - input.pos, + input.scale, + input.border_widths, + input.border_top, + input.border_right, + input.border_bottom, + input.border_left + ), + border_coverage( + input.position.xy - input.pos, + input.scale, + input.border_radius, + input.border_widths + ) ); } diff --git a/wgpu/src/shader/quad/solid_uniform.wgsl b/wgpu/src/shader/quad/solid_uniform.wgsl new file mode 100644 index 0000000000..f2e797dc13 --- /dev/null +++ b/wgpu/src/shader/quad/solid_uniform.wgsl @@ -0,0 +1,100 @@ +struct UniformSolidVertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) color: vec4, + @location(1) pos: vec2, + @location(2) scale: vec2, + @location(3) border_color: vec4, + @location(4) border_radius: vec4, + @location(5) border_width: f32, + @location(6) shadow_color: vec4, + @location(7) shadow_offset: vec2, + @location(8) shadow_blur_radius: f32, + @location(9) snap: u32, +} + +struct UniformSolidVertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) border_color: vec4, + @location(2) pos: vec2, + @location(3) scale: vec2, + @location(4) border_radius: vec4, + @location(5) border_width: f32, + @location(6) shadow_color: vec4, + @location(7) shadow_offset: vec2, + @location(8) shadow_blur_radius: f32, +} + +@vertex +fn uniform_solid_vs_main(input: UniformSolidVertexInput) -> UniformSolidVertexOutput { + var out: UniformSolidVertexOutput; + + var pos: vec2 = (input.pos + min(input.shadow_offset, vec2(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; + var scale: vec2 = (input.scale + vec2(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; + + var pos_snap = vec2(0.0, 0.0); + var scale_snap = vec2(0.0, 0.0); + + if bool(input.snap) { + pos_snap = round(pos + vec2(0.001, 0.001)) - pos; + scale_snap = round(pos + scale + vec2(0.001, 0.001)) - pos - pos_snap - scale; + } + + let border_radius = min(input.border_radius, vec4(min(input.scale.x, input.scale.y) / 2.0)); + + var transform: mat4x4 = mat4x4( + vec4(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(pos + pos_snap - vec2(0.5, 0.5), 0.0, 1.0) + ); + + out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); + out.color = premultiply(input.color); + out.border_color = premultiply(input.border_color); + out.pos = input.pos * globals.scale + pos_snap; + out.scale = input.scale * globals.scale + scale_snap; + out.border_radius = border_radius * globals.scale; + out.border_width = input.border_width * globals.scale; + out.shadow_color = premultiply(input.shadow_color); + out.shadow_offset = input.shadow_offset * globals.scale; + out.shadow_blur_radius = input.shadow_blur_radius * globals.scale; + + return out; +} + +@fragment +fn uniform_solid_fs_main(input: UniformSolidVertexOutput) -> @location(0) vec4 { + var mixed_color: vec4 = input.color; + + var dist = rounded_box_sdf( + -(input.position.xy - input.pos - input.scale * 0.5) * 2.0, + input.scale, + input.border_radius * 2.0 + ) / 2.0; + + if (input.border_width > 0.0) { + mixed_color = mix( + input.color, + input.border_color, + clamp(0.5 + dist + input.border_width, 0.0, 1.0) + ); + } + + var quad_alpha: f32 = clamp(0.5-dist, 0.0, 1.0); + + let quad_color = mixed_color * quad_alpha; + + if input.shadow_color.a > 0.0 { + var shadow_dist: f32 = rounded_box_sdf( + -(input.position.xy - input.pos - input.shadow_offset - input.scale/2.0) * 2.0, + input.scale, + input.border_radius * 2.0 + ) / 2.0; + let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, max(shadow_dist, 0.0)); + + return mix(quad_color, input.shadow_color, (1.0 - quad_alpha) * shadow_alpha); + } else { + return quad_color; + } +} diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index d31437a3ec..84546e3b8b 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -695,6 +695,7 @@ fn styled( radius: 2.0.into(), width: 1.0, color: border, + ..Border::default() }, text_color: None, } diff --git a/widget/src/container.rs b/widget/src/container.rs index 4862dbaab5..7b43ab7061 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -556,6 +556,7 @@ pub fn bordered_box(theme: &Theme) -> Style { width: 1.0, radius: 5.0.into(), color: palette.background.weak.color, + ..Border::default() }, ..Style::default() } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 7530df7cf2..a97df68605 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -632,6 +632,7 @@ pub fn default(theme: &Theme) -> Style { width: 1.0, radius: 0.0.into(), color: palette.background.strong.color, + ..Border::default() }, text_color: palette.background.weak.text, selected_text_color: palette.primary.strong.text, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index cb4554f76e..a6a3faad6a 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -1285,6 +1285,7 @@ pub fn default(theme: &Theme) -> Style { width: 2.0, color: palette.primary.strong.color, radius: 0.0.into(), + ..Border::default() }, }, hovered_split: Line { diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 378aefcb42..570e4ad468 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -914,6 +914,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, + ..Border::default() }, }; diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 1433f90e4d..d84ddab452 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -407,6 +407,7 @@ where radius: (size / 2.0).into(), width: style.border_width, color: style.border_color, + ..Border::default() }, ..renderer::Quad::default() }, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index cf5f77e091..67954604fd 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -492,6 +492,7 @@ where radius: handle_border_radius, width: style.handle.border_width, color: style.handle.border_color, + ..Border::default() }, ..renderer::Quad::default() }, @@ -665,6 +666,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { radius: 2.0.into(), width: 0.0, color: Color::TRANSPARENT, + ..Border::default() }, }, handle: Handle { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 31e96ba155..323b4ce898 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1369,6 +1369,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, + ..Border::default() }, placeholder: palette.secondary.base.color, value: palette.background.base.text, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 942a1ccf13..2097a52700 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1665,6 +1665,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, + ..Border::default() }, icon: palette.background.weak.text, placeholder: palette.secondary.base.color, diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index e9abfb9f64..50fb9e6757 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -430,6 +430,7 @@ where radius: border_radius, width: style.background_border_width, color: style.background_border_color, + ..Border::default() }, ..renderer::Quad::default() }, @@ -466,6 +467,7 @@ where radius: border_radius, width: style.foreground_border_width, color: style.foreground_border_color, + ..Border::default() }, ..renderer::Quad::default() }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 06500ad277..e98c446f7c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -492,6 +492,7 @@ where radius: handle_border_radius, width: style.handle.border_width, color: style.handle.border_color, + ..Border::default() }, ..renderer::Quad::default() },