diff --git a/compositor/src/effects.rs b/compositor/src/effects.rs index 1e88990..3d8104b 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -56,7 +56,7 @@ struct SurfaceEffectsState { attached: AtomicBool, generation: AtomicU64, role: Mutex>, - regions: Mutex>>, + regions: Mutex>>, opacity: Mutex, presentation_supported: AtomicBool, dismissing: AtomicBool, @@ -183,7 +183,7 @@ impl Dispatch for Ferese { } } -pub(crate) fn surface_regions(surface: &WlSurface) -> Option> { +pub(crate) fn surface_regions(surface: &WlSurface) -> Option> { with_states(surface, |states| { states .data_map @@ -195,7 +195,7 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option> { }) } -fn decode_regions(bytes: &[u8]) -> Option> { +fn decode_regions(bytes: &[u8]) -> Option> { if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 { return None; } @@ -204,9 +204,12 @@ fn decode_regions(bytes: &[u8]) -> Option> { .0 .iter() .map(|tuple| { - let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index])); - (values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768)) - .then_some(values) + let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i]))); + (values[2] > 0.0 + && values[3] > 0.0 + && values[4] >= 0.0 + && values.iter().all(|v| v.is_finite() && v.abs() <= 32768.0)) + .then_some(values) }) .collect() } @@ -439,15 +442,25 @@ mod tests { use super::*; #[test] - fn rounded_regions_are_bounded_and_validated() { - let region = [12_i32, 8, 120, 48, 11]; - let bytes: Vec<_> = region.into_iter().flat_map(i32::to_ne_bytes).collect(); - assert_eq!(decode_regions(&bytes), Some(vec![region])); + fn fractional_regions_validate_all_wire_values() { + let region = [0.25f32, -0.75, 100.5, 40.25, 13.5]; + let encode = |r: [f32; 5]| r.into_iter().flat_map(f32::to_ne_bytes).collect::>(); + let bytes = encode(region); + assert_eq!(decode_regions(&bytes), Some(vec![region.map(f64::from)])); assert_eq!(decode_regions(&[]), Some(vec![])); assert!(decode_regions(&bytes[..19]).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]] { - assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::>()).is_none()); + for index in 0..5 { + 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()); } } diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index d2f4b1b..1476cbe 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -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 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)))); + 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 { - Ok((texture, border, shadow)) => { + Ok((texture, border, shadow, solid)) => { let programs = RoundedClipPrograms { texture, + solid, border, shadow, }; @@ -343,7 +354,7 @@ pub(super) fn rounded_window_elements( alpha: f32, clip_changed: bool, output: &Output, - program: GlesTexProgram, + programs: RoundedClipPrograms, behavior: ConstrainScaleBehavior, ) -> Vec { let Some(toplevel) = window.toplevel() else { @@ -390,7 +401,7 @@ pub(super) fn rounded_window_elements( .map(|inner| { RoundedSurfaceRenderElement { inner, - program: program.clone(), + programs: programs.clone(), clip_rect: clip, radius, clip_changed, diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index ce388f7..c04f352 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -20,23 +20,12 @@ pub(super) fn append_material_surface( }; let regions = crate::effects::surface_regions(surface); let targets: Vec<_> = match ®ions { - None => vec![(geometry, radius as f32)], + None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius))], Some(regions) => regions .iter() - .filter_map(|r| { - 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)) - }) + .filter_map(|r| material_region_geometry(*r, geometry, content_origin, scale)) .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() @@ -65,7 +54,7 @@ pub(super) fn append_material_surface( let mode = output.current_mode()?; let program = rounded_clip_program(state, renderer)?; Some(( - program.texture, + program, framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), corners.radius, )) @@ -84,7 +73,7 @@ pub(super) fn append_material_surface( let element = if let Some((program, clip_rect, radius)) = &clip { WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement { inner: element, - program: program.clone(), + programs: program.clone(), clip_rect: *clip_rect, radius: *radius, 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, + origin: Point, + scale: f64, +) -> Option<(Rectangle, RoundedRect)> { + let rect = Rectangle::::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 geometry: Rectangle, pub corners: RoundedRect, @@ -424,3 +434,22 @@ pub(super) fn blur_damage( 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()); + } +} diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index f76f0f1..5a79146 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -96,6 +96,8 @@ render_elements! { 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 WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag"); @@ -280,6 +282,7 @@ impl RenderElement for BlurRenderElement { #[derive(Clone, Debug)] pub(crate) struct RoundedClipPrograms { + solid: GlesPixelProgram, texture: GlesTexProgram, border: GlesPixelProgram, shadow: GlesPixelProgram, @@ -390,7 +393,7 @@ pub(crate) struct WindowShadowBuffers { #[derive(Debug)] struct RoundedSurfaceRenderElement { inner: WaylandSurfaceRenderElement, - program: GlesTexProgram, + programs: RoundedClipPrograms, clip_rect: [f32; 4], radius: f32, clip_changed: bool, @@ -459,13 +462,25 @@ impl RenderElement for RoundedSurfaceRenderElement { opaque_regions, self.transform(), self.alpha(), - Some(&self.program), + Some(&self.programs.texture), &[ Uniform::new("clip_rect", self.clip_rect), 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, }; + #[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::::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] #[ignore = "requires an EGL rendering device"] fn shared_corner_shaders_compile() { @@ -529,6 +603,7 @@ mod tests { .unwrap(); } for source in [ + super::ROUNDED_SOLID_SHADER, super::ROUNDED_BORDER_SHADER, super::WINDOW_SHADOW_SHADER, super::MATERIAL_SHADER, diff --git a/compositor/src/render/overview.rs b/compositor/src/render/overview.rs index 9628c44..2a195ab 100644 --- a/compositor/src/render/overview.rs +++ b/compositor/src/render/overview.rs @@ -228,7 +228,7 @@ pub(super) fn overview_strip_elements( alpha, true, output, - programs.texture.clone(), + programs.clone(), ConstrainScaleBehavior::Stretch, )); } diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index b4b76f4..d5a6604 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -265,7 +265,7 @@ pub(crate) fn output_elements( .get(&id) .is_some_and(|geometry| geometry.presentation_changed), output, - programs.texture.clone(), + programs.clone(), behavior, )); let source = window.geometry().size; diff --git a/compositor/src/render/shaders/corners.glsl b/compositor/src/render/shaders/corners.glsl index c7dd456..d46dd31 100644 --- a/compositor/src/render/shaders/corners.glsl +++ b/compositor/src/render/shaders/corners.glsl @@ -6,5 +6,6 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) { } 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); } diff --git a/compositor/src/render/shaders/rounded_solid_shader.frag b/compositor/src/render/shaders/rounded_solid_shader.frag new file mode 100644 index 0000000..84c2fe4 --- /dev/null +++ b/compositor/src/render/shaders/rounded_solid_shader.frag @@ -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); +} diff --git a/crates/ferese-config/src/theme.rs b/crates/ferese-config/src/theme.rs index 28dd3c1..368aff1 100644 --- a/crates/ferese-config/src/theme.rs +++ b/crates/ferese-config/src/theme.rs @@ -604,9 +604,6 @@ 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 { @@ -1301,8 +1298,8 @@ pub fn import_family(id: &str, source: &str) -> Result { #[cfg(test)] mod tests { #[test] - fn resolved_shell_radius_matches_integer_effect_regions() { - for (requested, expected) in [(0.0, 0.0), (0.4, 0.0), (13.4, 13.0), (13.5, 14.0)] { + fn resolved_shell_radius_preserves_fractional_values() { + 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!( "theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}" )) diff --git a/docs/configuration.md b/docs/configuration.md index 1c53a30..d394722 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -183,7 +183,7 @@ Filled controls choose a contrasting text color automatically. | `border-width` | number ≥ 0 | `1` | Window border thickness | | `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness | | `window-radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell | -| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 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-margin-top` | integer ≥ 0 | `0` | Space above bar | | `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar | diff --git a/protocols/ferese-effects-v1.xml b/protocols/ferese-effects-v1.xml index ab68e13..80ab1b8 100644 --- a/protocols/ferese-effects-v1.xml +++ b/protocols/ferese-effects-v1.xml @@ -81,8 +81,11 @@ - An array of native-endian signed 32-bit tuples (x, y, width, height, - radius), measured in logical pixels. At most 32 rectangles are allowed. + An array of native-endian IEEE 754 binary32 tuples (x, y, width, height, + 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. Surfaces which never send this request retain full-surface materials. diff --git a/shell/ferese-shell/src/motion.rs b/shell/ferese-shell/src/motion.rs index 7efb91c..344cc8b 100644 --- a/shell/ferese-shell/src/motion.rs +++ b/shell/ferese-shell/src/motion.rs @@ -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::{Element, Theme}; -pub type Regions = std::sync::Arc>>; +pub type Regions = std::sync::Arc>>; #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[serde(default)] @@ -463,18 +463,18 @@ impl Widget for Motion<'_, M> { struct CollectRegions(Vec); 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 .into_iter() .take(32) .filter(|r| r.width > 0.0 && r.height > 0.0) .map(|r| { [ - (r.x + translation.x).round() as i32, - (r.y + translation.y).round() as i32, - r.width.round() as i32, - r.height.round() as i32, - radius.round().max(0.0) as i32, + r.x + translation.x, + r.y + translation.y, + r.width, + r.height, + radius.max(0.0), ] }) .collect() @@ -508,6 +508,15 @@ impl widget::Operation for CollectRegions { mod tests { 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] fn centered_modal_material_tracks_surface_position_and_animation() { 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); assert_eq!( collector.into_regions(Vector::new(0.0, -2.0), 14.0), - vec![[380, 418, 440, 220, 14]] + vec![[380., 418., 440., 220., 14.]] ); } diff --git a/shell/ferese-shell/src/notification_ui.rs b/shell/ferese-shell/src/notification_ui.rs index 19fa0ce..807a6cf 100644 --- a/shell/ferese-shell/src/notification_ui.rs +++ b/shell/ferese-shell/src/notification_ui.rs @@ -467,6 +467,7 @@ impl FereseShell { width: if history { 0.0 } else { 1.0 }, radius: palette.material_radius.into(), }, + snap: true, ..Default::default() })); let card: Element<'_, cosmic::Action> = card.into(); @@ -738,6 +739,7 @@ impl FereseShell { width: 1.0, radius: palette.material_radius.into(), }, + snap: true, ..Default::default() })) .into(); diff --git a/shell/ferese-shell/src/status_ui/mod.rs b/shell/ferese-shell/src/status_ui/mod.rs index 05693bc..50a171c 100644 --- a/shell/ferese-shell/src/status_ui/mod.rs +++ b/shell/ferese-shell/src/status_ui/mod.rs @@ -222,6 +222,7 @@ impl FereseShell { width: 1.0, radius: theme.material_radius.into(), }, + snap: true, ..Default::default() })); diff --git a/shell/ferese-shell/src/surfaces.rs b/shell/ferese-shell/src/surfaces.rs index 19d73cb..100644d 100644 --- a/shell/ferese-shell/src/surfaces.rs +++ b/shell/ferese-shell/src/surfaces.rs @@ -433,12 +433,19 @@ pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Sty } } +fn encode_regions(regions: &[[f32; 5]]) -> Vec { + regions + .iter() + .flat_map(|region| region.iter().flat_map(|value| value.to_ne_bytes())) + .collect() +} + pub(super) struct EffectsBinding { connection: Connection, _manager: FereseEffectsManagerV1, pub(super) surface: FereseSurfaceEffectsV1, _queue: EventQueue, - regions: std::cell::RefCell>>, + regions: std::cell::RefCell>>, opacity: std::cell::Cell>, } @@ -463,13 +470,11 @@ impl EffectsBinding { let connection = Connection::from_backend(backend); let (globals, queue) = registry_queue_init::(&connection)?; let qh = queue.handle(); - let manager = globals.bind::(&qh, 1..=3, ())?; + let manager = globals.bind::(&qh, 3..=3, ())?; let effects = manager.get_surface_effects(surface, &qh, ()); 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 { effects.set_role(role); } else { @@ -487,24 +492,19 @@ impl EffectsBinding { }) } - pub(super) fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box> { + pub(super) fn set_regions(&self, regions: &[[f32; 5]]) -> Result<(), Box> { self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover) } pub(super) fn set_material_regions( &self, - regions: &[[i32; 5]], + regions: &[[f32; 5]], role: ferese_surface_effects_v1::Role, ) -> Result<(), Box> { - if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) { + if self.regions.borrow().as_deref() == Some(regions) { return Ok(()); } - self.surface.set_regions( - regions - .iter() - .flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes())) - .collect(), - ); + self.surface.set_regions(encode_regions(regions)); if regions.is_empty() { self.surface.clear_role(); } else { @@ -529,7 +529,7 @@ impl EffectsBinding { pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box> { 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.connection.flush()?; self.opacity.set(Some(opacity)); @@ -554,3 +554,17 @@ impl Dispatch for EffectsState { delegate_noop!(EffectsState: ignore FereseEffectsManagerV1); 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(®ions); + let decoded: Vec<_> = bytes + .chunks_exact(4) + .map(|v| f32::from_ne_bytes(v.try_into().unwrap())) + .collect(); + assert_eq!(decoded, regions[0]); + } +} diff --git a/shell/ferese-shell/src/system_modal.rs b/shell/ferese-shell/src/system_modal.rs index b3aaa6e..7a36582 100644 --- a/shell/ferese-shell/src/system_modal.rs +++ b/shell/ferese-shell/src/system_modal.rs @@ -541,6 +541,7 @@ impl FereseShell { width: 1.0, radius: theme.material_radius.into(), }, + snap: true, ..Default::default() })) .into()