From 567b7d9e9f7bd90a99264983c85e2cf40d320138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 30 May 2025 00:30:23 +0200 Subject: [PATCH] Add `crisp` feature for enabling default quad snapping --- Cargo.toml | 2 + core/Cargo.toml | 1 + core/src/renderer.rs | 4 + examples/custom_quad/src/main.rs | 256 +++++++++++++++-------------- wgpu/src/layer.rs | 1 + wgpu/src/quad.rs | 3 + wgpu/src/quad/gradient.rs | 4 +- wgpu/src/quad/solid.rs | 2 + wgpu/src/shader/quad/gradient.wgsl | 17 +- wgpu/src/shader/quad/solid.wgsl | 22 +-- widget/Cargo.toml | 1 + widget/src/button.rs | 4 + widget/src/container.rs | 3 + widget/src/float.rs | 2 + 14 files changed, 184 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4973715c..3c19290b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,8 @@ smol = ["iced_futures/smol"] system = ["iced_winit/system"] # Enables broken "sRGB linear" blending to reproduce color management of the Web web-colors = ["iced_renderer/web-colors"] +# Enables pixel snapping for crisp edges by default (can cause jitter!) +crisp = ["iced_core/crisp", "iced_widget/crisp"] # Enables the WebGL backend webgl = ["iced_renderer/webgl"] # Enables syntax highligthing diff --git a/core/Cargo.toml b/core/Cargo.toml index fb8f778e..f57aaa4d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [features] auto-detect-theme = ["dep:dark-light"] advanced = [] +crisp = [] [dependencies] bitflags.workspace = true diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 199ca09b..53f59303 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -75,6 +75,9 @@ pub struct Quad { /// The [`Shadow`] of the [`Quad`]. pub shadow: Shadow, + + /// Whether the [`Quad`] should be snapped to the pixel grid. + pub snap: bool, } impl Default for Quad { @@ -83,6 +86,7 @@ impl Default for Quad { bounds: Rectangle::with_size(Size::ZERO), border: Border::default(), shadow: Shadow::default(), + snap: cfg!(feature = "crisp"), } } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index f0d9c28c..b23b8e96 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -1,4 +1,134 @@ //! This example showcases a drawing a quad. +use iced::border; +use iced::widget::{center, column, slider, text, toggler}; +use iced::{Center, Color, Element, Shadow, Vector}; + +pub fn main() -> iced::Result { + iced::run(Example::update, Example::view) +} + +struct Example { + radius: border::Radius, + border_width: f32, + shadow: Shadow, + snap: bool, +} + +#[derive(Debug, Clone, Copy)] +#[allow(clippy::enum_variant_names)] +enum Message { + RadiusTopLeftChanged(f32), + RadiusTopRightChanged(f32), + RadiusBottomRightChanged(f32), + RadiusBottomLeftChanged(f32), + BorderWidthChanged(f32), + ShadowXOffsetChanged(f32), + ShadowYOffsetChanged(f32), + ShadowBlurRadiusChanged(f32), + SnapToggled(bool), +} + +impl Example { + fn new() -> Self { + Self { + radius: border::radius(50), + border_width: 0.0, + shadow: Shadow { + color: Color::from_rgba(0.0, 0.0, 0.0, 0.8), + offset: Vector::new(0.0, 8.0), + blur_radius: 16.0, + }, + snap: false, + } + } + + fn update(&mut self, message: Message) { + match message { + Message::RadiusTopLeftChanged(radius) => { + self.radius = self.radius.top_left(radius); + } + Message::RadiusTopRightChanged(radius) => { + self.radius = self.radius.top_right(radius); + } + Message::RadiusBottomRightChanged(radius) => { + self.radius = self.radius.bottom_right(radius); + } + Message::RadiusBottomLeftChanged(radius) => { + self.radius = self.radius.bottom_left(radius); + } + Message::BorderWidthChanged(width) => { + self.border_width = width; + } + Message::ShadowXOffsetChanged(x) => { + self.shadow.offset.x = x; + } + Message::ShadowYOffsetChanged(y) => { + self.shadow.offset.y = y; + } + Message::ShadowBlurRadiusChanged(s) => { + self.shadow.blur_radius = s; + } + Message::SnapToggled(snap) => { + self.snap = snap; + } + } + } + + fn view(&self) -> Element { + let border::Radius { + top_left, + top_right, + bottom_right, + bottom_left, + } = self.radius; + + let Shadow { + offset: Vector { x: sx, y: sy }, + blur_radius: sr, + .. + } = self.shadow; + + let content = column![ + quad::CustomQuad::new( + 200.0, + self.radius, + self.border_width, + self.shadow, + self.snap, + ), + text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"), + slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01), + slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01), + slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged) + .step(0.01), + slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged) + .step(0.01), + slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) + .step(0.01), + text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"), + slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged) + .step(0.01), + slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged) + .step(0.01), + slider(0.0..=100.0, sr, Message::ShadowBlurRadiusChanged) + .step(0.01), + toggler(self.snap).label("Snap to pixel grid").on_toggle(Message::SnapToggled), + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_x(Center); + + center(content).into() + } +} + +impl Default for Example { + fn default() -> Self { + Self::new() + } +} + mod quad { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; @@ -12,6 +142,7 @@ mod quad { radius: border::Radius, border_width: f32, shadow: Shadow, + snap: bool, } impl CustomQuad { @@ -20,12 +151,14 @@ mod quad { radius: border::Radius, border_width: f32, shadow: Shadow, + snap: bool, ) -> Self { Self { size, radius, border_width, shadow, + snap, } } } @@ -69,6 +202,7 @@ mod quad { color: Color::from_rgb(1.0, 0.0, 0.0), }, shadow: self.shadow, + snap: self.snap, }, Color::BLACK, ); @@ -81,125 +215,3 @@ mod quad { } } } - -use iced::border; -use iced::widget::{center, column, slider, text}; -use iced::{Center, Color, Element, Shadow, Vector}; - -pub fn main() -> iced::Result { - iced::run(Example::update, Example::view) -} - -struct Example { - radius: border::Radius, - border_width: f32, - shadow: Shadow, -} - -#[derive(Debug, Clone, Copy)] -#[allow(clippy::enum_variant_names)] -enum Message { - RadiusTopLeftChanged(f32), - RadiusTopRightChanged(f32), - RadiusBottomRightChanged(f32), - RadiusBottomLeftChanged(f32), - BorderWidthChanged(f32), - ShadowXOffsetChanged(f32), - ShadowYOffsetChanged(f32), - ShadowBlurRadiusChanged(f32), -} - -impl Example { - fn new() -> Self { - Self { - radius: border::radius(50), - border_width: 0.0, - shadow: Shadow { - color: Color::from_rgba(0.0, 0.0, 0.0, 0.8), - offset: Vector::new(0.0, 8.0), - blur_radius: 16.0, - }, - } - } - - fn update(&mut self, message: Message) { - match message { - Message::RadiusTopLeftChanged(radius) => { - self.radius = self.radius.top_left(radius); - } - Message::RadiusTopRightChanged(radius) => { - self.radius = self.radius.top_right(radius); - } - Message::RadiusBottomRightChanged(radius) => { - self.radius = self.radius.bottom_right(radius); - } - Message::RadiusBottomLeftChanged(radius) => { - self.radius = self.radius.bottom_left(radius); - } - Message::BorderWidthChanged(width) => { - self.border_width = width; - } - Message::ShadowXOffsetChanged(x) => { - self.shadow.offset.x = x; - } - Message::ShadowYOffsetChanged(y) => { - self.shadow.offset.y = y; - } - Message::ShadowBlurRadiusChanged(s) => { - self.shadow.blur_radius = s; - } - } - } - - fn view(&self) -> Element { - let border::Radius { - top_left, - top_right, - bottom_right, - bottom_left, - } = self.radius; - - let Shadow { - offset: Vector { x: sx, y: sy }, - blur_radius: sr, - .. - } = self.shadow; - - let content = column![ - quad::CustomQuad::new( - 200.0, - self.radius, - self.border_width, - self.shadow - ), - text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"), - slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01), - slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01), - slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged) - .step(0.01), - slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged) - .step(0.01), - slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) - .step(0.01), - text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"), - slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged) - .step(0.01), - slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged) - .step(0.01), - slider(0.0..=100.0, sr, Message::ShadowBlurRadiusChanged) - .step(0.01), - ] - .padding(20) - .spacing(20) - .max_width(500) - .align_x(Center); - - center(content).into() - } -} - -impl Default for Example { - fn default() -> Self { - Self::new() - } -} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 8003d8f6..2d8fcee5 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -54,6 +54,7 @@ impl Layer { shadow_color: color::pack(quad.shadow.color), shadow_offset: quad.shadow.offset.into(), shadow_blur_radius: quad.shadow.blur_radius, + snap: quad.snap as u32, }; self.quads.add(quad, &background); diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index b3ac3f48..bf9d8961 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -41,6 +41,9 @@ pub struct Quad { /// The shadow blur radius of the [`Quad`]. pub shadow_blur_radius: f32, + + /// Whether the [`Quad`] should be snapped to the pixel grid. + pub snap: u32, } #[derive(Debug, Clone)] diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 351e941d..cd99c8ca 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -153,7 +153,9 @@ impl Pipeline { // Border radius 8 => Float32x4, // Border width - 9 => Float32 + 9 => Float32, + // Snap + 10 => Uint32, ), }], compilation_options: diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index fa1db172..7e48358e 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -114,6 +114,8 @@ impl Pipeline { 7 => Float32x2, // Shadow blur radius 8 => Float32, + // Snap + 9 => Uint32, ), }], compilation_options: diff --git a/wgpu/src/shader/quad/gradient.wgsl b/wgpu/src/shader/quad/gradient.wgsl index b80b3f7f..7c7a6e45 100644 --- a/wgpu/src/shader/quad/gradient.wgsl +++ b/wgpu/src/shader/quad/gradient.wgsl @@ -10,6 +10,7 @@ struct GradientVertexInput { @location(7) border_color: vec4, @location(8) border_radius: vec4, @location(9) border_width: f32, + @location(10) snap: u32, } struct GradientVertexOutput { @@ -33,6 +34,14 @@ fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { var pos: vec2 = input.position_and_scale.xy * globals.scale; var scale: vec2 = input.position_and_scale.zw * 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; + } + var min_border_radius = min(input.position_and_scale.z, input.position_and_scale.w) * 0.5; var border_radius: vec4 = vec4( min(input.border_radius.x, min_border_radius), @@ -42,10 +51,10 @@ fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { ); var transform: mat4x4 = mat4x4( - vec4(scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, scale.y + 1.0, 0.0, 0.0), + 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 - vec2(0.5, 0.5), 0.0, 1.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); @@ -55,7 +64,7 @@ fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { out.colors_4 = input.colors_4; out.offsets = input.offsets; out.direction = input.direction * globals.scale; - out.position_and_scale = vec4(pos, scale); + out.position_and_scale = vec4(pos + pos_snap, scale + scale_snap); out.border_color = premultiply(input.border_color); out.border_radius = border_radius * globals.scale; out.border_width = input.border_width * globals.scale; diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index 5578ade3..a2d8433d 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -9,6 +9,7 @@ struct SolidVertexInput { @location(6) shadow_color: vec4, @location(7) shadow_offset: vec2, @location(8) shadow_blur_radius: f32, + @location(9) snap: u32, } struct SolidVertexOutput { @@ -30,14 +31,13 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { 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 snap: vec2 = vec2(0.0, 0.0); - if input.scale.x == 1.0 { - snap.x = round(pos.x) - pos.x; - } + var pos_snap = vec2(0.0, 0.0); + var scale_snap = vec2(0.0, 0.0); - if input.scale.y == 1.0 { - snap.y = round(pos.y) - pos.y; + 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; } var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; @@ -49,17 +49,17 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { ); var transform: mat4x4 = mat4x4( - vec4(scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, scale.y + 1.0, 0.0, 0.0), + 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 - vec2(0.5, 0.5) + snap, 0.0, 1.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 + snap; - out.scale = input.scale * globals.scale; + 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); diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 6d1f054e..024314ff 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -27,6 +27,7 @@ wgpu = ["iced_renderer/wgpu"] markdown = ["dep:pulldown-cmark", "dep:url"] highlighter = ["dep:iced_highlighter"] advanced = [] +crisp = [] [dependencies] iced_renderer.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index d084cd61..85f16f59 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -384,6 +384,7 @@ where bounds, border: style.border, shadow: style.shadow, + snap: style.snap, }, style .background @@ -492,6 +493,8 @@ pub struct Style { pub border: Border, /// The [`Shadow`] of the button. pub shadow: Shadow, + /// Whether the button should be snapped to the pixel grid. + pub snap: bool, } impl Style { @@ -511,6 +514,7 @@ impl Default for Style { text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), + snap: cfg!(feature = "crisp"), } } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 7b9c8bf7..4f6725b1 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -451,6 +451,7 @@ pub fn draw_background( bounds, border: style.border, shadow: style.shadow, + snap: style.snap, }, style .background @@ -592,6 +593,8 @@ pub struct Style { pub border: Border, /// The [`Shadow`] of the container. pub shadow: Shadow, + /// Whether the container should be snapped to the pixel grid. + pub snap: bool, } impl Style { diff --git a/widget/src/float.rs b/widget/src/float.rs index 129ff0b0..b61b4319 100644 --- a/widget/src/float.rs +++ b/widget/src/float.rs @@ -167,6 +167,7 @@ where bounds: layout.bounds().shrink(1.0), shadow: style.shadow, border: border::rounded(style.shadow_border_radius), + snap: false, }, style.shadow.color, ); @@ -332,6 +333,7 @@ where border: border::rounded( style.shadow_border_radius, ), + snap: false, }, style.shadow.color, );