diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index 7ad5dc1..b8642c6 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -96,6 +96,41 @@ pub(crate) fn physical_rect( ) } +/// A single physical outline shared by content and all of its decorations. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct RoundedRect { + pub rect: Rectangle, + pub radius: f32, +} + +impl RoundedRect { + pub(crate) fn new(rect: ferese_layout::Rect, origin: Point, scale: f64, radius: f64) -> Self { + let rect = physical_rect(rect, origin, scale); + Self { + radius: clamp_radius(radius * scale, rect.size), + rect, + } + } + + pub(crate) fn from_logical(rect: Rectangle, scale: f64, radius: f64) -> Self { + Self::new( + ferese_layout::Rect::new( + rect.loc.x as f64, + rect.loc.y as f64, + rect.size.w as f64, + rect.size.h as f64, + ), + (0, 0).into(), + scale, + radius, + ) + } +} + +pub(crate) fn clamp_radius(requested: f64, size: smithay::utils::Size) -> f32 { + requested.min(f64::from(size.w.min(size.h)) / 2.0).max(0.0) as f32 +} + pub(crate) fn handoff_alpha(elapsed: Duration) -> f32 { let progress = (elapsed.as_secs_f64() / HANDOFF.as_secs_f64()).clamp(0.0, 1.0); (1.0 - progress * progress * (3.0 - 2.0 * progress)) as f32 @@ -259,6 +294,51 @@ impl RenderElement for NativeTextureElement { #[cfg(test)] mod tests { + #[test] + fn rounded_outline_clamps_after_physical_snapping() { + // Logical clamping would give 1.125; the snapped 3px rect allows radius 1.5. + let outline = RoundedRect::new(ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0), (0, 0).into(), 1.5, 20.0); + assert_eq!(outline.rect, Rectangle::new((0, 0).into(), (3, 6).into())); + assert_eq!(outline.radius, 1.5); + for scale in [1.0, 1.25, 1.5, 2.0] { + let rect = ferese_layout::Rect::new(-20.4, -10.2, 8.8, 6.4); + let outline = RoundedRect::new(rect, (-10, -5).into(), scale, 100.0); + assert_eq!(outline.rect, physical_rect(rect, (-10, -5).into(), scale)); + assert_eq!( + outline.radius, + outline.rect.size.w.min(outline.rect.size.h) as f32 / 2.0 + ); + } + } + + #[test] + fn physical_radius_preserves_fractions_and_handles_zero() { + let size = (11, 7).into(); + assert_eq!(clamp_radius(100.0, size), 3.5); + assert_eq!(clamp_radius(2.25, size), 2.25); + assert_eq!(clamp_radius(0.0, size), 0.0); + assert_eq!(clamp_radius(-1.0, size), 0.0); + assert_eq!(clamp_radius(2.0, (0, 10).into()), 0.0); + let outline = RoundedRect::new(ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0), (0, 0).into(), 1.5, 1.5); + assert_eq!(outline.radius, 2.25); + } + + #[test] + fn material_and_window_outline_agree_for_the_same_rect() { + let logical = Rectangle::new((-3, 7).into(), (11, 9).into()); + for scale in [1.0, 1.25, 1.5, 2.0] { + assert_eq!( + RoundedRect::from_logical(logical, scale, 100.0), + RoundedRect::new( + ferese_layout::Rect::new(-3.0, 7.0, 11.0, 9.0), + (0, 0).into(), + scale, + 100.0 + ) + ); + } + } + use super::*; #[test] diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index ea4a9fd..d2f4b1b 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -29,9 +29,9 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender UniformName::new("opacity", UniformType::_1f), UniformName::new("shadow_color", UniformType::_4f), ]; - let texture = renderer.compile_custom_texture_shader(ROUNDED_TEXTURE_SHADER, &texture_uniforms); - let border = renderer.compile_custom_pixel_shader(ROUNDED_BORDER_SHADER, &border_uniforms); - let shadow = renderer.compile_custom_pixel_shader(WINDOW_SHADOW_SHADER, &shadow_uniforms); + let texture = renderer.compile_custom_texture_shader(&corner_shader(ROUNDED_TEXTURE_SHADER), &texture_uniforms); + let border = renderer.compile_custom_pixel_shader(&corner_shader(ROUNDED_BORDER_SHADER), &border_uniforms); + let shadow = renderer.compile_custom_pixel_shader(&corner_shader(WINDOW_SHADOW_SHADER), &shadow_uniforms); let compiled = texture.and_then(|texture| border.and_then(|border| shadow.map(|shadow| (texture, border, shadow)))); match compiled { Ok((texture, border, shadow)) => { @@ -44,7 +44,9 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender Some(programs) } Err(error) => { - tracing::error!(%error, "failed to compile rounded-window shader"); + if state.rounded_clip_warnings.insert(context) { + tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering"); + } None } } @@ -56,9 +58,8 @@ pub(super) fn window_border_element( renderer: &GlesRenderer, id: ferese_layout::WindowId, geometry: Rectangle, - physical: Rectangle, + corners: RoundedRect, scale: f64, - requested_radius: f64, requested_width: f64, color: [f32; 4], gradient: Option, @@ -68,7 +69,7 @@ pub(super) fn window_border_element( programs: &RoundedClipPrograms, ) -> Option { let mode = output.current_mode()?; - let width = scaled_effect_value(requested_width, geometry, scale); + let width = clamp_radius(requested_width * scale, corners.rect.size); if width == 0.0 { return None; } @@ -77,7 +78,12 @@ pub(super) fn window_border_element( Some(gradient) => ( gradient.from.0, gradient.to.0, - border_gradient_line(physical, mode.size, output.current_transform().invert(), gradient.angle), + border_gradient_line( + corners.rect, + mode.size, + output.current_transform().invert(), + gradient.angle, + ), ), None => (color, color, [0.0; 4]), }; @@ -85,7 +91,7 @@ pub(super) fn window_border_element( Some(g) => ( g.from.0, g.to.0, - border_gradient_line(physical, mode.size, output.current_transform().invert(), g.angle), + border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle), ), None => ( state.theme_settings.accent_color.0, @@ -95,8 +101,8 @@ pub(super) fn window_border_element( }; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()), - radius: scaled_effect_value(requested_radius, geometry, scale), + clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + radius: corners.radius, width, color: color_with_alpha(from, opacity), color_to: color_with_alpha(to, opacity), @@ -137,7 +143,7 @@ pub(super) fn window_border_element( Some(PhysicalShaderElement { inner: cached.element.clone(), - geometry: physical, + geometry: corners.rect, }) } @@ -162,9 +168,7 @@ pub(super) fn window_tint_element( renderer: &mut GlesRenderer, id: ferese_layout::WindowId, geometry: Rectangle, - physical: Rectangle, - scale: f64, - radius: f64, + corners: RoundedRect, color: [f32; 4], resize_fill: bool, output: &Output, @@ -181,8 +185,8 @@ pub(super) fn window_tint_element( let program = material_program(state, renderer)?; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()), - radius: scaled_effect_value(radius, geometry, scale), + clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + radius: corners.radius, width: 0.0, color, color_to: color, @@ -230,7 +234,7 @@ pub(super) fn window_tint_element( } Some(PhysicalShaderElement { inner: cached.element.clone(), - geometry: physical, + geometry: corners.rect, }) } @@ -240,9 +244,8 @@ pub(super) fn window_shadow_element( renderer: &GlesRenderer, id: ferese_layout::WindowId, geometry: Rectangle, - physical: Rectangle, + corners: RoundedRect, scale: f64, - requested_radius: f64, offset_y: f64, blur: f64, opacity: f64, @@ -256,15 +259,19 @@ pub(super) fn window_shadow_element( } let shadow_geometry = Rectangle::new( - (physical.loc.x, physical.loc.y + (offset_y * scale).round() as i32).into(), - physical.size, + ( + corners.rect.loc.x, + corners.rect.loc.y + (offset_y * scale).round() as i32, + ) + .into(), + corners.rect.size, ); let bounds = shadow_bounds(geometry, offset_y, blur); let parameters = ShadowParameters { blur: (blur * scale) as f32, bounds, shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, output.current_transform().invert()), - radius: scaled_effect_value(requested_radius, geometry, scale), + radius: corners.radius, opacity: opacity as f32, color, }; @@ -327,21 +334,12 @@ pub(super) fn shadow_bounds(geometry: Rectangle, offset_y: f64, bl ) } -pub(super) fn scaled_effect_value(requested: f64, geometry: Rectangle, scale: f64) -> f32 { - requested - .min(f64::from(geometry.size.w.min(geometry.size.h)) / 2.0) - .max(0.0) as f32 - * scale as f32 -} - #[allow(clippy::too_many_arguments)] pub(super) fn rounded_window_elements( renderer: &mut GlesRenderer, window: &smithay::desktop::Window, - constrain: Rectangle, - physical_constrain: Rectangle, + corners: RoundedRect, scale: f64, - requested_radius: f64, alpha: f32, clip_changed: bool, output: &Output, @@ -355,11 +353,12 @@ pub(super) fn rounded_window_elements( return Vec::new(); }; + let physical_constrain = corners.rect; let geometry = window.geometry(); let reference = geometry.to_physical_precise_round(scale); let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale); let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert()); - let radius = scaled_effect_value(requested_radius, constrain, scale); + let radius = corners.radius; let surface = toplevel.wl_surface(); let mut content = PopupManager::popups_for_surface(surface) diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index a7e5b2c..ce388f7 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -33,10 +33,14 @@ pub(super) fn append_material_surface( }) .collect(), }; + let targets: Vec<_> = targets + .into_iter() + .map(|(rect, radius)| (rect, RoundedRect::from_logical(rect, scale, f64::from(radius)))) + .collect(); let materials: Vec<_> = targets .iter() .enumerate() - .filter_map(|(index, (rect, radius))| { + .filter_map(|(index, (rect, corners))| { material_element( state, renderer, @@ -44,7 +48,7 @@ pub(super) fn append_material_surface( surface, MaterialSurface { geometry: *rect, - radius: *radius, + corners: *corners, index, capture_geometry: geometry, alpha: 1.0, @@ -57,16 +61,13 @@ pub(super) fn append_material_surface( } let material = (!materials.is_empty() && regions.is_none()).then_some(()); let clip = material.as_ref().and_then(|_| { + let corners = targets.first()?.1; let mode = output.current_mode()?; let program = rounded_clip_program(state, renderer)?; Some(( program.texture, - framebuffer_clip_rect( - geometry.to_physical_precise_round(scale), - mode.size, - output.current_transform().invert(), - ), - scaled_effect_value(radius, geometry, scale), + framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + corners.radius, )) }); // Smithay element lists are front to back: content, material, shadow. @@ -103,7 +104,7 @@ pub(super) fn append_material_surface( pub(super) struct MaterialSurface { pub geometry: Rectangle, - pub radius: f32, + pub corners: RoundedRect, pub index: usize, pub capture_geometry: Rectangle, pub alpha: f32, @@ -118,7 +119,7 @@ pub(super) fn material_element( ) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> { let MaterialSurface { geometry, - radius, + corners, index, capture_geometry, alpha, @@ -139,7 +140,7 @@ pub(super) fn material_element( state.theme_settings.surface_base_color }; let [red, green, blue, _] = background.0; - let radius = radius.min(geometry.size.w.min(geometry.size.h) as f32 * 0.5); + let radius = corners.radius; let [offset_y, shadow_blur, shadow_opacity] = material.shadow; let edge_bar = role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0; let shadow_opacity = if edge_bar { @@ -148,8 +149,12 @@ pub(super) fn material_element( shadow_opacity * (state.theme_settings.shadow_opacity / 0.2) * f64::from(presentation_alpha) }; let shadow_geometry = Rectangle::new( - (geometry.loc.x, geometry.loc.y + offset_y.round() as i32).into(), - geometry.size, + ( + corners.rect.loc.x, + corners.rect.loc.y + (offset_y * scale).round() as i32, + ) + .into(), + corners.rect.size, ); let background_opacity = material.opacity; let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur); @@ -169,15 +174,15 @@ pub(super) fn material_element( sample_geometry, sample_physical, sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform), - radius: radius * scale as f32, - shadow_rect: framebuffer_clip_rect(shadow_geometry.to_physical_precise_round(scale), mode.size, transform), + radius, + shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, transform), shadow_values: [(shadow_blur * scale) as f32, shadow_opacity as f32], shadow_bounds: shadow_bounds(geometry, offset_y, shadow_blur), geometry, tint: [red, green, blue, material.opacity * presentation_alpha], generation, opaque: material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0, - visible_framebuffer: framebuffer_clip_rect(geometry.to_physical_precise_round(scale), mode.size, transform), + visible_framebuffer: framebuffer_clip_rect(corners.rect, mode.size, transform), }; let context = renderer.context_id().erased(); let program = material_program(state, renderer)?; @@ -281,7 +286,14 @@ pub(super) fn material_element( cached.parameters = parameters; } let background = match &cached.element { - MaterialElement::Fill(element) => element.clone().into(), + // Keep the shader's opaque regions when its canvas already has the + // shared edges. Animated modal geometry may need a physical override. + MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => element.clone().into(), + MaterialElement::Fill(element) => PhysicalShaderElement { + inner: element.clone(), + geometry: corners.rect, + } + .into(), MaterialElement::Blur(element) => element.clone().into(), }; Some((background, cached.shadow.clone().into())) @@ -300,7 +312,7 @@ pub(super) fn blur_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> O if let Some(program) = state.blur_programs.get(&context) { return Some(program.clone()); } - match renderer.compile_custom_texture_shader(BLUR_SHADER, &blur_uniform_names()) { + match renderer.compile_custom_texture_shader(&corner_shader(BLUR_SHADER), &blur_uniform_names()) { Ok(program) => { let program = BlurProgram(program); state.blur_programs.insert(context, program.clone()); @@ -372,7 +384,7 @@ pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) UniformName::new("shadow_rect", UniformType::_4f), UniformName::new("shadow_values", UniformType::_2f), ]; - match renderer.compile_custom_pixel_shader(MATERIAL_SHADER, &uniforms) { + match renderer.compile_custom_pixel_shader(&corner_shader(MATERIAL_SHADER), &uniforms) { Ok(program) => { let program = MaterialProgram(program); state.material_programs.insert(context, program.clone()); diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index 0ef04d0..f76f0f1 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -48,7 +48,7 @@ use smithay::wayland::shell::wlr_layer::Layer; use crate::Ferese; use crate::metrics::FrameEffectMetrics; -use crate::presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect}; +use crate::presentation::{NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, physical_rect}; type SurfaceRenderElement = CropRenderElement>>>; @@ -106,6 +106,10 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag"); const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag"); +fn corner_shader(source: &str) -> String { + source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")) +} + #[derive(Clone, Debug)] pub(crate) struct BlurProgram(GlesTexProgram); @@ -510,6 +514,31 @@ mod tests { scaled_visual_rect, shadow_bounds, }; + #[test] + #[ignore = "requires an EGL rendering device"] + fn shared_corner_shaders_compile() { + use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; + use smithay::backend::renderer::gles::GlesRenderer; + let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let display = unsafe { EGLDisplay::new(device).unwrap() }; + let context = EGLContext::new(&display).unwrap(); + let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; + for source in [super::ROUNDED_TEXTURE_SHADER, super::BLUR_SHADER] { + renderer + .compile_custom_texture_shader(&super::corner_shader(source), &[]) + .unwrap(); + } + for source in [ + super::ROUNDED_BORDER_SHADER, + super::WINDOW_SHADOW_SHADER, + super::MATERIAL_SHADER, + ] { + renderer + .compile_custom_pixel_shader(&super::corner_shader(source), &[]) + .unwrap(); + } + } + #[test] #[ignore = "requires an EGL rendering device"] fn translucent_blur_does_not_leak_the_sharp_backdrop() { @@ -523,7 +552,7 @@ mod tests { let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let program = renderer - .compile_custom_texture_shader(super::BLUR_SHADER, &super::blur_uniform_names()) + .compile_custom_texture_shader(&super::corner_shader(super::BLUR_SHADER), &super::blur_uniform_names()) .unwrap(); let size = (32, 32).into(); let mut pixels = vec![128u8; 32 * 32 * 4]; diff --git a/compositor/src/render/overview.rs b/compositor/src/render/overview.rs index f259571..9628c44 100644 --- a/compositor/src/render/overview.rs +++ b/compositor/src/render/overview.rs @@ -15,12 +15,12 @@ pub(super) fn overview_chrome_element( ) -> Option { let output_id = state.output_id(output)?; let geometry = rounded_visual_rect(rect, output_geometry.loc); - let physical = physical_rect(rect, output_geometry.loc, scale); + let corners = RoundedRect::new(rect, output_geometry.loc, scale, radius); let mode = output.current_mode()?; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()), - radius: (radius * scale) as f32, + clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + radius: corners.radius, width: (width * scale) as f32, color, color_to: color, @@ -79,7 +79,7 @@ pub(super) fn overview_chrome_element( } Some(PhysicalShaderElement { inner: cached.element.clone(), - geometry: physical, + geometry: corners.rect, }) } @@ -223,10 +223,8 @@ pub(super) fn overview_strip_elements( elements.extend(rounded_window_elements( renderer, window, - rounded_visual_rect(*rect, output_geometry.loc), - physical_rect(*rect, output_geometry.loc, scale), + RoundedRect::new(*rect, output_geometry.loc, scale, 4.0), scale, - 4.0, alpha, true, output, diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index 4473156..b4b76f4 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -136,7 +136,6 @@ pub(crate) fn output_elements( for (window, id, visual, decoration_progress, close_alpha) in windows { let constrain = rounded_visual_rect(visual, output_geometry.loc); - let pixels = physical_rect(visual, output_geometry.loc, scale); let material_surface = window .toplevel() .map(|toplevel| toplevel.wl_surface()) @@ -149,6 +148,8 @@ pub(crate) fn output_elements( } else { state.theme_settings.window_radius } * decoration_progress; + let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius); + let pixels = corners.rect; // Only overview/close intentionally scale the complete application. let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0; let behavior = resize_content_behavior(scale_content); @@ -159,9 +160,7 @@ pub(crate) fn output_elements( renderer, id, constrain, - pixels, - scale, - window_radius, + corners, [0.0, 0.0, 0.0, dim as f32 * close_alpha], false, output, @@ -195,9 +194,8 @@ pub(crate) fn output_elements( renderer, id, constrain, - pixels, + corners, scale, - window_radius, border_width, border_color, gradient, @@ -213,9 +211,8 @@ pub(crate) fn output_elements( renderer, id, constrain, - pixels, + corners, scale, - window_radius, shadow_offset_y, shadow_blur, shadow_opacity * f64::from(close_alpha) * decoration_progress, @@ -250,8 +247,7 @@ pub(crate) fn output_elements( program: Some(programs.texture.clone()), uniforms: vec![ Uniform::new("clip_rect", clip).into_owned(), - Uniform::new("radius", scaled_effect_value(window_radius, constrain, scale)) - .into_owned(), + Uniform::new("radius", corners.radius).into_owned(), ], } .into(), @@ -261,10 +257,8 @@ pub(crate) fn output_elements( elements.extend(rounded_window_elements( renderer, &window, - constrain, - pixels, + corners, scale, - window_radius, close_alpha, state .window_geometry @@ -281,18 +275,7 @@ pub(crate) fn output_elements( { let mut color = state.theme_settings.surface_base_color.0; color[3] = close_alpha; - if let Some(fill) = window_tint_element( - state, - renderer, - id, - constrain, - pixels, - scale, - window_radius, - color, - true, - output, - ) { + if let Some(fill) = window_tint_element(state, renderer, id, constrain, corners, color, true, output) { // Front-to-back: fill uncovered strips behind the native // content instead of stretching it or exposing wallpaper. elements.push(fill.into()); @@ -308,7 +291,7 @@ pub(crate) fn output_elements( surface, MaterialSurface { geometry: constrain, - radius: window_radius as f32, + corners, index: 0, capture_geometry: constrain, alpha: close_alpha, diff --git a/compositor/src/render/shaders/blur_shader.frag b/compositor/src/render/shaders/blur_shader.frag index cd45a95..e7c1063 100644 --- a/compositor/src/render/shaders/blur_shader.frag +++ b/compositor/src/render/shaders/blur_shader.frag @@ -19,11 +19,11 @@ uniform float presentation_alpha; uniform float background_opacity; uniform vec4 tint; varying vec2 v_coords; +//_CORNERS_ + void main() { - vec2 half_size = visible_rect.zw * 0.5; - vec2 d = abs(gl_FragCoord.xy - visible_rect.xy - half_size) - (half_size - vec2(material_radius)); - float sdf = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - material_radius; - float coverage = alpha * presentation_alpha * (1.0 - smoothstep(-0.5, 0.5, sdf)); + float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius); + float coverage = alpha * presentation_alpha * edge_coverage(sdf); if (coverage <= 0.0) { gl_FragColor = vec4(0.0); return; } vec2 coords = (gl_FragCoord.xy - capture_origin) / texture_size; vec4 color = vec4(0.0); diff --git a/compositor/src/render/shaders/corners.glsl b/compositor/src/render/shaders/corners.glsl new file mode 100644 index 0000000..c7dd456 --- /dev/null +++ b/compositor/src/render/shaders/corners.glsl @@ -0,0 +1,10 @@ +// Coordinates and radius are physical pixels. Rust clamps the outer radius. +float rounded_rect_distance(vec2 point, vec4 rect, float radius) { + vec2 half_size = rect.zw * 0.5; + vec2 d = abs(point - rect.xy - half_size) - (half_size - vec2(radius)); + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; +} + +float edge_coverage(float distance) { + return 1.0 - smoothstep(-0.5, 0.5, distance); +} diff --git a/compositor/src/render/shaders/material_shader.frag b/compositor/src/render/shaders/material_shader.frag index 27e401e..4e5603e 100644 --- a/compositor/src/render/shaders/material_shader.frag +++ b/compositor/src/render/shaders/material_shader.frag @@ -9,17 +9,13 @@ uniform vec4 shadow_rect; uniform vec2 shadow_values; varying vec2 v_coords; -float rounded_distance(vec2 point, vec4 rect) { - vec2 half_size = rect.zw * 0.5; - float radius = min(material_radius, min(half_size.x, half_size.y)); - vec2 d = abs(point - rect.xy - half_size) - (half_size - vec2(radius)); - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; -} +//_CORNERS_ + void main() { - float sdf = rounded_distance(gl_FragCoord.xy, visible_rect); - float coverage = 1.0 - smoothstep(-0.5, 0.5, sdf); + float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius); + float coverage = edge_coverage(sdf); if (paint_mode > 0.5) { - float distance = max(rounded_distance(gl_FragCoord.xy, shadow_rect), 0.0); + float distance = max(rounded_rect_distance(gl_FragCoord.xy, shadow_rect, material_radius), 0.0); float sigma = max(shadow_values.x * 0.5, 0.5); float opacity = exp(-0.5 * distance * distance / (sigma * sigma)) * shadow_values.y * (1.0 - coverage) * alpha; diff --git a/compositor/src/render/shaders/rounded_border_shader.frag b/compositor/src/render/shaders/rounded_border_shader.frag index 4728db1..34dcd9d 100644 --- a/compositor/src/render/shaders/rounded_border_shader.frag +++ b/compositor/src/render/shaders/rounded_border_shader.frag @@ -18,23 +18,16 @@ varying vec2 v_coords; uniform float tint; #endif -void main() { - vec2 point = gl_FragCoord.xy - clip_rect.xy; - vec2 half_size = clip_rect.zw * 0.5; - vec2 distance = abs(point - half_size) - (half_size - vec2(radius)); - float signed_distance = length(max(distance, 0.0)) - + min(max(distance.x, distance.y), 0.0) - - radius; - float outer_coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance); +//_CORNERS_ - vec2 inner_half_size = max(half_size - vec2(border_width), vec2(0.0)); +void main() { + float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius); + float outer_coverage = edge_coverage(signed_distance); + + vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0)); + vec4 inner_rect = vec4(clip_rect.xy + (clip_rect.zw - inner_size) * 0.5, inner_size); float inner_radius = max(radius - border_width, 0.0); - vec2 inner_distance = abs(point - half_size) - - (inner_half_size - vec2(inner_radius)); - float inner_signed_distance = length(max(inner_distance, 0.0)) - + min(max(inner_distance.x, inner_distance.y), 0.0) - - inner_radius; - float inner_coverage = 1.0 - smoothstep(-0.5, 0.5, inner_signed_distance); + float inner_coverage = edge_coverage(rounded_rect_distance(gl_FragCoord.xy, inner_rect, inner_radius)); float coverage = max(outer_coverage - inner_coverage, 0.0); float progress = clamp(dot(gl_FragCoord.xy - gradient_line.xy, gradient_line.zw), 0.0, 1.0); // Interpolate premultiplied endpoints: a transparent endpoint must not diff --git a/compositor/src/render/shaders/rounded_texture_shader.frag b/compositor/src/render/shaders/rounded_texture_shader.frag index f27c82a..ef93e88 100644 --- a/compositor/src/render/shaders/rounded_texture_shader.frag +++ b/compositor/src/render/shaders/rounded_texture_shader.frag @@ -22,6 +22,8 @@ varying vec2 v_coords; uniform float tint; #endif +//_CORNERS_ + void main() { vec4 color = texture2D(tex, v_coords); @@ -31,13 +33,8 @@ void main() { color = color * alpha; #endif - vec2 point = gl_FragCoord.xy - clip_rect.xy; - vec2 half_size = clip_rect.zw * 0.5; - vec2 distance = abs(point - half_size) - (half_size - vec2(radius)); - float signed_distance = length(max(distance, 0.0)) - + min(max(distance.x, distance.y), 0.0) - - radius; - float coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance); + float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius); + float coverage = edge_coverage(signed_distance); color *= coverage; #if defined(DEBUG_FLAGS) diff --git a/compositor/src/render/shaders/window_shadow_shader.frag b/compositor/src/render/shaders/window_shadow_shader.frag index ac8b45a..b4380da 100644 --- a/compositor/src/render/shaders/window_shadow_shader.frag +++ b/compositor/src/render/shaders/window_shadow_shader.frag @@ -13,13 +13,10 @@ varying vec2 v_coords; uniform float tint; #endif +//_CORNERS_ + void main() { - vec2 point = gl_FragCoord.xy - shadow_rect.xy; - vec2 half_size = shadow_rect.zw * 0.5; - vec2 distance = abs(point - half_size) - (half_size - vec2(radius)); - float signed_distance = length(max(distance, 0.0)) - + min(max(distance.x, distance.y), 0.0) - - radius; + float signed_distance = rounded_rect_distance(gl_FragCoord.xy, shadow_rect, radius); float sigma = max(blur * 0.5, 0.5); float normalized_distance = max(signed_distance, 0.0) / sigma; float coverage = exp(-0.5 * normalized_distance * normalized_distance); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 95a3cc3..c415773 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -346,6 +346,7 @@ pub struct Ferese { pub(crate) window_dimming: HashMap, pub(crate) window_focus: HashMap, pub(crate) window_shadows: HashMap, + pub(crate) rounded_clip_warnings: HashSet, pub(crate) rounded_clip_programs: HashMap, pub(crate) overview_scrims: HashMap, pub(crate) material_programs: HashMap, @@ -581,6 +582,7 @@ impl Ferese { window_focus: HashMap::new(), window_shadows: HashMap::new(), rounded_clip_programs: HashMap::new(), + rounded_clip_warnings: HashSet::new(), overview_scrims: HashMap::new(), material_programs: HashMap::new(), material_buffers: HashMap::new(), diff --git a/crates/ferese-config/src/theme.rs b/crates/ferese-config/src/theme.rs index f3be80c..28dd3c1 100644 --- a/crates/ferese-config/src/theme.rs +++ b/crates/ferese-config/src/theme.rs @@ -604,6 +604,9 @@ pub fn resolve_with_context( *path = theme_path(directory, path); } validate(&tokens)?; + // Effects regions encode integer logical radii; publish the same value + // to the shell and compositor, without rewriting the authored config. + tokens.geometry.shell_radius = tokens.geometry.shell_radius.round(); let requested_accent = tokens.colors.accent.clone(); transform(&mut tokens, &policy.accessibility, &mut warnings); let theme = ResolvedTheme { @@ -1297,6 +1300,22 @@ pub fn import_family(id: &str, source: &str) -> Result { #[cfg(test)] mod tests { + #[test] + fn resolved_shell_radius_matches_integer_effect_regions() { + for (requested, expected) in [(0.0, 0.0), (0.4, 0.0), (13.4, 13.0), (13.5, 14.0)] { + let document = crate::Document::parse(&format!( + "theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}" + )) + .unwrap(); + let result = super::resolve(&document, std::path::Path::new("/tmp"), jiff::Timestamp::now(), |_| { + Err("unexpected theme file".into()) + }) + .unwrap(); + assert_eq!(result.theme.tokens.geometry.shell_radius, expected); + assert_eq!(result.theme.tokens.geometry.window_radius, 7.25); + } + } + use super::*; fn candidate(source: &str) -> Result { diff --git a/crates/ferese-theme/src/geometry.rs b/crates/ferese-theme/src/geometry.rs new file mode 100644 index 0000000..629a2b2 --- /dev/null +++ b/crates/ferese-theme/src/geometry.rs @@ -0,0 +1,19 @@ +/// Radius of a concentric inset, in the same units as the outer radius. +pub fn inner_radius(outer: f32, padding: f32) -> f32 { + (outer - padding).max(0.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inset_corners_preserve_the_center_and_saturate() { + assert_eq!(inner_radius(14.0, 4.0), 10.0); + assert_eq!(inner_radius(7.5, 2.25), 5.25); + assert_eq!(inner_radius(14.0, 0.0), 14.0); + assert_eq!(inner_radius(14.0, 14.0), 0.0); + assert_eq!(inner_radius(14.0, 20.0), 0.0); + assert_eq!(inner_radius(0.0, 4.0), 0.0); + } +} diff --git a/crates/ferese-theme/src/lib.rs b/crates/ferese-theme/src/lib.rs index 56d6dea..09ca02d 100644 --- a/crates/ferese-theme/src/lib.rs +++ b/crates/ferese-theme/src/lib.rs @@ -4,6 +4,7 @@ pub mod calendar; mod contrast; pub mod controls; pub mod gallery; +mod geometry; pub mod icons; pub mod material; pub mod menus; @@ -14,6 +15,7 @@ mod typography; pub use button::accent_button; pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance}; +pub use geometry::inner_radius; pub use palette::{Palette, mix, parse_color, surface_shade}; pub use presets::{PRESETS, Preset}; pub use typography::{font, text}; diff --git a/crates/ferese-theme/src/menus.rs b/crates/ferese-theme/src/menus.rs index 4a62ed9..2fa268e 100644 --- a/crates/ferese-theme/src/menus.rs +++ b/crates/ferese-theme/src/menus.rs @@ -78,20 +78,25 @@ pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> { pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -> Element<'a, M> { let (track, thumb) = crate::controls::switch_colors(palette, enabled, false); + let track_radius = 10.; + let padding = 2.; let knob = container(cosmic::widget::Space::new().width(16).height(16)) .width(16) .height(16) - .class(crate::controls::surface(Color { a: opacity, ..thumb }, 8.)); + .class(crate::controls::surface( + Color { a: opacity, ..thumb }, + crate::inner_radius(track_radius, padding), + )); container(knob) .width(36) .height(20) - .padding(2) + .padding(padding) .align_x(if enabled { cosmic::iced::alignment::Horizontal::Right } else { cosmic::iced::alignment::Horizontal::Left }) - .class(crate::controls::surface(track.scale_alpha(opacity), 10.)) + .class(crate::controls::surface(track.scale_alpha(opacity), track_radius)) .into() } diff --git a/docs/configuration.md b/docs/configuration.md index c2ec7c9..1c53a30 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -183,7 +183,7 @@ Filled controls choose a contrasting text color automatically. | `border-width` | number ≥ 0 | `1` | Window border thickness | | `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness | | `window-radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell | -| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 0 makes them square | +| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; rounded to the nearest logical pixel to match blur regions; 0 makes them square | | `top-bar-height` | number > 0 | `28` | Menu-bar height | | `top-bar-margin-top` | integer ≥ 0 | `0` | Space above bar | | `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar | diff --git a/shell/ferese-shell/src/desktop_widgets.rs b/shell/ferese-shell/src/desktop_widgets.rs index 22779dc..f5aa76b 100644 --- a/shell/ferese-shell/src/desktop_widgets.rs +++ b/shell/ferese-shell/src/desktop_widgets.rs @@ -361,12 +361,13 @@ impl FereseShell { && editor.id == id { let id = id.to_owned(); + let editor_radius = ferese_theme::inner_radius(self.config.theme.material_radius, note.padding); body = body.push( cosmic::widget::TextEditor::new(&editor.content) - .style(|theme, status| { + .style(move |theme, status| { use cosmic::iced::widget::text_editor::Catalog; let mut style = theme.style(&::default(), status); - style.border.radius = motion::radius(f32::MAX).into(); + style.border.radius = editor_radius.into(); style }) .height(Length::Fill)