Unify circular corner geometry and shader masks

This commit is contained in:
Samuel Onoja committed 2026-10-01 16:08:54 +01:00
1 parent 0162870cca
commit f110ec80e1
19 files changed
+277 -135

No files matched your search

+80
View File
@@ -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<i32, Physical>,
pub radius: f32,
}
impl RoundedRect {
pub(crate) fn new(rect: ferese_layout::Rect, origin: Point<i32, Logical>, 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<i32, Logical>, 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<i32, Physical>) -> 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<GlesRenderer> 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]
+33 -34
View File
@@ -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<i32, Logical>,
physical: Rectangle<i32, Physical>,
corners: RoundedRect,
scale: f64,
requested_radius: f64,
requested_width: f64,
color: [f32; 4],
gradient: Option<crate::config::BorderGradient>,
@@ -68,7 +69,7 @@ pub(super) fn window_border_element(
programs: &RoundedClipPrograms,
) -> Option<PhysicalShaderElement> {
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<i32, Logical>,
physical: Rectangle<i32, Physical>,
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<i32, Logical>,
physical: Rectangle<i32, Physical>,
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<i32, Logical>, offset_y: f64, bl
)
}
pub(super) fn scaled_effect_value(requested: f64, geometry: Rectangle<i32, Logical>, 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<i32, Logical>,
physical_constrain: Rectangle<i32, Physical>,
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)
+31 -19
View File
@@ -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<i32, Logical>,
pub radius: f32,
pub corners: RoundedRect,
pub index: usize,
pub capture_geometry: Rectangle<i32, Logical>,
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());
+31 -2
View File
@@ -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<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
@@ -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];
+5 -7
View File
@@ -15,12 +15,12 @@ pub(super) fn overview_chrome_element(
) -> Option<PhysicalShaderElement> {
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,
+9 -26
View File
@@ -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,
@@ -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);
@@ -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);
}
@@ -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;
@@ -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
@@ -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)
@@ -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);
+2
View File
@@ -346,6 +346,7 @@ pub struct Ferese {
pub(crate) window_dimming: HashMap<WindowId, DimAnimation>,
pub(crate) window_focus: HashMap<WindowId, DimAnimation>,
pub(crate) window_shadows: HashMap<WindowId, WindowShadowBuffers>,
pub(crate) rounded_clip_warnings: HashSet<ErasedContextId>,
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, RoundedClipPrograms>,
pub(crate) overview_scrims: HashMap<OutputId, OverviewScrim>,
pub(crate) material_programs: HashMap<ErasedContextId, MaterialProgram>,
@@ -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(),
+19
View File
@@ -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<ImportedFamily, String> {
#[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<Candidate, String> {
+19
View File
@@ -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);
}
}
+2
View File
@@ -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};
+8 -3
View File
@@ -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()
}
+1 -1
View File
@@ -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 |
+3 -2
View File
@@ -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(&<cosmic::Theme as Catalog>::default(), status);
style.border.radius = motion::radius(f32::MAX).into();
style.border.radius = editor_radius.into();
style
})
.height(Length::Fill)