Align corner clipping, antialiasing, and fractional material regions

This commit is contained in:
Samuel Onoja committed 2026-10-01 16:27:31 +01:00
1 parent f110ec80e1
commit 9666e4ff6c
16 files changed
+237 -68

No files matched your search

+25 -12
View File
@@ -56,7 +56,7 @@ struct SurfaceEffectsState {
attached: AtomicBool, attached: AtomicBool,
generation: AtomicU64, generation: AtomicU64,
role: Mutex<Option<SemanticRole>>, role: Mutex<Option<SemanticRole>>,
regions: Mutex<Option<Vec<[i32; 5]>>>, regions: Mutex<Option<Vec<[f64; 5]>>>,
opacity: Mutex<u32>, opacity: Mutex<u32>,
presentation_supported: AtomicBool, presentation_supported: AtomicBool,
dismissing: AtomicBool, dismissing: AtomicBool,
@@ -183,7 +183,7 @@ impl Dispatch<FereseSurfaceMaterialV1, SurfaceEffectsUserData> for Ferese {
} }
} }
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> { pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[f64; 5]>> {
with_states(surface, |states| { with_states(surface, |states| {
states states
.data_map .data_map
@@ -195,7 +195,7 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
}) })
} }
fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> { fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 { if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 {
return None; return None;
} }
@@ -204,9 +204,12 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
.0 .0
.iter() .iter()
.map(|tuple| { .map(|tuple| {
let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index])); let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i])));
(values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768)) (values[2] > 0.0
.then_some(values) && values[3] > 0.0
&& values[4] >= 0.0
&& values.iter().all(|v| v.is_finite() && v.abs() <= 32768.0))
.then_some(values)
}) })
.collect() .collect()
} }
@@ -439,15 +442,25 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn rounded_regions_are_bounded_and_validated() { fn fractional_regions_validate_all_wire_values() {
let region = [12_i32, 8, 120, 48, 11]; let region = [0.25f32, -0.75, 100.5, 40.25, 13.5];
let bytes: Vec<_> = region.into_iter().flat_map(i32::to_ne_bytes).collect(); let encode = |r: [f32; 5]| r.into_iter().flat_map(f32::to_ne_bytes).collect::<Vec<_>>();
assert_eq!(decode_regions(&bytes), Some(vec![region])); let bytes = encode(region);
assert_eq!(decode_regions(&bytes), Some(vec![region.map(f64::from)]));
assert_eq!(decode_regions(&[]), Some(vec![])); assert_eq!(decode_regions(&[]), Some(vec![]));
assert!(decode_regions(&bytes[..19]).is_none()); assert!(decode_regions(&bytes[..19]).is_none());
assert!(decode_regions(&bytes.repeat(33)).is_none()); assert!(decode_regions(&bytes.repeat(33)).is_none());
for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] { for index in 0..5 {
assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::<Vec<_>>()).is_none()); for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 32769.0] {
let mut bad = region;
bad[index] = invalid;
assert!(decode_regions(&encode(bad)).is_none());
}
}
for (index, value) in [(2, 0.0), (3, -1.0), (4, -0.25)] {
let mut bad = region;
bad[index] = value;
assert!(decode_regions(&encode(bad)).is_none());
} }
} }
+15 -4
View File
@@ -32,11 +32,22 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
let texture = renderer.compile_custom_texture_shader(&corner_shader(ROUNDED_TEXTURE_SHADER), &texture_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 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 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)))); let solid = renderer.compile_custom_pixel_shader(
&corner_shader(ROUNDED_SOLID_SHADER),
&[
UniformName::new("clip_rect", UniformType::_4f),
UniformName::new("radius", UniformType::_1f),
UniformName::new("color", UniformType::_4f),
],
);
let compiled = texture.and_then(|texture| {
border.and_then(|border| shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid))))
});
match compiled { match compiled {
Ok((texture, border, shadow)) => { Ok((texture, border, shadow, solid)) => {
let programs = RoundedClipPrograms { let programs = RoundedClipPrograms {
texture, texture,
solid,
border, border,
shadow, shadow,
}; };
@@ -343,7 +354,7 @@ pub(super) fn rounded_window_elements(
alpha: f32, alpha: f32,
clip_changed: bool, clip_changed: bool,
output: &Output, output: &Output,
program: GlesTexProgram, programs: RoundedClipPrograms,
behavior: ConstrainScaleBehavior, behavior: ConstrainScaleBehavior,
) -> Vec<AnimatedWindowRenderElement> { ) -> Vec<AnimatedWindowRenderElement> {
let Some(toplevel) = window.toplevel() else { let Some(toplevel) = window.toplevel() else {
@@ -390,7 +401,7 @@ pub(super) fn rounded_window_elements(
.map(|inner| { .map(|inner| {
RoundedSurfaceRenderElement { RoundedSurfaceRenderElement {
inner, inner,
program: program.clone(), programs: programs.clone(),
clip_rect: clip, clip_rect: clip,
radius, radius,
clip_changed, clip_changed,
+44 -15
View File
@@ -20,23 +20,12 @@ pub(super) fn append_material_surface(
}; };
let regions = crate::effects::surface_regions(surface); let regions = crate::effects::surface_regions(surface);
let targets: Vec<_> = match &regions { let targets: Vec<_> = match &regions {
None => vec![(geometry, radius as f32)], None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius))],
Some(regions) => regions Some(regions) => regions
.iter() .iter()
.filter_map(|r| { .filter_map(|r| material_region_geometry(*r, geometry, content_origin, scale))
Rectangle::new(
(content_origin.x + r[0], content_origin.y + r[1]).into(),
(r[2], r[3]).into(),
)
.intersection(geometry)
.map(|rect| (rect, r[4] as f32))
})
.collect(), .collect(),
}; };
let targets: Vec<_> = targets
.into_iter()
.map(|(rect, radius)| (rect, RoundedRect::from_logical(rect, scale, f64::from(radius))))
.collect();
let materials: Vec<_> = targets let materials: Vec<_> = targets
.iter() .iter()
.enumerate() .enumerate()
@@ -65,7 +54,7 @@ pub(super) fn append_material_surface(
let mode = output.current_mode()?; let mode = output.current_mode()?;
let program = rounded_clip_program(state, renderer)?; let program = rounded_clip_program(state, renderer)?;
Some(( Some((
program.texture, program,
framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
corners.radius, corners.radius,
)) ))
@@ -84,7 +73,7 @@ pub(super) fn append_material_surface(
let element = if let Some((program, clip_rect, radius)) = &clip { let element = if let Some((program, clip_rect, radius)) = &clip {
WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement { WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement {
inner: element, inner: element,
program: program.clone(), programs: program.clone(),
clip_rect: *clip_rect, clip_rect: *clip_rect,
radius: *radius, radius: *radius,
clip_changed: false, clip_changed: false,
@@ -102,6 +91,27 @@ pub(super) fn append_material_surface(
} }
} }
/// Preserve the surface-local fractional edges until the shared physical snap.
fn material_region_geometry(
region: [f64; 5],
geometry: Rectangle<i32, Logical>,
origin: Point<i32, Logical>,
scale: f64,
) -> Option<(Rectangle<i32, Logical>, RoundedRect)> {
let rect = Rectangle::<f64, Logical>::new(
(f64::from(origin.x) + region[0], f64::from(origin.y) + region[1]).into(),
(region[2], region[3]).into(),
)
.intersection(geometry.to_f64())?;
let corners = RoundedRect::new(
ferese_layout::Rect::new(rect.loc.x, rect.loc.y, rect.size.w, rect.size.h),
(0, 0).into(),
scale,
region[4],
);
Some((rect.to_i32_up(), corners))
}
pub(super) struct MaterialSurface { pub(super) struct MaterialSurface {
pub geometry: Rectangle<i32, Logical>, pub geometry: Rectangle<i32, Logical>,
pub corners: RoundedRect, pub corners: RoundedRect,
@@ -424,3 +434,22 @@ pub(super) fn blur_damage(
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())]) DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
} }
} }
#[cfg(test)]
mod region_geometry_tests {
use super::*;
#[test]
fn fractional_regions_snap_once_in_output_pixels() {
let bounds = Rectangle::from_size((200, 100).into());
let (_, corners) = material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5).unwrap();
assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into()));
assert_eq!(corners.radius, 21.0);
// The old logical rounding would put this at physical (0, 0).
let (_, clipped) =
material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5).unwrap();
assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into()));
assert_eq!(clipped.radius, 30.0);
assert!(material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5).is_none());
}
}
+78 -3
View File
@@ -96,6 +96,8 @@ render_elements! {
const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag"); const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag");
const ROUNDED_SOLID_SHADER: &str = include_str!("shaders/rounded_solid_shader.frag");
const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag"); const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag");
const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag"); const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag");
@@ -280,6 +282,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct RoundedClipPrograms { pub(crate) struct RoundedClipPrograms {
solid: GlesPixelProgram,
texture: GlesTexProgram, texture: GlesTexProgram,
border: GlesPixelProgram, border: GlesPixelProgram,
shadow: GlesPixelProgram, shadow: GlesPixelProgram,
@@ -390,7 +393,7 @@ pub(crate) struct WindowShadowBuffers {
#[derive(Debug)] #[derive(Debug)]
struct RoundedSurfaceRenderElement { struct RoundedSurfaceRenderElement {
inner: WaylandSurfaceRenderElement<GlesRenderer>, inner: WaylandSurfaceRenderElement<GlesRenderer>,
program: GlesTexProgram, programs: RoundedClipPrograms,
clip_rect: [f32; 4], clip_rect: [f32; 4],
radius: f32, radius: f32,
clip_changed: bool, clip_changed: bool,
@@ -459,13 +462,25 @@ impl RenderElement<GlesRenderer> for RoundedSurfaceRenderElement {
opaque_regions, opaque_regions,
self.transform(), self.transform(),
self.alpha(), self.alpha(),
Some(&self.program), Some(&self.programs.texture),
&[ &[
Uniform::new("clip_rect", self.clip_rect), Uniform::new("clip_rect", self.clip_rect),
Uniform::new("radius", self.radius), Uniform::new("radius", self.radius),
], ],
), ),
WaylandSurfaceTexture::SolidColor(_) => self.inner.draw(frame, src, dst, damage, opaque_regions), WaylandSurfaceTexture::SolidColor(color) => frame.render_pixel_shader_to(
&self.programs.solid,
src,
dst,
(dst.size.w, dst.size.h).into(),
Some(damage),
self.alpha(),
&[
Uniform::new("clip_rect", self.clip_rect),
Uniform::new("radius", self.radius),
Uniform::new("color", color.components()),
],
),
} }
} }
@@ -514,6 +529,65 @@ mod tests {
scaled_visual_rect, shadow_bounds, scaled_visual_rect, shadow_bounds,
}; };
#[test]
#[ignore = "requires an EGL rendering device"]
fn solid_corner_mask_preserves_premultiplied_color() {
use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform, UniformName, UniformType};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
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() };
let program = renderer
.compile_custom_pixel_shader(
&super::corner_shader(super::ROUNDED_SOLID_SHADER),
&[
UniformName::new("clip_rect", UniformType::_4f),
UniformName::new("radius", UniformType::_1f),
UniformName::new("color", UniformType::_4f),
],
)
.unwrap();
let size = (32, 32).into();
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
let rect = Rectangle::<i32, Physical>::from_size((32, 32).into());
for (radius, corner_alpha, inset) in [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] {
let mut target = renderer.bind(&mut texture).unwrap();
{
let mut frame = renderer
.render(&mut target, (32, 32).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]),
0.5,
&[
Uniform::new("clip_rect", [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset]),
Uniform::new("radius", radius),
Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]),
],
)
.unwrap();
let _ = frame.finish().unwrap();
}
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
.unwrap();
let pixels = renderer.map_texture(&mapping).unwrap();
let center = (16 * 32 + 16) * 4;
assert!(pixels[center].abs_diff(64) <= 1);
assert!(pixels[center + 3].abs_diff(64) <= 1);
assert!(pixels[3].abs_diff(corner_alpha) <= 1);
}
}
#[test] #[test]
#[ignore = "requires an EGL rendering device"] #[ignore = "requires an EGL rendering device"]
fn shared_corner_shaders_compile() { fn shared_corner_shaders_compile() {
@@ -529,6 +603,7 @@ mod tests {
.unwrap(); .unwrap();
} }
for source in [ for source in [
super::ROUNDED_SOLID_SHADER,
super::ROUNDED_BORDER_SHADER, super::ROUNDED_BORDER_SHADER,
super::WINDOW_SHADOW_SHADER, super::WINDOW_SHADOW_SHADER,
super::MATERIAL_SHADER, super::MATERIAL_SHADER,
+1 -1
View File
@@ -228,7 +228,7 @@ pub(super) fn overview_strip_elements(
alpha, alpha,
true, true,
output, output,
programs.texture.clone(), programs.clone(),
ConstrainScaleBehavior::Stretch, ConstrainScaleBehavior::Stretch,
)); ));
} }
+1 -1
View File
@@ -265,7 +265,7 @@ pub(crate) fn output_elements(
.get(&id) .get(&id)
.is_some_and(|geometry| geometry.presentation_changed), .is_some_and(|geometry| geometry.presentation_changed),
output, output,
programs.texture.clone(), programs.clone(),
behavior, behavior,
)); ));
let source = window.geometry().size; let source = window.geometry().size;
+2 -1
View File
@@ -6,5 +6,6 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
} }
float edge_coverage(float distance) { float edge_coverage(float distance) {
return 1.0 - smoothstep(-0.5, 0.5, distance); // Match Iced/WGPU quad edges: a one-physical-pixel linear ramp.
return clamp(0.5 - distance, 0.0, 1.0);
} }
@@ -0,0 +1,13 @@
precision highp float;
uniform float alpha;
uniform vec4 clip_rect;
uniform float radius;
uniform vec4 color;
//_CORNERS_
void main() {
float distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
// Single-pixel buffer colors are already premultiplied.
gl_FragColor = color * alpha * edge_coverage(distance);
}
+2 -5
View File
@@ -604,9 +604,6 @@ pub fn resolve_with_context(
*path = theme_path(directory, path); *path = theme_path(directory, path);
} }
validate(&tokens)?; 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(); let requested_accent = tokens.colors.accent.clone();
transform(&mut tokens, &policy.accessibility, &mut warnings); transform(&mut tokens, &policy.accessibility, &mut warnings);
let theme = ResolvedTheme { let theme = ResolvedTheme {
@@ -1301,8 +1298,8 @@ pub fn import_family(id: &str, source: &str) -> Result<ImportedFamily, String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test] #[test]
fn resolved_shell_radius_matches_integer_effect_regions() { fn resolved_shell_radius_preserves_fractional_values() {
for (requested, expected) in [(0.0, 0.0), (0.4, 0.0), (13.4, 13.0), (13.5, 14.0)] { for (requested, expected) in [(0.0, 0.0), (0.4, 0.4), (13.4, 13.4), (13.5, 13.5)] {
let document = crate::Document::parse(&format!( let document = crate::Document::parse(&format!(
"theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}" "theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}"
)) ))
+1 -1
View File
@@ -183,7 +183,7 @@ Filled controls choose a contrasting text color automatically.
| `border-width` | number ≥ 0 | `1` | Window border thickness | | `border-width` | number ≥ 0 | `1` | Window border thickness |
| `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness | | `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness |
| `window-radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell | | `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; rounded to the nearest logical pixel to match blur regions; 0 makes them square | | `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; fractional radii are preserved; 0 makes them square |
| `top-bar-height` | number > 0 | `28` | Menu-bar height | | `top-bar-height` | number > 0 | `28` | Menu-bar height |
| `top-bar-margin-top` | integer ≥ 0 | `0` | Space above bar | | `top-bar-margin-top` | integer ≥ 0 | `0` | Space above bar |
| `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar | | `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar |
+5 -2
View File
@@ -81,8 +81,11 @@
<request name="set_regions" since="2"> <request name="set_regions" since="2">
<description summary="restrict material to rounded surface-local rectangles"> <description summary="restrict material to rounded surface-local rectangles">
An array of native-endian signed 32-bit tuples (x, y, width, height, An array of native-endian IEEE 754 binary32 tuples (x, y, width, height,
radius), measured in logical pixels. At most 32 rectangles are allowed. radius) in logical pixels. All values must be finite and have magnitude
at most 32768; width and height must be positive and radius nonnegative.
At most 32 rectangles are allowed. Invalid arrays are ignored.
Coordinates are rounded only after output scaling.
An empty array disables material painting without clearing the role. An empty array disables material painting without clearing the role.
Surfaces which never send this request retain full-surface materials. Surfaces which never send this request retain full-surface materials.
</description> </description>
+17 -8
View File
@@ -3,7 +3,7 @@ use cosmic::iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget, la
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector}; use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
use cosmic::{Element, Theme}; use cosmic::{Element, Theme};
pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[i32; 5]>>>; pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[f32; 5]>>>;
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
#[serde(default)] #[serde(default)]
@@ -463,18 +463,18 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
struct CollectRegions(Vec<Rectangle>); struct CollectRegions(Vec<Rectangle>);
impl CollectRegions { impl CollectRegions {
fn into_regions(self, translation: Vector, radius: f32) -> Vec<[i32; 5]> { fn into_regions(self, translation: Vector, radius: f32) -> Vec<[f32; 5]> {
self.0 self.0
.into_iter() .into_iter()
.take(32) .take(32)
.filter(|r| r.width > 0.0 && r.height > 0.0) .filter(|r| r.width > 0.0 && r.height > 0.0)
.map(|r| { .map(|r| {
[ [
(r.x + translation.x).round() as i32, r.x + translation.x,
(r.y + translation.y).round() as i32, r.y + translation.y,
r.width.round() as i32, r.width,
r.height.round() as i32, r.height,
radius.round().max(0.0) as i32, radius.max(0.0),
] ]
}) })
.collect() .collect()
@@ -508,6 +508,15 @@ impl widget::Operation for CollectRegions {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn material_regions_preserve_fractional_layout_and_translation() {
let collector = CollectRegions(vec![Rectangle::new((0.25, 2.5).into(), (100.25, 40.5).into())]);
assert_eq!(
collector.into_regions(Vector::new(0.125, -0.25), 13.5),
vec![[0.375, 2.25, 100.25, 40.5, 13.5]]
);
}
#[test] #[test]
fn centered_modal_material_tracks_surface_position_and_animation() { fn centered_modal_material_tracks_surface_position_and_animation() {
let mut collector = CollectRegions(Vec::new()); let mut collector = CollectRegions(Vec::new());
@@ -515,7 +524,7 @@ mod tests {
widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card); widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card);
assert_eq!( assert_eq!(
collector.into_regions(Vector::new(0.0, -2.0), 14.0), collector.into_regions(Vector::new(0.0, -2.0), 14.0),
vec![[380, 418, 440, 220, 14]] vec![[380., 418., 440., 220., 14.]]
); );
} }
@@ -467,6 +467,7 @@ impl FereseShell {
width: if history { 0.0 } else { 1.0 }, width: if history { 0.0 } else { 1.0 },
radius: palette.material_radius.into(), radius: palette.material_radius.into(),
}, },
snap: true,
..Default::default() ..Default::default()
})); }));
let card: Element<'_, cosmic::Action<Message>> = card.into(); let card: Element<'_, cosmic::Action<Message>> = card.into();
@@ -738,6 +739,7 @@ impl FereseShell {
width: 1.0, width: 1.0,
radius: palette.material_radius.into(), radius: palette.material_radius.into(),
}, },
snap: true,
..Default::default() ..Default::default()
})) }))
.into(); .into();
+1
View File
@@ -222,6 +222,7 @@ impl FereseShell {
width: 1.0, width: 1.0,
radius: theme.material_radius.into(), radius: theme.material_radius.into(),
}, },
snap: true,
..Default::default() ..Default::default()
})); }));
+29 -15
View File
@@ -433,12 +433,19 @@ pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Sty
} }
} }
fn encode_regions(regions: &[[f32; 5]]) -> Vec<u8> {
regions
.iter()
.flat_map(|region| region.iter().flat_map(|value| value.to_ne_bytes()))
.collect()
}
pub(super) struct EffectsBinding { pub(super) struct EffectsBinding {
connection: Connection, connection: Connection,
_manager: FereseEffectsManagerV1, _manager: FereseEffectsManagerV1,
pub(super) surface: FereseSurfaceEffectsV1, pub(super) surface: FereseSurfaceEffectsV1,
_queue: EventQueue<EffectsState>, _queue: EventQueue<EffectsState>,
regions: std::cell::RefCell<Option<Vec<[i32; 5]>>>, regions: std::cell::RefCell<Option<Vec<[f32; 5]>>>,
opacity: std::cell::Cell<Option<u32>>, opacity: std::cell::Cell<Option<u32>>,
} }
@@ -463,13 +470,11 @@ impl EffectsBinding {
let connection = Connection::from_backend(backend); let connection = Connection::from_backend(backend);
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?; let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
let qh = queue.handle(); let qh = queue.handle();
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=3, ())?; let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 3..=3, ())?;
let effects = manager.get_surface_effects(surface, &qh, ()); let effects = manager.get_surface_effects(surface, &qh, ());
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
if effects.version() >= 3 { effects.set_opacity(opacity);
effects.set_opacity(opacity);
}
if let Some(role) = role { if let Some(role) = role {
effects.set_role(role); effects.set_role(role);
} else { } else {
@@ -487,24 +492,19 @@ impl EffectsBinding {
}) })
} }
pub(super) fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> { pub(super) fn set_regions(&self, regions: &[[f32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover) self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
} }
pub(super) fn set_material_regions( pub(super) fn set_material_regions(
&self, &self,
regions: &[[i32; 5]], regions: &[[f32; 5]],
role: ferese_surface_effects_v1::Role, role: ferese_surface_effects_v1::Role,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) { if self.regions.borrow().as_deref() == Some(regions) {
return Ok(()); return Ok(());
} }
self.surface.set_regions( self.surface.set_regions(encode_regions(regions));
regions
.iter()
.flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes()))
.collect(),
);
if regions.is_empty() { if regions.is_empty() {
self.surface.clear_role(); self.surface.clear_role();
} else { } else {
@@ -529,7 +529,7 @@ impl EffectsBinding {
pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> { pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32; let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) { if self.opacity.get() != Some(opacity) {
self.surface.set_opacity(opacity); self.surface.set_opacity(opacity);
self.connection.flush()?; self.connection.flush()?;
self.opacity.set(Some(opacity)); self.opacity.set(Some(opacity));
@@ -554,3 +554,17 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1); delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1); delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
#[cfg(test)]
mod region_encoding_tests {
#[test]
fn fractional_region_encoding_preserves_values() {
let regions = [[0.25, -0.75, 100.5, 40.25, 14.0]];
let bytes = super::encode_regions(&regions);
let decoded: Vec<_> = bytes
.chunks_exact(4)
.map(|v| f32::from_ne_bytes(v.try_into().unwrap()))
.collect();
assert_eq!(decoded, regions[0]);
}
}
+1
View File
@@ -541,6 +541,7 @@ impl FereseShell {
width: 1.0, width: 1.0,
radius: theme.material_radius.into(), radius: theme.material_radius.into(),
}, },
snap: true,
..Default::default() ..Default::default()
})) }))
.into() .into()