Add Apple-style window corners with isolated shell rendering
This commit is contained in:
1 parent
b45917c41d
commit
73da6c68d2
10 files changed
+880
-35
No files matched your search
@@ -79,19 +79,42 @@ pub(crate) fn physical_rect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub(crate) enum CornerShape {
|
||||||
|
#[default]
|
||||||
|
Circular,
|
||||||
|
Continuous,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conservative extent of the corner shoulders, in physical pixels.
|
||||||
|
pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size<i32, Physical>, shape: CornerShape) -> f32 {
|
||||||
|
if shape == CornerShape::Circular {
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
(radius * 1.528_665).min(size.w.min(size.h) as f32 * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
/// A single physical outline shared by content and all of its decorations.
|
/// A single physical outline shared by content and all of its decorations.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub(crate) struct RoundedRect {
|
pub(crate) struct RoundedRect {
|
||||||
pub rect: Rectangle<i32, Physical>,
|
pub rect: Rectangle<i32, Physical>,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
|
pub shape: CornerShape,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoundedRect {
|
impl RoundedRect {
|
||||||
|
pub(crate) fn with_shape(mut self, shape: CornerShape) -> Self {
|
||||||
|
self.shape = shape;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new(rect: ferese_layout::Rect, origin: Point<i32, Logical>, scale: f64, radius: f64) -> Self {
|
pub(crate) fn new(rect: ferese_layout::Rect, origin: Point<i32, Logical>, scale: f64, radius: f64) -> Self {
|
||||||
let rect = physical_rect(rect, origin, scale);
|
let rect = physical_rect(rect, origin, scale);
|
||||||
Self {
|
Self {
|
||||||
radius: clamp_radius(radius * scale, rect.size),
|
radius: clamp_radius(radius * scale, rect.size),
|
||||||
rect,
|
rect,
|
||||||
|
shape: CornerShape::Circular,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,552 @@
|
|||||||
|
//! Independent path oracle for the measured UIKit profile and GPU edge coverage.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const EXTENT: f64 = 1.52866498;
|
||||||
|
type Point2 = [f64; 2];
|
||||||
|
type Cubic = [Point2; 4];
|
||||||
|
|
||||||
|
// Fixture: normalized points extracted from UIKit's UIBezierPath output.
|
||||||
|
// https://liamrosenfeld.com/posts/apple_icon_quest/
|
||||||
|
const APPLE: [Cubic; 3] = [
|
||||||
|
[
|
||||||
|
[0.0, EXTENT],
|
||||||
|
[0.0, 1.08849296],
|
||||||
|
[0.0, 0.86840694],
|
||||||
|
[0.07491139, 0.63149379],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[0.07491139, 0.63149379],
|
||||||
|
[0.16905956, 0.37282383],
|
||||||
|
[0.37282383, 0.16905956],
|
||||||
|
[0.63149379, 0.07491139],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[0.63149379, 0.07491139],
|
||||||
|
[0.86840694, 0.0],
|
||||||
|
[1.08849296, 0.0],
|
||||||
|
[EXTENT, 0.0],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const CIRCLE: [Cubic; 3] = [
|
||||||
|
[
|
||||||
|
[0.0, 1.0],
|
||||||
|
[0.0, 0.8686781289],
|
||||||
|
[0.0258657631, 0.7386421566],
|
||||||
|
[0.0761204675, 0.6173165676],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[0.0761204675, 0.6173165676],
|
||||||
|
[0.1776144241, 0.3722884810],
|
||||||
|
[0.3722884810, 0.1776144241],
|
||||||
|
[0.6173165676, 0.0761204675],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[0.6173165676, 0.0761204675],
|
||||||
|
[0.7386421566, 0.0258657631],
|
||||||
|
[0.8686781289, 0.0],
|
||||||
|
[1.0, 0.0],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
fn lerp(a: Point2, b: Point2, t: f64) -> Point2 {
|
||||||
|
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curve(mut points: Cubic, t: f64) -> Point2 {
|
||||||
|
// De Casteljau, independent of the shader's polynomial/Newton calculation.
|
||||||
|
for count in (1..4).rev() {
|
||||||
|
for index in 0..count {
|
||||||
|
points[index] = lerp(points[index], points[index + 1], t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
points[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn squared_distance(a: Point2, b: Point2) -> f64 {
|
||||||
|
(a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearest_distance(points: Cubic, query: Point2) -> f64 {
|
||||||
|
let index = (0usize..=64)
|
||||||
|
.min_by(|a, b| {
|
||||||
|
squared_distance(curve(points, *a as f64 / 64.0), query)
|
||||||
|
.total_cmp(&squared_distance(curve(points, *b as f64 / 64.0), query))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut low = index.saturating_sub(1) as f64 / 64.0;
|
||||||
|
let mut high = (index + 1).min(64) as f64 / 64.0;
|
||||||
|
|
||||||
|
// Bracketed minimization, rather than reimplementing the GPU Newton solver.
|
||||||
|
for _ in 0..36 {
|
||||||
|
let a = low + (high - low) / 3.0;
|
||||||
|
let b = high - (high - low) / 3.0;
|
||||||
|
|
||||||
|
if squared_distance(curve(points, a), query) < squared_distance(curve(points, b), query) {
|
||||||
|
high = b;
|
||||||
|
} else {
|
||||||
|
low = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
squared_distance(curve(points, (low + high) * 0.5), query).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference_distance(point: Point2, rect: [f64; 4], radius: f64, shape: CornerShape) -> f64 {
|
||||||
|
let half = [rect[2] * 0.5, rect[3] * 0.5];
|
||||||
|
let inset = [
|
||||||
|
half[0] - (point[0] - rect[0] - half[0]).abs(),
|
||||||
|
half[1] - (point[1] - rect[1] - half[1]).abs(),
|
||||||
|
];
|
||||||
|
let limit = half[0].min(half[1]);
|
||||||
|
|
||||||
|
if shape == CornerShape::Circular || radius <= 0.0 || radius >= limit {
|
||||||
|
let q = [radius - inset[0], radius - inset[1]];
|
||||||
|
return q[0].max(0.0).hypot(q[1].max(0.0)) + q[0].max(q[1]).min(0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
let blend = ((limit / radius - 1.0) / (EXTENT - 1.0)).clamp(0.0, 1.0);
|
||||||
|
let extent = 1.0 + (EXTENT - 1.0) * blend;
|
||||||
|
|
||||||
|
if inset[0].max(inset[1]) >= extent * radius {
|
||||||
|
return -inset[0].min(inset[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = [inset[0] / radius, inset[1] / radius];
|
||||||
|
let curves = std::array::from_fn::<_, 3, _>(|segment| {
|
||||||
|
std::array::from_fn(|index| lerp(CIRCLE[segment][index], APPLE[segment][index], blend))
|
||||||
|
});
|
||||||
|
let distance = curves
|
||||||
|
.iter()
|
||||||
|
.map(|points| nearest_distance(*points, query))
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
let mut inside = false;
|
||||||
|
|
||||||
|
if query[0] >= 0.0 && query[1] >= 0.0 {
|
||||||
|
for points in curves {
|
||||||
|
if query[0] >= points[0][0] && query[0] <= points[3][0] {
|
||||||
|
let mut low = 0.0;
|
||||||
|
let mut high = 1.0;
|
||||||
|
|
||||||
|
for _ in 0..36 {
|
||||||
|
let middle = (low + high) * 0.5;
|
||||||
|
|
||||||
|
if curve(points, middle)[0] < query[0] {
|
||||||
|
low = middle;
|
||||||
|
} else {
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inside = query[1] >= curve(points, (low + high) * 0.5)[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distance * radius * if inside { -1.0 } else { 1.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coverage(distance: f64) -> f64 {
|
||||||
|
(0.5 - distance).clamp(0.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_roles_and_shader_sources_keep_circular_corners() {
|
||||||
|
use crate::effects::SemanticRole::*;
|
||||||
|
|
||||||
|
for role in [Panel, PanelElevated, Popover, Menu, Notification, Hud, Modal] {
|
||||||
|
assert_eq!(corner_shape_for_role(Some(role)), CornerShape::Circular);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(corner_shape_for_role(None), CornerShape::Continuous);
|
||||||
|
|
||||||
|
for source in [BLUR_SHADER, MATERIAL_SHADER, ROUNDED_TEXTURE_SHADER] {
|
||||||
|
assert_eq!(
|
||||||
|
corner_shader(source),
|
||||||
|
source.replace("//_CORNERS_", include_str!("shaders/corners.glsl"))
|
||||||
|
);
|
||||||
|
assert!(!corner_shader(source).contains("CORNER_EXTENT"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderer() -> GlesRenderer {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "manual EGL timing sample; requires a rendering device"]
|
||||||
|
fn window_corner_shader_timing_sample() {
|
||||||
|
let mut renderer = renderer();
|
||||||
|
let driver = renderer
|
||||||
|
.with_context(|gl| {
|
||||||
|
unsafe { std::ffi::CStr::from_ptr(gl.GetString(0x1F01).cast()) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
let size = (1024, 768).into();
|
||||||
|
let rect = Rectangle::<i32, Physical>::from_size(size);
|
||||||
|
let uniforms = [
|
||||||
|
Uniform::new("clip_rect", [0.0f32, 0.0, 1024.0, 768.0]),
|
||||||
|
Uniform::new("radius", 16.0f32),
|
||||||
|
];
|
||||||
|
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap();
|
||||||
|
|
||||||
|
for shape in [CornerShape::Circular, CornerShape::Continuous] {
|
||||||
|
let programs = corner_program(&mut resources, &mut renderer, shape).unwrap();
|
||||||
|
let mut samples = Vec::new();
|
||||||
|
|
||||||
|
for iteration in 0..110 {
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let mut target = renderer.bind(&mut texture).unwrap();
|
||||||
|
let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap();
|
||||||
|
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
|
||||||
|
frame
|
||||||
|
.render_pixel_shader_to(
|
||||||
|
&programs.solid,
|
||||||
|
Rectangle::from_size((1024.0, 768.0).into()),
|
||||||
|
rect,
|
||||||
|
(1024, 768).into(),
|
||||||
|
Some(&[rect]),
|
||||||
|
1.0,
|
||||||
|
&uniforms,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
frame.finish().unwrap().wait().unwrap();
|
||||||
|
|
||||||
|
if iteration >= 10 {
|
||||||
|
samples.push(started.elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
samples.sort_unstable();
|
||||||
|
eprintln!(
|
||||||
|
"corner shader {shape:?}: driver={driver}, median={:?}, p95={:?}, size=1024x768, radius=16",
|
||||||
|
samples[50], samples[95]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize(renderer: &mut GlesRenderer, program: &GlesPixelProgram, uniforms: &[Uniform<'_>]) -> Vec<u8> {
|
||||||
|
let size = (64, 64).into();
|
||||||
|
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||||
|
let rect = Rectangle::<i32, Physical>::from_size((64, 64).into());
|
||||||
|
let mut target = renderer.bind(&mut texture).unwrap();
|
||||||
|
{
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(&mut target, (64, 64).into(), Transform::Normal)
|
||||||
|
.unwrap();
|
||||||
|
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
|
||||||
|
frame
|
||||||
|
.render_pixel_shader_to(
|
||||||
|
program,
|
||||||
|
Rectangle::from_size(size.to_f64()),
|
||||||
|
rect,
|
||||||
|
size,
|
||||||
|
Some(&[rect]),
|
||||||
|
1.0,
|
||||||
|
uniforms,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let _ = frame.finish().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn apple_window_fill_and_inset_border_match_path_reference_at_fractional_scales() {
|
||||||
|
let mut renderer = renderer();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
|
||||||
|
// Include tiny radii, fractional physical coordinates, overlapping shoulder
|
||||||
|
// adaptation, exact pills, square/fullscreen, and broad inset borders.
|
||||||
|
for (radius, width) in [
|
||||||
|
(0.0f32, 1.0f32),
|
||||||
|
(0.375, 0.75),
|
||||||
|
(8.25, 1.25),
|
||||||
|
(12.375, 2.25),
|
||||||
|
(24.0, 3.0),
|
||||||
|
(29.49, 2.5),
|
||||||
|
(29.5, 2.5),
|
||||||
|
(12.375, 12.0),
|
||||||
|
] {
|
||||||
|
let rect = [1.25f32, 2.5, 61.5, 59.0];
|
||||||
|
let fill = rasterize(
|
||||||
|
&mut renderer,
|
||||||
|
&programs.solid,
|
||||||
|
&[
|
||||||
|
Uniform::new("clip_rect", rect),
|
||||||
|
Uniform::new("radius", radius),
|
||||||
|
Uniform::new("color", [1.0f32; 4]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let border = BorderParameters {
|
||||||
|
geometry: Rectangle::from_size((64, 64).into()),
|
||||||
|
clip_rect: rect,
|
||||||
|
radius,
|
||||||
|
width,
|
||||||
|
color: [1.0; 4],
|
||||||
|
color_to: [1.0; 4],
|
||||||
|
gradient_line: [0.0; 4],
|
||||||
|
focus_color: [1.0; 4],
|
||||||
|
focus_color_to: [1.0; 4],
|
||||||
|
focus_gradient_line: [0.0; 4],
|
||||||
|
focus_mix: 0.0,
|
||||||
|
};
|
||||||
|
let edge = rasterize(&mut renderer, &programs.border, &border_uniforms(&border));
|
||||||
|
|
||||||
|
for y in 0..64 {
|
||||||
|
for x in 0..64 {
|
||||||
|
let distance = reference_distance(
|
||||||
|
[x as f64 + 0.5, y as f64 + 0.5],
|
||||||
|
rect.map(f64::from),
|
||||||
|
radius.into(),
|
||||||
|
CornerShape::Continuous,
|
||||||
|
);
|
||||||
|
let outer = coverage(distance);
|
||||||
|
let inner = coverage(distance + f64::from(width));
|
||||||
|
let index = (y * 64 + x) * 4;
|
||||||
|
|
||||||
|
for (actual, expected) in [(fill[index + 3], outer), (edge[index + 3], (outer - inner).max(0.0))] {
|
||||||
|
let expected = (expected * 255.0).round() as u8;
|
||||||
|
assert!(
|
||||||
|
actual.abs_diff(expected) <= 2,
|
||||||
|
"x={x} y={y} radius={radius} width={width}: GPU={actual}, reference={expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn window_programs_do_not_change_cached_shell_materials_and_blur() {
|
||||||
|
let mut renderer = renderer();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let circular = rounded_clip_program(&mut resources, &mut renderer).unwrap();
|
||||||
|
material_program(&mut resources, &mut renderer).unwrap();
|
||||||
|
blur_program(&mut resources, &mut renderer).unwrap();
|
||||||
|
let uniforms = [
|
||||||
|
Uniform::new("clip_rect", [0.0f32, 0.0, 64.0, 64.0]),
|
||||||
|
Uniform::new("radius", 12.0f32),
|
||||||
|
Uniform::new("color", [1.0f32; 4]),
|
||||||
|
];
|
||||||
|
let before = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||||
|
corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
let circular = rounded_clip_program(&mut resources, &mut renderer).unwrap();
|
||||||
|
let after = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||||
|
assert_eq!(before, after);
|
||||||
|
let cache = &resources.contexts[&context];
|
||||||
|
assert!(cache.material.is_some() && cache.window_material.is_some() && cache.blur.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apple_shoulder_extent_never_claims_transparent_pixels_as_opaque() {
|
||||||
|
let clip = Rectangle::from_size((64, 64).into());
|
||||||
|
|
||||||
|
for radius in [0.375f32, 8.25, 12.375, 24.0, 31.99, 32.0] {
|
||||||
|
let extent = corner_extent(radius, clip.size, CornerShape::Continuous);
|
||||||
|
let opaque = rounded_opaque_regions(&[clip], Point::default(), clip, extent);
|
||||||
|
|
||||||
|
for region in opaque.iter() {
|
||||||
|
for y in region.loc.y..region.loc.y + region.size.h {
|
||||||
|
for x in region.loc.x..region.loc.x + region.size.w {
|
||||||
|
let distance = reference_distance(
|
||||||
|
[x as f64 + 0.5, y as f64 + 0.5],
|
||||||
|
[0.0, 0.0, 64.0, 64.0],
|
||||||
|
radius.into(),
|
||||||
|
CornerShape::Continuous,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
coverage(distance),
|
||||||
|
1.0,
|
||||||
|
"transparent opaque claim at ({x}, {y}), radius={radius}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn apple_shadow_and_window_tint_match_the_same_outline() {
|
||||||
|
let mut renderer = renderer();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
let rect = [1.25f32, 2.5, 61.5, 59.0];
|
||||||
|
let radius = 12.375f32;
|
||||||
|
let shadow = rasterize(
|
||||||
|
&mut renderer,
|
||||||
|
&programs.shadow,
|
||||||
|
&[
|
||||||
|
Uniform::new("shadow_rect", rect),
|
||||||
|
Uniform::new("radius", radius),
|
||||||
|
Uniform::new("blur", 8.0f32),
|
||||||
|
Uniform::new("opacity", 1.0f32),
|
||||||
|
Uniform::new("shadow_color", [1.0f32; 4]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let tint = rasterize(
|
||||||
|
&mut renderer,
|
||||||
|
&material.0,
|
||||||
|
&[
|
||||||
|
Uniform::new("visible_rect", rect),
|
||||||
|
Uniform::new("material_radius", radius),
|
||||||
|
Uniform::new("tint", [1.0f32; 4]),
|
||||||
|
Uniform::new("paint_mode", 0.0f32),
|
||||||
|
Uniform::new("shadow_rect", rect),
|
||||||
|
Uniform::new("shadow_values", [0.0f32; 2]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
for y in 0..64 {
|
||||||
|
for x in 0..64 {
|
||||||
|
let distance = reference_distance(
|
||||||
|
[x as f64 + 0.5, y as f64 + 0.5],
|
||||||
|
rect.map(f64::from),
|
||||||
|
radius.into(),
|
||||||
|
CornerShape::Continuous,
|
||||||
|
);
|
||||||
|
let shadow_alpha = (-0.5 * (distance.max(0.0) / 4.0).powi(2)).exp();
|
||||||
|
let index = (y * 64 + x) * 4;
|
||||||
|
|
||||||
|
for (actual, expected) in [(tint[index + 3], coverage(distance)), (shadow[index + 3], shadow_alpha)] {
|
||||||
|
assert!(
|
||||||
|
actual.abs_diff((expected * 255.0).round() as u8) <= 2,
|
||||||
|
"outline mismatch at ({x}, {y})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot() {
|
||||||
|
let mut renderer = renderer();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
let id = ferese_layout::WindowId(1);
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
assert!(!resources.prepare_window_corners(id, CornerShape::Continuous));
|
||||||
|
let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
|
let parameters = BorderParameters {
|
||||||
|
geometry: Rectangle::from_size((64, 64).into()),
|
||||||
|
clip_rect: [0.0, 0.0, 64.0, 64.0],
|
||||||
|
radius: 12.0,
|
||||||
|
width: 2.0,
|
||||||
|
color: [1.0; 4],
|
||||||
|
color_to: [1.0; 4],
|
||||||
|
gradient_line: [0.0; 4],
|
||||||
|
focus_color: [1.0; 4],
|
||||||
|
focus_color_to: [1.0; 4],
|
||||||
|
focus_gradient_line: [0.0; 4],
|
||||||
|
focus_mix: 0.0,
|
||||||
|
};
|
||||||
|
let element = SharedPixelShaderElement::new(
|
||||||
|
programs.border,
|
||||||
|
parameters.geometry,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
border_uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
);
|
||||||
|
resources
|
||||||
|
.windows
|
||||||
|
.get_mut(&id)
|
||||||
|
.unwrap()
|
||||||
|
.borders
|
||||||
|
.contexts
|
||||||
|
.insert(context.clone(), CachedBorder { element, parameters });
|
||||||
|
let texture = renderer.create_buffer(Fourcc::Abgr8888, (64, 64).into()).unwrap();
|
||||||
|
resources.set_snapshot(
|
||||||
|
id,
|
||||||
|
ResizeSnapshot {
|
||||||
|
texture,
|
||||||
|
context: context.clone(),
|
||||||
|
id: Id::new(),
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
elapsed: Duration::ZERO,
|
||||||
|
last_tick: None,
|
||||||
|
scale: 1.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let snapshot_id = resources.snapshot(&id).unwrap().id.clone();
|
||||||
|
let commit = resources.snapshot(&id).unwrap().commit;
|
||||||
|
assert!(!resources.prepare_window_corners(id, CornerShape::Continuous));
|
||||||
|
assert_eq!(resources.windows[&id].borders.contexts.len(), 1);
|
||||||
|
assert!(resources.prepare_window_corners(id, CornerShape::Circular));
|
||||||
|
assert!(resources.windows[&id].borders.contexts.is_empty());
|
||||||
|
let snapshot = resources.snapshot(&id).unwrap();
|
||||||
|
assert_eq!(snapshot.id, snapshot_id);
|
||||||
|
assert_ne!(snapshot.commit, commit);
|
||||||
|
let updated_commit = snapshot.commit;
|
||||||
|
assert!(!resources.prepare_window_corners(id, CornerShape::Circular));
|
||||||
|
assert_eq!(resources.snapshot(&id).unwrap().commit, updated_commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) {
|
||||||
|
if end {
|
||||||
|
(
|
||||||
|
[3.0 * (points[3][0] - points[2][0]), 3.0 * (points[3][1] - points[2][1])],
|
||||||
|
[
|
||||||
|
6.0 * (points[3][0] - 2.0 * points[2][0] + points[1][0]),
|
||||||
|
6.0 * (points[3][1] - 2.0 * points[2][1] + points[1][1]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
[3.0 * (points[1][0] - points[0][0]), 3.0 * (points[1][1] - points[0][1])],
|
||||||
|
[
|
||||||
|
6.0 * (points[2][0] - 2.0 * points[1][0] + points[0][0]),
|
||||||
|
6.0 * (points[2][1] - 2.0 * points[1][1] + points[0][1]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curvature(tangent: Point2, acceleration: Point2) -> f64 {
|
||||||
|
(tangent[0] * acceleration[1] - tangent[1] * acceleration[0]).abs() / tangent[0].hypot(tangent[1]).powi(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn measured_uikit_profile_has_flat_shoulders_but_small_internal_join_discontinuities() {
|
||||||
|
let (start, start_acceleration) = endpoint_derivatives(APPLE[0], false);
|
||||||
|
let (end, end_acceleration) = endpoint_derivatives(APPLE[2], true);
|
||||||
|
assert_eq!(start[0], 0.0);
|
||||||
|
assert_eq!(end[1], 0.0);
|
||||||
|
assert_eq!(curvature(start, start_acceleration), 0.0);
|
||||||
|
assert_eq!(curvature(end, end_acceleration), 0.0);
|
||||||
|
|
||||||
|
for pair in APPLE.windows(2) {
|
||||||
|
assert_eq!(pair[0][3], pair[1][0], "join must be position-continuous");
|
||||||
|
let (before, before_acceleration) = endpoint_derivatives(pair[0], true);
|
||||||
|
let (after, after_acceleration) = endpoint_derivatives(pair[1], false);
|
||||||
|
let cosine =
|
||||||
|
(before[0] * after[0] + before[1] * after[1]) / before[0].hypot(before[1]) / after[0].hypot(after[1]);
|
||||||
|
let angle = cosine.clamp(-1.0, 1.0).acos().to_degrees();
|
||||||
|
let curvature_jump = (curvature(before, before_acceleration) - curvature(after, after_acceleration)).abs();
|
||||||
|
// This is a measured Apple-style approximation, not a globally G1/G2
|
||||||
|
// curve. Preserve and document its limitation instead of falsely
|
||||||
|
// passing a continuity test, or silently changing the source profile.
|
||||||
|
assert!((angle - 2.453_166_93).abs() < 1.0e-6);
|
||||||
|
assert!((curvature_jump - 0.354_989_57).abs() < 1.0e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,20 @@ use super::*;
|
|||||||
pub(super) fn rounded_clip_program(
|
pub(super) fn rounded_clip_program(
|
||||||
resources: &mut RenderResources,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<RoundedClipPrograms> {
|
||||||
|
corner_program(resources, renderer, CornerShape::Circular)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn corner_program(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
shape: CornerShape,
|
||||||
) -> Option<RoundedClipPrograms> {
|
) -> Option<RoundedClipPrograms> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = resources
|
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape {
|
||||||
.contexts
|
CornerShape::Circular => programs.rounded.as_ref(),
|
||||||
.get(&context)
|
CornerShape::Continuous => programs.continuous.as_ref(),
|
||||||
.and_then(|programs| programs.rounded.as_ref())
|
}) {
|
||||||
{
|
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +43,13 @@ pub(super) fn rounded_clip_program(
|
|||||||
UniformName::new("opacity", UniformType::_1f),
|
UniformName::new("opacity", UniformType::_1f),
|
||||||
UniformName::new("shadow_color", UniformType::_4f),
|
UniformName::new("shadow_color", UniformType::_4f),
|
||||||
];
|
];
|
||||||
let texture = renderer.compile_custom_texture_shader(corner_shader(ROUNDED_TEXTURE_SHADER), &texture_uniforms);
|
let texture =
|
||||||
let border = renderer.compile_custom_pixel_shader(corner_shader(ROUNDED_BORDER_SHADER), &border_uniforms);
|
renderer.compile_custom_texture_shader(corner_shader_for(ROUNDED_TEXTURE_SHADER, shape), &texture_uniforms);
|
||||||
let shadow = renderer.compile_custom_pixel_shader(corner_shader(WINDOW_SHADOW_SHADER), &shadow_uniforms);
|
let border =
|
||||||
|
renderer.compile_custom_pixel_shader(corner_shader_for(ROUNDED_BORDER_SHADER, shape), &border_uniforms);
|
||||||
|
let shadow = renderer.compile_custom_pixel_shader(corner_shader_for(WINDOW_SHADOW_SHADER, shape), &shadow_uniforms);
|
||||||
let solid = renderer.compile_custom_pixel_shader(
|
let solid = renderer.compile_custom_pixel_shader(
|
||||||
corner_shader(ROUNDED_SOLID_SHADER),
|
corner_shader_for(ROUNDED_SOLID_SHADER, shape),
|
||||||
&[
|
&[
|
||||||
UniformName::new("clip_rect", UniformType::_4f),
|
UniformName::new("clip_rect", UniformType::_4f),
|
||||||
UniformName::new("radius", UniformType::_1f),
|
UniformName::new("radius", UniformType::_1f),
|
||||||
@@ -53,16 +62,27 @@ pub(super) fn rounded_clip_program(
|
|||||||
match compiled {
|
match compiled {
|
||||||
Ok((texture, border, shadow, solid)) => {
|
Ok((texture, border, shadow, solid)) => {
|
||||||
let programs = RoundedClipPrograms {
|
let programs = RoundedClipPrograms {
|
||||||
|
shape,
|
||||||
texture,
|
texture,
|
||||||
solid,
|
solid,
|
||||||
border,
|
border,
|
||||||
shadow,
|
shadow,
|
||||||
};
|
};
|
||||||
resources.contexts.entry(context).or_default().rounded = Some(programs.clone());
|
let context = resources.contexts.entry(context).or_default();
|
||||||
|
match shape {
|
||||||
|
CornerShape::Circular => context.rounded = Some(programs.clone()),
|
||||||
|
CornerShape::Continuous => context.continuous = Some(programs.clone()),
|
||||||
|
}
|
||||||
Some(programs)
|
Some(programs)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if !std::mem::replace(&mut resources.contexts.entry(context).or_default().rounded_warned, true) {
|
let context = resources.contexts.entry(context).or_default();
|
||||||
|
let warned = match shape {
|
||||||
|
CornerShape::Circular => &mut context.rounded_warned,
|
||||||
|
CornerShape::Continuous => &mut context.continuous_warned,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !std::mem::replace(warned, true) {
|
||||||
tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering");
|
tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering");
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -197,7 +217,7 @@ pub(super) fn window_tint_element(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = material_program(resources, renderer)?;
|
let program = material_program_for_corners(resources, renderer, corners.shape)?;
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
|
|||||||
@@ -399,13 +399,20 @@ pub(super) fn expanded_blur_region(
|
|||||||
pub(super) fn material_program(
|
pub(super) fn material_program(
|
||||||
resources: &mut RenderResources,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<MaterialProgram> {
|
||||||
|
material_program_for_corners(resources, renderer, CornerShape::Circular)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn material_program_for_corners(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
shape: CornerShape,
|
||||||
) -> Option<MaterialProgram> {
|
) -> Option<MaterialProgram> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = resources
|
if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape {
|
||||||
.contexts
|
CornerShape::Circular => programs.material.as_ref(),
|
||||||
.get(&context)
|
CornerShape::Continuous => programs.window_material.as_ref(),
|
||||||
.and_then(|programs| programs.material.as_ref())
|
}) {
|
||||||
{
|
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,10 +424,14 @@ pub(super) fn material_program(
|
|||||||
UniformName::new("shadow_rect", UniformType::_4f),
|
UniformName::new("shadow_rect", UniformType::_4f),
|
||||||
UniformName::new("shadow_values", UniformType::_2f),
|
UniformName::new("shadow_values", UniformType::_2f),
|
||||||
];
|
];
|
||||||
match renderer.compile_custom_pixel_shader(corner_shader(MATERIAL_SHADER), &uniforms) {
|
match renderer.compile_custom_pixel_shader(corner_shader_for(MATERIAL_SHADER, shape), &uniforms) {
|
||||||
Ok(program) => {
|
Ok(program) => {
|
||||||
let program = MaterialProgram(program);
|
let program = MaterialProgram(program);
|
||||||
resources.contexts.entry(context).or_default().material = Some(program.clone());
|
let context = resources.contexts.entry(context).or_default();
|
||||||
|
match shape {
|
||||||
|
CornerShape::Circular => context.material = Some(program.clone()),
|
||||||
|
CornerShape::Continuous => context.window_material = Some(program.clone()),
|
||||||
|
}
|
||||||
Some(program)
|
Some(program)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
mod backdrop;
|
mod backdrop;
|
||||||
mod capture;
|
mod capture;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod corner_tests;
|
||||||
mod decorations;
|
mod decorations;
|
||||||
mod materials;
|
mod materials;
|
||||||
mod overview;
|
mod overview;
|
||||||
@@ -51,7 +53,9 @@ use smithay::wayland::shell::wlr_layer::Layer;
|
|||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::metrics::FrameEffectMetrics;
|
use crate::metrics::FrameEffectMetrics;
|
||||||
use crate::presentation::{NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, physical_rect};
|
use crate::presentation::{
|
||||||
|
CornerShape, NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, corner_extent, physical_rect,
|
||||||
|
};
|
||||||
|
|
||||||
type SurfaceRenderElement =
|
type SurfaceRenderElement =
|
||||||
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
||||||
@@ -112,7 +116,39 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag");
|
|||||||
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
||||||
|
|
||||||
fn corner_shader(source: &str) -> String {
|
fn corner_shader(source: &str) -> String {
|
||||||
source.replace("//_CORNERS_", include_str!("shaders/corners.glsl"))
|
corner_shader_for(source, CornerShape::Circular)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||||
|
match shape {
|
||||||
|
CornerShape::Circular => source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")),
|
||||||
|
CornerShape::Continuous => source
|
||||||
|
.replace(
|
||||||
|
"//_CORNERS_",
|
||||||
|
&format!(
|
||||||
|
"#define CONTINUOUS_WINDOW_CORNERS\n{}",
|
||||||
|
include_str!("shaders/window_corners.glsl")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.replace("precision mediump float;", "precision highp float;"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_corner_shape(window: &smithay::desktop::Window) -> CornerShape {
|
||||||
|
// Semantic shell surfaces keep their client-matching circular outlines.
|
||||||
|
corner_shape_for_role(
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.and_then(|toplevel| crate::effects::surface_role(toplevel.wl_surface()).map(|(role, _)| role)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn corner_shape_for_role(role: Option<crate::effects::SemanticRole>) -> CornerShape {
|
||||||
|
if role.is_some() {
|
||||||
|
CornerShape::Circular
|
||||||
|
} else {
|
||||||
|
CornerShape::Continuous
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -287,6 +323,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct RoundedClipPrograms {
|
pub(crate) struct RoundedClipPrograms {
|
||||||
|
shape: CornerShape,
|
||||||
solid: GlesPixelProgram,
|
solid: GlesPixelProgram,
|
||||||
texture: GlesTexProgram,
|
texture: GlesTexProgram,
|
||||||
border: GlesPixelProgram,
|
border: GlesPixelProgram,
|
||||||
@@ -436,6 +473,18 @@ fn rounded_opaque_regions(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clipped_surface_damage(
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
clip_changed: bool,
|
||||||
|
client_damage: impl FnOnce() -> DamageSet<i32, Physical>,
|
||||||
|
) -> DamageSet<i32, Physical> {
|
||||||
|
if clip_changed {
|
||||||
|
DamageSet::from_slice(&[Rectangle::from_size(size)])
|
||||||
|
} else {
|
||||||
|
client_damage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Element for RoundedSurfaceRenderElement {
|
impl Element for RoundedSurfaceRenderElement {
|
||||||
fn id(&self) -> &Id {
|
fn id(&self) -> &Id {
|
||||||
self.inner.id()
|
self.inner.id()
|
||||||
@@ -458,10 +507,9 @@ impl Element for RoundedSurfaceRenderElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||||
if self.clip_changed {
|
clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || {
|
||||||
return DamageSet::from_slice(&[Rectangle::from_size(self.inner.geometry(scale).size)]);
|
|
||||||
}
|
|
||||||
self.inner.damage_since(scale, commit)
|
self.inner.damage_since(scale, commit)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||||
@@ -479,7 +527,7 @@ impl Element for RoundedSurfaceRenderElement {
|
|||||||
&self.inner.opaque_regions(scale),
|
&self.inner.opaque_regions(scale),
|
||||||
self.inner.geometry(scale).loc,
|
self.inner.geometry(scale).loc,
|
||||||
clip,
|
clip,
|
||||||
self.radius,
|
corner_extent(self.radius, clip.size, self.programs.shape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +852,7 @@ mod tests {
|
|||||||
id: Id,
|
id: Id,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
commit: CommitCounter,
|
commit: CommitCounter,
|
||||||
|
clip_changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DamageElement {
|
impl DamageElement {
|
||||||
@@ -812,6 +861,7 @@ mod tests {
|
|||||||
id: Id::new(),
|
id: Id::new(),
|
||||||
geometry,
|
geometry,
|
||||||
commit: CommitCounter::default(),
|
commit: CommitCounter::default(),
|
||||||
|
clip_changed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,6 +888,7 @@ mod tests {
|
|||||||
scale: Scale<f64>,
|
scale: Scale<f64>,
|
||||||
commit: Option<CommitCounter>,
|
commit: Option<CommitCounter>,
|
||||||
) -> smithay::backend::renderer::utils::DamageSet<i32, Physical> {
|
) -> smithay::backend::renderer::utils::DamageSet<i32, Physical> {
|
||||||
|
super::clipped_surface_damage(self.geometry(scale).size, self.clip_changed, || {
|
||||||
if commit == Some(self.current_commit()) {
|
if commit == Some(self.current_commit()) {
|
||||||
smithay::backend::renderer::utils::DamageSet::default()
|
smithay::backend::renderer::utils::DamageSet::default()
|
||||||
} else {
|
} else {
|
||||||
@@ -845,9 +896,45 @@ mod tests {
|
|||||||
self.geometry(scale).size,
|
self.geometry(scale).size,
|
||||||
)])
|
)])
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overview_role_changes_damage_unchanged_client_buffers_then_settle() {
|
||||||
|
use ferese_core::OutputId;
|
||||||
|
use ferese_layout::WindowId;
|
||||||
|
|
||||||
|
use super::CornerShape::{Circular, Continuous};
|
||||||
|
|
||||||
|
let mut resources = super::RenderResources::default();
|
||||||
|
let mut element = DamageElement::new(Rectangle::new((100, 100).into(), (200, 160).into()));
|
||||||
|
let client_commit = element.commit;
|
||||||
|
let mut tracker = OutputDamageTracker::new((640, 480), Scale::from(1.0), Transform::Normal);
|
||||||
|
let window = WindowId(1);
|
||||||
|
let output = OutputId(1);
|
||||||
|
tracker.damage_output(0, &[&element]).unwrap();
|
||||||
|
|
||||||
|
for shape in [Continuous, Circular, Continuous] {
|
||||||
|
// The main scene and thumbnail have independent clip histories.
|
||||||
|
resources.prepare_window_corners(window, shape);
|
||||||
|
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
|
||||||
|
assert_eq!(element.commit, client_commit);
|
||||||
|
assert!(element.clip_changed);
|
||||||
|
let damage = tracker.damage_output(1, &[&element]).unwrap().0.cloned().unwrap();
|
||||||
|
assert!(damage.iter().any(|rect| rect.contains((100, 100))));
|
||||||
|
assert!(damage.iter().any(|rect| rect.contains((299, 259))));
|
||||||
|
element.clip_changed = resources.thumbnail_corner_changed(window, output, shape);
|
||||||
|
assert!(!element.clip_changed);
|
||||||
|
assert!(tracker.damage_output(1, &[&element]).unwrap().0.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(resources.thumbnail_corner_changed(window, OutputId(2), Continuous));
|
||||||
|
assert!(!resources.thumbnail_corner_changed(window, output, Continuous));
|
||||||
|
resources.remove_output(output);
|
||||||
|
assert!(resources.thumbnail_corner_changed(window, output, Continuous));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
|
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
|
||||||
use smithay::backend::renderer::element::utils::{ConstrainAlign, constrain_render_elements};
|
use smithay::backend::renderer::element::utils::{ConstrainAlign, constrain_render_elements};
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ pub(super) fn overview_strip_elements(
|
|||||||
)
|
)
|
||||||
.opacity;
|
.opacity;
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
let programs = rounded_clip_program(&mut state.render, renderer);
|
|
||||||
state.retain_overview_labels();
|
state.retain_overview_labels();
|
||||||
|
|
||||||
let card_ids = cards
|
let card_ids = cards
|
||||||
@@ -221,19 +220,24 @@ pub(super) fn overview_strip_elements(
|
|||||||
elements.push(border.into());
|
elements.push(border.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(programs) = &programs {
|
|
||||||
for (id, rect) in &card.windows {
|
for (id, rect) in &card.windows {
|
||||||
if let Some(window) = state.windows.window(*id) {
|
if let Some(window) = state.windows.window(*id) {
|
||||||
|
let shape = window_corner_shape(window);
|
||||||
|
|
||||||
|
if let Some(programs) = corner_program(&mut state.render, renderer, shape) {
|
||||||
|
let clip_changed = state
|
||||||
|
.output_id(output)
|
||||||
|
.is_some_and(|output| state.render.thumbnail_corner_changed(*id, output, shape));
|
||||||
elements.extend(rounded_window_elements(
|
elements.extend(rounded_window_elements(
|
||||||
renderer,
|
renderer,
|
||||||
window,
|
window,
|
||||||
RoundedRect::new(*rect, output_geometry.loc, scale, 4.0),
|
RoundedRect::new(*rect, output_geometry.loc, scale, 4.0).with_shape(shape),
|
||||||
scale,
|
scale,
|
||||||
alpha,
|
alpha,
|
||||||
// The damage tracker tracks each miniature's crop,
|
// Crop/scale/alpha/geometry already track their own
|
||||||
// scale, alpha and geometry. Its corner radius is fixed,
|
// damage. Role changes also alter the corner shader,
|
||||||
// so an unchanged clip needs no extra full damage.
|
// even when the client buffer itself is unchanged.
|
||||||
false,
|
clip_changed,
|
||||||
output,
|
output,
|
||||||
programs.clone(),
|
programs.clone(),
|
||||||
ConstrainScaleBehavior::Stretch,
|
ConstrainScaleBehavior::Stretch,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub(super) struct WindowBuffers {
|
|||||||
pub dim: WindowBorderBuffers,
|
pub dim: WindowBorderBuffers,
|
||||||
pub resize_fill: WindowBorderBuffers,
|
pub resize_fill: WindowBorderBuffers,
|
||||||
pub shadow: WindowShadowBuffers,
|
pub shadow: WindowShadowBuffers,
|
||||||
|
corner_shape: Option<CornerShape>,
|
||||||
|
thumbnail_shapes: HashMap<OutputId, CornerShape>,
|
||||||
snapshot: Option<ResizeSnapshot>,
|
snapshot: Option<ResizeSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,17 +29,54 @@ pub(super) struct WindowBuffers {
|
|||||||
pub(super) struct ContextPrograms {
|
pub(super) struct ContextPrograms {
|
||||||
pub rounded: Option<RoundedClipPrograms>,
|
pub rounded: Option<RoundedClipPrograms>,
|
||||||
pub rounded_warned: bool,
|
pub rounded_warned: bool,
|
||||||
|
pub continuous: Option<RoundedClipPrograms>,
|
||||||
|
pub continuous_warned: bool,
|
||||||
|
pub window_material: Option<MaterialProgram>,
|
||||||
pub material: Option<MaterialProgram>,
|
pub material: Option<MaterialProgram>,
|
||||||
pub blur: Option<BlurProgram>,
|
pub blur: Option<BlurProgram>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderResources {
|
impl RenderResources {
|
||||||
|
pub(super) fn prepare_window_corners(&mut self, id: WindowId, shape: CornerShape) -> bool {
|
||||||
|
let buffers = self.windows.entry(id).or_default();
|
||||||
|
|
||||||
|
let changed = buffers.corner_shape.is_some_and(|old| old != shape);
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
buffers.borders.contexts.clear();
|
||||||
|
buffers.dim.contexts.clear();
|
||||||
|
buffers.resize_fill.contexts.clear();
|
||||||
|
buffers.shadow.contexts.clear();
|
||||||
|
|
||||||
|
if let Some(snapshot) = &mut buffers.snapshot {
|
||||||
|
snapshot.commit.increment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.corner_shape = Some(shape);
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn thumbnail_corner_changed(&mut self, id: WindowId, output: OutputId, shape: CornerShape) -> bool {
|
||||||
|
self.windows
|
||||||
|
.entry(id)
|
||||||
|
.or_default()
|
||||||
|
.thumbnail_shapes
|
||||||
|
.insert(output, shape)
|
||||||
|
.is_none_or(|previous| previous != shape)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_window(&mut self, id: WindowId) {
|
pub fn remove_window(&mut self, id: WindowId) {
|
||||||
self.windows.remove(&id);
|
self.windows.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_output(&mut self, id: OutputId) {
|
pub fn remove_output(&mut self, id: OutputId) {
|
||||||
self.outputs.remove(&id);
|
self.outputs.remove(&id);
|
||||||
|
|
||||||
|
for window in self.windows.values_mut() {
|
||||||
|
window.thumbnail_shapes.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
for surface in self.surfaces.values_mut() {
|
for surface in self.surfaces.values_mut() {
|
||||||
surface.contexts.retain(|(_, output, _), _| *output != id);
|
surface.contexts.retain(|(_, output, _), _| *output != id);
|
||||||
surface.captures.retain(|(_, output, _), _| *output != id);
|
surface.captures.retain(|(_, output, _), _| *output != id);
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ pub(crate) fn sampled_output_elements(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let scale = output.current_scale().fractional_scale();
|
let scale = output.current_scale().fractional_scale();
|
||||||
let rounded_clip_program = rounded_clip_program(&mut state.render, renderer);
|
|
||||||
|
|
||||||
let mut elements = if include_cursor {
|
let mut elements = if include_cursor {
|
||||||
cursor_elements(state, renderer, output_geometry, scale)
|
cursor_elements(state, renderer, output_geometry, scale)
|
||||||
@@ -160,7 +159,10 @@ pub(crate) fn sampled_output_elements(
|
|||||||
} else {
|
} else {
|
||||||
state.theme_settings.window_radius
|
state.theme_settings.window_radius
|
||||||
} * decoration_progress;
|
} * decoration_progress;
|
||||||
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
|
let shape = window_corner_shape(&window);
|
||||||
|
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape);
|
||||||
|
let corner_shape_changed = state.render.prepare_window_corners(id, shape);
|
||||||
|
let rounded_clip_program = corner_program(&mut state.render, renderer, shape);
|
||||||
let pixels = corners.rect;
|
let pixels = corners.rect;
|
||||||
// Only overview/close intentionally scale the complete application.
|
// Only overview/close intentionally scale the complete application.
|
||||||
let scale_content = frame.overview.is_presenting() || sample.close_scale != 1.0;
|
let scale_content = frame.overview.is_presenting() || sample.close_scale != 1.0;
|
||||||
@@ -262,7 +264,7 @@ pub(crate) fn sampled_output_elements(
|
|||||||
corners,
|
corners,
|
||||||
scale,
|
scale,
|
||||||
close_alpha,
|
close_alpha,
|
||||||
sample.geometry.presentation_changed,
|
sample.geometry.presentation_changed || corner_shape_changed,
|
||||||
output,
|
output,
|
||||||
programs.clone(),
|
programs.clone(),
|
||||||
behavior,
|
behavior,
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ void main() {
|
|||||||
float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
|
float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
|
||||||
float outer_coverage = edge_coverage(signed_distance);
|
float outer_coverage = edge_coverage(signed_distance);
|
||||||
|
|
||||||
|
#ifdef CONTINUOUS_WINDOW_CORNERS
|
||||||
|
// Inset the outer distance field along its normals, rather than shrinking
|
||||||
|
// the quartic's bounding box/radius and changing border thickness.
|
||||||
|
float inner_coverage = edge_coverage(signed_distance + border_width);
|
||||||
|
#else
|
||||||
vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0));
|
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);
|
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);
|
float inner_radius = max(radius - border_width, 0.0);
|
||||||
float inner_coverage = edge_coverage(rounded_rect_distance(gl_FragCoord.xy, inner_rect, inner_radius));
|
float inner_coverage = edge_coverage(rounded_rect_distance(gl_FragCoord.xy, inner_rect, inner_radius));
|
||||||
|
#endif
|
||||||
float coverage = max(outer_coverage - inner_coverage, 0.0);
|
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);
|
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
|
// Interpolate premultiplied endpoints: a transparent endpoint must not
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Apple-style corner approximation: three cubic Beziers per corner, using
|
||||||
|
// measured UIKit path coordinates (radius units), rather than a superellipse.
|
||||||
|
// Source and limits: https://liamrosenfeld.com/posts/apple_icon_quest/
|
||||||
|
// CoreGraphics' private implementation is not available on Linux. These
|
||||||
|
// published cubics have small tangent/curvature jumps at their internal joins;
|
||||||
|
// they must not be described as an exact globally G1/G2-continuous curve.
|
||||||
|
const float CORNER_EXTENT = 1.52866498;
|
||||||
|
|
||||||
|
void corner_controls(int segment, float blend, out vec2 a, out vec2 b, out vec2 c, out vec2 d) {
|
||||||
|
if (segment == 0) {
|
||||||
|
a = mix(vec2(0.0, 1.0), vec2(0.0, CORNER_EXTENT), blend);
|
||||||
|
b = mix(vec2(0.0, 0.8686781289), vec2(0.0, 1.08849296), blend);
|
||||||
|
c = mix(vec2(0.0258657631, 0.7386421566), vec2(0.0, 0.86840694), blend);
|
||||||
|
d = mix(vec2(0.0761204675, 0.6173165676), vec2(0.07491139, 0.63149379), blend);
|
||||||
|
} else if (segment == 1) {
|
||||||
|
a = mix(vec2(0.0761204675, 0.6173165676), vec2(0.07491139, 0.63149379), blend);
|
||||||
|
b = mix(vec2(0.1776144241, 0.3722884810), vec2(0.16905956, 0.37282383), blend);
|
||||||
|
c = b.yx;
|
||||||
|
d = a.yx;
|
||||||
|
} else {
|
||||||
|
a = mix(vec2(0.6173165676, 0.0761204675), vec2(0.63149379, 0.07491139), blend);
|
||||||
|
b = mix(vec2(0.7386421566, 0.0258657631), vec2(0.86840694, 0.0), blend);
|
||||||
|
c = mix(vec2(0.8686781289, 0.0), vec2(1.08849296, 0.0), blend);
|
||||||
|
d = mix(vec2(1.0, 0.0), vec2(CORNER_EXTENT, 0.0), blend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void corner_sample(float t, vec2 a, vec2 b, vec2 c, vec2 d,
|
||||||
|
out vec2 point, out vec2 tangent, out vec2 acceleration) {
|
||||||
|
vec2 first = 3.0 * (b - a);
|
||||||
|
vec2 second = 3.0 * (c - 2.0 * b + a);
|
||||||
|
vec2 third = d - 3.0 * c + 3.0 * b - a;
|
||||||
|
point = a + t * (first + t * (second + t * third));
|
||||||
|
tangent = first + t * (2.0 * second + 3.0 * t * third);
|
||||||
|
acceleration = 2.0 * second + 6.0 * t * third;
|
||||||
|
}
|
||||||
|
|
||||||
|
float corner_nearest(vec2 query, float t, vec2 a, vec2 b, vec2 c, vec2 d) {
|
||||||
|
// Bounded safeguarded Newton steps. Both endpoint seeds are needed for
|
||||||
|
// interior points with two minima; the midpoint may be a local maximum.
|
||||||
|
for (int iteration = 0; iteration < 8; iteration++) {
|
||||||
|
vec2 point, tangent, acceleration;
|
||||||
|
corner_sample(t, a, b, c, d, point, tangent, acceleration);
|
||||||
|
vec2 error = point - query;
|
||||||
|
float speed_squared = dot(tangent, tangent);
|
||||||
|
float denominator = max(speed_squared + dot(error, acceleration), speed_squared * 0.25);
|
||||||
|
t = clamp(t - clamp(dot(error, tangent) / denominator, -0.25, 0.25), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
|
||||||
|
vec2 half_size = rect.zw * 0.5;
|
||||||
|
vec2 inset = half_size - abs(point - rect.xy - half_size);
|
||||||
|
float limit = min(half_size.x, half_size.y);
|
||||||
|
|
||||||
|
// Preserve exact circular pills/circles and square/fullscreen outlines.
|
||||||
|
if (radius <= 0.0 || radius >= limit) {
|
||||||
|
vec2 q = vec2(radius) - inset;
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The measured profile extends about 1.53 radii along the straight edge.
|
||||||
|
// When that would overlap its opposite corner, smoothly converge to the
|
||||||
|
// circular profile. This size adaptation is Ferese policy, not UIKit code.
|
||||||
|
float blend = clamp((limit / radius - 1.0) / (CORNER_EXTENT - 1.0), 0.0, 1.0);
|
||||||
|
float extent = radius * mix(1.0, CORNER_EXTENT, blend);
|
||||||
|
|
||||||
|
if (max(inset.x, inset.y) >= extent) {
|
||||||
|
return -min(inset.x, inset.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 query = inset / radius;
|
||||||
|
float best_squared = 1.0e20;
|
||||||
|
float side = 1.0;
|
||||||
|
|
||||||
|
for (int segment = 0; segment < 3; segment++) {
|
||||||
|
vec2 a, b, c, d;
|
||||||
|
corner_controls(segment, blend, a, b, c, d);
|
||||||
|
|
||||||
|
for (int seed = 0; seed < 2; seed++) {
|
||||||
|
float t = corner_nearest(query, float(seed), a, b, c, d);
|
||||||
|
vec2 closest, tangent, acceleration;
|
||||||
|
corner_sample(t, a, b, c, d, closest, tangent, acceleration);
|
||||||
|
vec2 error = query - closest;
|
||||||
|
float squared = dot(error, error);
|
||||||
|
|
||||||
|
if (squared < best_squared) {
|
||||||
|
best_squared = squared;
|
||||||
|
side = dot(error, vec2(tangent.y, -tangent.x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqrt(best_squared) * radius * (side < 0.0 ? -1.0 : 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float edge_coverage(float distance) {
|
||||||
|
return clamp(0.5 - distance, 0.0, 1.0);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user