diff --git a/Cargo.lock b/Cargo.lock index a4f8826..6ee9349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "build_helpers" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "cfg_aliases", ] @@ -902,7 +902,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.2", + "unicode-width 0.1.14", ] [[package]] @@ -1034,7 +1034,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1055,7 +1055,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "quote", "syn 2.0.119", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "almost", "configparser", @@ -1655,6 +1655,7 @@ dependencies = [ "ferese-ipc", "ferese-layout", "ferese-protocols", + "ferese-shape", "fontdb", "futures-lite", "image", @@ -1782,6 +1783,11 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ferese-shape" +version = "0.1.0" +source = "git+https://github.com/ferese-wm/ferese-shape?tag=v0.1.0#63d7b6dfd520dd882fc493d3272e26029ae0c9b8" + [[package]] name = "ferese-shell" version = "0.1.0" @@ -2617,7 +2623,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "build_helpers", "dnd", @@ -2639,7 +2645,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "accesskit", "accesskit_winit", @@ -2648,13 +2654,14 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "bitflags 2.13.2", "build_helpers", "bytes", "cosmic-client-toolkit", "dnd", + "ferese-shape", "glam", "iced_accessibility", "lilt", @@ -2675,7 +2682,7 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "iced_core", "iced_futures", @@ -2685,7 +2692,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "futures", "iced_core", @@ -2699,7 +2706,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "bitflags 2.13.2", "bytemuck", @@ -2720,7 +2727,7 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "iced_graphics", "iced_runtime", @@ -2729,7 +2736,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2741,7 +2748,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "build_helpers", "bytes", @@ -2758,7 +2765,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "bytemuck", "cosmic-text", @@ -2775,7 +2782,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "as-raw-xcb-connection", "bitflags 2.13.2", @@ -2807,7 +2814,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.2" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "build_helpers", "cosmic-client-toolkit", @@ -2827,7 +2834,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "build_helpers", "cosmic-client-toolkit", @@ -3358,7 +3365,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libcosmic" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic?rev=03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0#03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" +source = "git+https://github.com/ferese-wm/libcosmic?tag=ferese-v0.1.0#9e7b137321cc197eca5034470031ab4896781048" dependencies = [ "apply", "ashpd 0.12.3", diff --git a/Cargo.toml b/Cargo.toml index dd66ff5..7501eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,12 @@ license = "MIT" rust-version = "1.95" [workspace.dependencies] +ferese-shape = { git = "https://github.com/ferese-wm/ferese-shape", tag = "v0.1.0" } bitflags = "2.9.4" calloop = { version = "0.14.4", features = ["signals"] } jiff = "0.2.37" libc = "0.2.176" -libcosmic = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0", default-features = false, features = ["multi-window", "tokio", "wayland"] } +libcosmic = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.0", default-features = false, features = ["multi-window", "tokio", "wayland"] } smithay = { version = "0.7.0", default-features = false, features = [ "backend_drm", "backend_gbm", diff --git a/README.md b/README.md index db48978..caaff40 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Ferese combines a window manager and desktop shell with one configuration. - [Continuum motion](docs/configuration.md#animations): interruptible springs, gesture momentum and motion settings shared by the compositor and shell - Multiple monitors with fractional scaling and automatic refresh-rate switching on low battery - Themes, wallpapers and blur that update live, with Light/Dark/Auto modes and custom KDL themes -- Squircle window corners with matching borders and shadows +- Squircle window and shell corners with matching borders, shadows and blur masks - Desktop widgets, including a clock and sticky notes - Screenshots with annotation, screen sharing and built-in recording - A built-in lock screen and authentication dialogs diff --git a/compositor/Cargo.toml b/compositor/Cargo.toml index 57913ea..1de664e 100644 --- a/compositor/Cargo.toml +++ b/compositor/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +ferese-shape.workspace = true ferese-config = { path = "../crates/ferese-config" } ab_glyph = "0.2.32" fontdb = "0.23.0" diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index a2c23d8..c70d2a9 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -146,12 +146,7 @@ pub(crate) fn physical_rect( ) } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub(crate) enum CornerShape { - #[default] - Circular, - Continuous, -} +pub(crate) use ferese_shape::Shape as CornerShape; /// Conservative extent of the corner shoulders, in physical pixels. pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size, shape: CornerShape) -> f32 { diff --git a/compositor/src/render/corner_tests.rs b/compositor/src/render/corner_tests.rs index e5145a8..4da6adb 100644 --- a/compositor/src/render/corner_tests.rs +++ b/compositor/src/render/corner_tests.rs @@ -153,28 +153,50 @@ fn coverage(distance: f64) -> f64 { } #[test] -fn shell_roles_and_shader_sources_keep_circular_corners() { - use crate::effects::SemanticRole::*; +fn shared_geometry_matches_the_existing_window_profile() { + let bounds = [1.25, 2.5, 61.5, 59.0]; - for role in [Panel, PanelElevated, Popover, Menu, Notification, Hud, Modal] { - assert_eq!(corner_shape_for_role(Some(role)), CornerShape::Circular); + for shape in [CornerShape::Circular, CornerShape::Continuous] { + for radius in [0.0, 0.375, 8.25, 12.375, 24.0, 29.49, 29.5] { + let outline = ferese_shape::Outline::new(bounds, [radius; 4], shape).unwrap(); + + for y in 0..64 { + for x in 0..64 { + let point = [x as f64 + 0.5, y as f64 + 0.5]; + let expected = reference_distance(point, bounds, radius, shape); + let actual = outline.signed_distance(point); + assert!( + (actual - expected).abs() < 0.01, + "{shape:?} radius={radius} point={point:?}" + ); + assert!((coverage(actual) - coverage(expected)).abs() <= 2.0 / 255.0); + } + } + } } +} - assert_eq!(corner_shape_for_role(None), CornerShape::Continuous); - +#[test] +fn shell_masks_match_the_shared_squircle_profile() { for source in [BLUR_SHADER, MATERIAL_SHADER, ROUNDED_TEXTURE_SHADER] { assert_eq!( corner_shader(source), - source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")) + corner_shader_for(source, CornerShape::Continuous) ); - assert!(!corner_shader(source).contains("CORNER_EXTENT")); + assert!(corner_shader(source).contains("CORNER_EXTENT")); + assert!(!corner_shader_for(source, CornerShape::Circular).contains("CORNER_EXTENT")); } } fn renderer() -> GlesRenderer { use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let devices = EGLDevice::enumerate().unwrap().collect::>(); + let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some(); + let device = devices + .into_iter() + .find(|device| device.is_software() == software) + .expect("the requested EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() } } @@ -334,12 +356,12 @@ fn apple_window_fill_and_inset_border_match_path_reference_at_fractional_scales( #[test] #[ignore = "requires an EGL rendering device"] -fn window_programs_do_not_change_cached_shell_materials_and_blur() { +fn circular_and_squircle_programs_keep_independent_caches() { 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(); + let circular = corner_program(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); + material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); blur_program(&mut resources, &mut renderer).unwrap(); let uniforms = [ Uniform::new("clip_rect", [0.0f32, 0.0, 64.0, 64.0]), @@ -349,7 +371,7 @@ fn window_programs_do_not_change_cached_shell_materials_and_blur() { 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 circular = corner_program(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let after = rasterize(&mut renderer, &circular.solid, &uniforms); assert_eq!(before, after); let cache = &resources.contexts[&context]; @@ -550,3 +572,57 @@ fn measured_uikit_profile_has_flat_shoulders_but_small_internal_join_discontinui assert!((curvature_jump - 0.354_989_57).abs() < 1.0e-6); } } + +#[test] +#[ignore = "manual EGL timing; optional FERESE_CORNER_REFERENCE_SHADER path"] +fn corner_heavy_shader_timing_sample() { + use smithay::backend::renderer::gles::{UniformName, UniformType}; + let mut renderer = renderer(); + let reference = std::env::var_os("FERESE_CORNER_REFERENCE_SHADER") + .map(|path| std::fs::read_to_string(path).expect("reference GLSL")); + let current = include_str!("shaders/window_corners.glsl"); + let size = (1024, 768).into(); + let rect = Rectangle::::from_size(size); + let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap(); + let body = "\nvoid main() { float d = rounded_rect_distance(mod(gl_FragCoord.xy, vec2(64.0)), vec4(0.0, 0.0, 64.0, 64.0), radius); gl_FragColor = vec4(edge_coverage(d)); }"; + for (name, source) in reference + .as_deref() + .map(|s| ("reference", s)) + .into_iter() + .chain([("optimized", current)]) + { + let shader = format!("precision highp float; uniform float radius;\n{source}{body}"); + let program = renderer + .compile_custom_pixel_shader(shader, &[UniformName::new("radius", UniformType::_1f)]) + .unwrap(); + for radius in [8.0f32, 16.0, 24.0] { + let mut samples = Vec::new(); + for i in 0..110 { + let start = 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( + &program, + Rectangle::from_size((1024.0, 768.0).into()), + rect, + (1024, 768).into(), + Some(&[rect]), + 1.0, + &[Uniform::new("radius", radius)], + ) + .unwrap(); + frame.finish().unwrap().wait().unwrap(); + if i >= 10 { + samples.push(start.elapsed()); + } + } + samples.sort_unstable(); + eprintln!( + "corner-heavy {name} radius={radius}: median={:?}, p95={:?}", + samples[50], samples[95] + ); + } + } +} diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index 832e37d..56c1341 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -4,7 +4,7 @@ pub(super) fn rounded_clip_program( resources: &mut RenderResources, renderer: &mut GlesRenderer, ) -> Option { - corner_program(resources, renderer, CornerShape::Circular) + corner_program(resources, renderer, CornerShape::Continuous) } pub(super) fn corner_program( diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index 7df6a78..02fea8e 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -428,7 +428,7 @@ pub(super) fn material_program( resources: &mut RenderResources, renderer: &mut GlesRenderer, ) -> Option { - material_program_for_corners(resources, renderer, CornerShape::Circular) + material_program_for_corners(resources, renderer, CornerShape::Continuous) } pub(super) fn material_program_for_corners( diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index b882e6c..d7d46bb 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -119,7 +119,7 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag"); const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag"); fn corner_shader(source: &str) -> String { - corner_shader_for(source, CornerShape::Circular) + corner_shader_for(source, CornerShape::Continuous) } fn corner_shader_for(source: &str, shape: CornerShape) -> String { @@ -137,21 +137,9 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String { } } -pub(crate) 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) -> CornerShape { - if role.is_some() { - CornerShape::Circular - } else { - CornerShape::Continuous - } +pub(crate) fn window_corner_shape(_window: &smithay::desktop::Window) -> CornerShape { + // Shell surfaces and application windows use the same shared squircle profile. + CornerShape::Continuous } #[derive(Clone, Debug)] diff --git a/compositor/src/render/shaders/material_shader.frag b/compositor/src/render/shaders/material_shader.frag index 4e5603e..b26d989 100644 --- a/compositor/src/render/shaders/material_shader.frag +++ b/compositor/src/render/shaders/material_shader.frag @@ -15,6 +15,9 @@ void main() { float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius); float coverage = edge_coverage(sdf); if (paint_mode > 0.5) { + // The shadow pass is multiplied by (1 - coverage). Fully covered + // pixels need neither a second contour solve nor a Gaussian sample. + if (coverage >= 1.0) { gl_FragColor = vec4(0.0); return; } float distance = max(rounded_rect_distance(gl_FragCoord.xy, shadow_rect, material_radius), 0.0); float sigma = max(shadow_values.x * 0.5, 0.5); float opacity = exp(-0.5 * distance * distance / (sigma * sigma)) diff --git a/compositor/src/render/shaders/rounded_border_shader.frag b/compositor/src/render/shaders/rounded_border_shader.frag index a359c23..902898f 100644 --- a/compositor/src/render/shaders/rounded_border_shader.frag +++ b/compositor/src/render/shaders/rounded_border_shader.frag @@ -26,7 +26,7 @@ void main() { #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. + // the profile'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)); diff --git a/compositor/src/render/shaders/window_corners.glsl b/compositor/src/render/shaders/window_corners.glsl index 8843dba..69e22ad 100644 --- a/compositor/src/render/shaders/window_corners.glsl +++ b/compositor/src/render/shaders/window_corners.glsl @@ -44,7 +44,11 @@ float corner_nearest(vec2 query, float t, vec2 a, vec2 b, vec2 c, vec2 d) { 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); + float next = clamp(t - clamp(dot(error, tangent) / denominator, -0.25, 0.25), 0.0, 1.0); + if (next == t) { + break; + } + t = next; } return t; @@ -71,13 +75,23 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) { return -min(inset.x, inset.y); } - vec2 query = inset / radius; + // Reflect onto the first half of the symmetric corner. The opposite + // shoulder cannot be nearer than its reflection, so omit segment 2. + vec2 query = vec2(min(inset.x, inset.y), max(inset.x, inset.y)) / radius; float best_squared = 1.0e20; float side = 1.0; - for (int segment = 0; segment < 3; segment++) { + for (int segment = 0; segment < 2; segment++) { vec2 a, b, c, d; corner_controls(segment, blend, a, b, c, d); + // The cubic stays inside its control hull. Reject only when even + // that hull's box cannot improve the closest point found so far. + vec2 low = min(min(a, b), min(c, d)); + vec2 high = max(max(a, b), max(c, d)); + vec2 delta = max(max(low - query, query - high), 0.0); + if (dot(delta, delta) > best_squared) { + continue; + } for (int seed = 0; seed < 2; seed++) { float t = corner_nearest(query, float(seed), a, b, c, d); diff --git a/crates/ferese-theme/Cargo.toml b/crates/ferese-theme/Cargo.toml index ae71748..73d3be3 100644 --- a/crates/ferese-theme/Cargo.toml +++ b/crates/ferese-theme/Cargo.toml @@ -10,7 +10,7 @@ jiff.workspace = true ferese-theme-model = { path = "../ferese-theme-model" } libcosmic = { workspace = true, features = ["a11y"] } -iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" } +iced_accessibility = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.0" } [dev-dependencies] ferese-config = { path = "../ferese-config" } diff --git a/crates/ferese-theme/src/button.rs b/crates/ferese-theme/src/button.rs index 2d3a6ab..dd48234 100644 --- a/crates/ferese-theme/src/button.rs +++ b/crates/ferese-theme/src/button.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Background, Color}; use cosmic::theme; use cosmic::widget::button; @@ -27,6 +28,8 @@ pub(crate) fn accent_button_style(theme: &cosmic::Theme, state: State, focused: let background = composite(fill, native.primary(false).base.into()); let foreground = foreground(background, component.on.into()); button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: Some(Background::Color(background)), text_color: Some(foreground), icon_color: Some(foreground), diff --git a/crates/ferese-theme/src/calendar.rs b/crates/ferese-theme/src/calendar.rs index 8f839e4..7196db6 100644 --- a/crates/ferese-theme/src/calendar.rs +++ b/crates/ferese-theme/src/calendar.rs @@ -1,4 +1,5 @@ //! Date/time labels and a calendar grid; callers own navigation and popup behavior. +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Alignment, Background, Border, Length}; use cosmic::widget::{column, container, row}; use cosmic::{Element, theme}; @@ -124,6 +125,7 @@ pub fn grid<'a, M: Clone + 'a>( .class(theme::Container::custom(move |_| container::Style { background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))), border: Border { + shape: BorderShape::Continuous, radius: palette.radius.min(12.).into(), ..Default::default() }, diff --git a/crates/ferese-theme/src/controls.rs b/crates/ferese-theme/src/controls.rs index a08e581..77fb528 100644 --- a/crates/ferese-theme/src/controls.rs +++ b/crates/ferese-theme/src/controls.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Background, Border, Color, Vector}; use cosmic::widget::{button, container}; use cosmic::{theme, widget}; @@ -66,6 +67,8 @@ pub fn text_button<'a, M: Clone + 'a>( pub fn switch<'a, M: Clone + 'a>(enabled: bool, palette: Palette) -> button::Button<'a, M> { let paint = move |focused| button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: None, text_color: Some(palette.text), icon_color: Some(palette.text), @@ -100,6 +103,8 @@ pub fn navigation_style(p: Palette, selected: bool) -> theme::Button { pub fn settings_input(p: Palette) -> theme::TextInput { let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance { + shape: Some(BorderShape::Continuous), + outline: None, background: mix(p.card, p.text, if hovered { 0.06 } else { 0.035 }).into(), border_radius: p.radius.min(7.).into(), border_width: 1., @@ -133,9 +138,11 @@ pub fn select<'a, M: 'a>( widget::container(content).class(theme::Container::custom(move |_| container::Style { background: Some(mix(p.card, p.text, 0.035).into()), border: Border { + shape: BorderShape::Continuous, width: 1., color: mix(p.card, p.text, 0.18), radius: p.radius.min(7.).into(), + ..Default::default() }, ..Default::default() })) @@ -170,6 +177,8 @@ fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32, foc }; button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: Some(Background::Color(background)), text_color: Some(on), icon_color: Some(on), @@ -208,6 +217,8 @@ pub fn authentication_button(palette: Palette) -> theme::Button { (crate::composite(palette.card, palette.background), palette.muted) }; button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: Some(Background::Color(fill)), text_color: Some(foreground), icon_color: Some(foreground), @@ -231,6 +242,8 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput { let accent = palette.accent; let radius = palette.radius.min(10.); let appearance = move |focused: bool| widget::text_input::Appearance { + shape: Some(BorderShape::Continuous), + outline: None, background: Color::from_rgba(text.r, text.g, text.b, 0.045).into(), border_radius: radius.into(), border_width: 1., @@ -258,6 +271,8 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput { pub fn lock_input(radius: f32, accent: cosmic::iced::Color, surface: cosmic::iced::Color) -> theme::TextInput { let appearance = move |focused: bool| widget::text_input::Appearance { + shape: Some(BorderShape::Continuous), + outline: None, background: cosmic::iced::Color::from_rgba( (surface.r + 1.) * 0.5, (surface.g + 1.) * 0.5, @@ -299,6 +314,8 @@ pub fn shell_button( pressed: bool, ) -> button::Style { button::Style { + shape: Some(BorderShape::Continuous), + outline: None, text_color: Some(foreground), icon_color: Some(foreground), border_radius: radius.into(), @@ -318,6 +335,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style { container::Style { background: Some(Background::Color(background)), border: Border { + shape: BorderShape::Continuous, radius: radius.into(), ..Default::default() }, @@ -327,6 +345,8 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style { pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button { let style = move |active: bool| button::Style { + shape: Some(BorderShape::Continuous), + outline: None, text_color: Some(foreground), icon_color: Some(foreground), background: (active || filled).then_some(Background::Color(hover)), @@ -343,6 +363,8 @@ pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32) -> theme::Button { let paint = move |outline| button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: Some(Color { a: opacity, ..fill }.into()), text_color: Some(foreground), icon_color: Some(foreground), diff --git a/crates/ferese-theme/src/gallery.rs b/crates/ferese-theme/src/gallery.rs index f7b830e..0283cfc 100644 --- a/crates/ferese-theme/src/gallery.rs +++ b/crates/ferese-theme/src/gallery.rs @@ -1,5 +1,6 @@ use cosmic::iced::advanced::widget::{Id, Operation, Tree, tree}; use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer}; +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Event, Length, Rectangle, Size, keyboard}; use cosmic::{Element, Renderer, Theme}; use ferese_theme_model::Appearance; @@ -65,6 +66,8 @@ pub fn tile( fn tile_style(palette: crate::Palette, selected: bool) -> cosmic::theme::Button { let style = move |focused: bool, hovered: bool| cosmic::widget::button::Style { + shape: Some(BorderShape::Continuous), + outline: None, background: Some(if hovered { palette.sidebar } else { palette.card }.into()), border_width: if selected { 2. } else { 0. }, border_color: palette.accent, @@ -395,9 +398,11 @@ impl Widget for Radio<'_, M> { renderer::Quad { bounds: layout.bounds(), border: cosmic::iced::Border { + shape: BorderShape::Continuous, color: style.text_color, width: 2., radius: 10.into(), + ..Default::default() }, ..Default::default() }, diff --git a/crates/ferese-theme/src/geometry.rs b/crates/ferese-theme/src/geometry.rs index 629a2b2..05096b2 100644 --- a/crates/ferese-theme/src/geometry.rs +++ b/crates/ferese-theme/src/geometry.rs @@ -1,4 +1,4 @@ -/// Radius of a concentric inset, in the same units as the outer radius. +/// Radius of a circular inset. Squircle insets retain an `Outline` and its distance offset. pub fn inner_radius(outer: f32, padding: f32) -> f32 { (outer - padding).max(0.0) } diff --git a/crates/ferese-theme/src/menus.rs b/crates/ferese-theme/src/menus.rs index 2fa268e..cf7b2e5 100644 --- a/crates/ferese-theme/src/menus.rs +++ b/crates/ferese-theme/src/menus.rs @@ -1,5 +1,6 @@ //! Shared menu layouts and paint; callers own actions and animation timing. use cosmic::Element; +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Alignment, Color, Length}; use cosmic::widget::{button, container, row as iced_row}; @@ -115,11 +116,13 @@ pub fn slider(foreground: Color, opacity: f32) -> cosmic::theme::iced::Slider { ), width: 4., border: cosmic::iced::Border { + shape: BorderShape::Continuous, radius: 2.into(), ..Default::default() }, }, handle: Handle { + corner_shape: BorderShape::Circular, shape: HandleShape::Circle { radius: 6. }, background: foreground.into(), border_width: 0., diff --git a/crates/ferese-theme/src/palette.rs b/crates/ferese-theme/src/palette.rs index 63209ff..69533c7 100644 --- a/crates/ferese-theme/src/palette.rs +++ b/crates/ferese-theme/src/palette.rs @@ -1,4 +1,5 @@ use cosmic::iced::Color; +use cosmic::iced::border::Shape as BorderShape; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Palette { @@ -68,8 +69,9 @@ impl Palette { .text_tint(rgba(self.text).color) .accent(rgba(self.accent).color) .build(); + crate::apply(&mut native, self.on_accent); - cosmic::Theme::custom(std::sync::Arc::new(native)) + cosmic::Theme::custom(std::sync::Arc::new(native)).corner_shape(BorderShape::Continuous) } pub fn application_style(self, background: Color) -> cosmic::iced::theme::Style { @@ -135,7 +137,9 @@ mod tests { resolved.accessibility.increase_contrast = high_contrast; let palette = Palette::from_resolved(&resolved); assert_eq!(palette.high_contrast, high_contrast); - assert_eq!(palette.native_theme().cosmic().is_high_contrast, high_contrast); + let native = palette.native_theme(); + assert_eq!(native.cosmic().is_high_contrast, high_contrast); + assert_eq!(native.corner_shape, BorderShape::Continuous); } } } diff --git a/docs/configuration.md b/docs/configuration.md index f1fef4d..fad2f7b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -193,7 +193,7 @@ Live windows, overview thumbnails and retained images share a window identity and presentation state. Entering overview inherits the window's visible position and velocity, including motion from a workspace slide. Resize images use the same content mapping in overview and remain visible if the window closes during a resize. -Content, clipping and borders use the same rounded rectangle at the output's scale. +Content, clipping and borders use the same squircle outline at the output's scale. ### Spring tuning @@ -837,7 +837,10 @@ Text input is available by default; Ctrl+Alt+Escape releases an active shortcut inhibitor. Use `feresectl --help` for runtime control commands. Shell rounding is controlled in Settings → Appearance → Shell corner radius. -Small controls cap the radius to fit their size. Window rounding remains under +The shell and compositor use the same squircle profile for fills, borders, shadows +and blur masks. Small controls cap the radius to fit their size; circles and pills +keep circular outlines. The measured profile has small tangent and curvature +discontinuities at its internal segment joins. Window rounding remains under Settings → Windows. `theme.geometry.shell-radius` takes precedence over the old `appearance.corner-radius` and then `theme.geometry.top-bar-radius` keys; when none are set, the shell uses 14 px. Legacy clock/note radius fields no longer diff --git a/shell/ferese-lock/src/main.rs b/shell/ferese-lock/src/main.rs index 9a12d8c..0d79e69 100644 --- a/shell/ferese-lock/src/main.rs +++ b/shell/ferese-lock/src/main.rs @@ -457,9 +457,11 @@ impl Locker { text_color: Some(on), icon_color: Some(on), border: iced::Border { + shape: cosmic::iced::border::Shape::Continuous, radius: (avatar_size / 2.).into(), width: 1., color: a.accent.scale_alpha(0.3), + ..Default::default() }, ..Default::default() } diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index b70b68a..0d23334 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -1130,6 +1130,120 @@ mod tests { } } + #[test] + #[ignore = "release scroll/render sample; requires both renderer backends"] + fn profile_dense_pages_on_both_renderers() { + use cosmic::iced::advanced::renderer::{Headless, Renderer as _}; + use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, mouse, renderer, widget::Tree}; + use cosmic::iced::{Color, Event, Font, Pixels, Point, Rectangle, Size}; + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let bounds = Rectangle::with_size(Size::new(1000.0, 780.0)); + let scale = 1.25; + let size = Size::new(1250, 975); + + for backend in ["wgpu", "tiny-skia"] { + let mut renderer = runtime + .block_on(::new( + Font::default(), + Pixels(14.0), + Some(backend), + )) + .expect("requested renderer"); + eprintln!("renderer={}", Headless::name(&renderer)); + + for page in [Page::Shortcuts, Page::Appearance] { + let mut app = shortcuts(500); + app.page = page; + let theme = app.native_palette.native_theme(); + let mut tree: Option = None; + let mut samples = Vec::new(); + + for frame in 0..48 { + let mut messages = Vec::new(); + let mut clipboard = clipboard::Null; + let mut shell = Shell::new(&mut messages); + let cursor = mouse::Cursor::Available(Point::new(700.0, 450.0)); + let started = std::time::Instant::now(); + let mut view = app.page_view(); + let tree = tree.get_or_insert_with(|| Tree::new(view.as_widget())); + tree.diff(view.as_widget_mut()); + let node = + view.as_widget_mut() + .layout(tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size())); + view.as_widget_mut().update( + tree, + &Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { + x: 0.0, + y: if frame < 24 { -1.0 } else { 1.0 }, + }, + }), + Layout::new(&node), + cursor, + &renderer, + &mut clipboard, + &mut shell, + &bounds, + ); + let layout_elapsed = started.elapsed(); + renderer.reset(bounds); + let draw_started = std::time::Instant::now(); + view.as_widget().draw( + tree, + &mut renderer, + &theme, + &renderer::Style { + text_color: Color::WHITE, + icon_color: Color::WHITE, + scale_factor: scale as f64, + }, + Layout::new(&node), + cursor, + &bounds, + ); + let draw_elapsed = draw_started.elapsed(); + drop(view); + let render_started = std::time::Instant::now(); + let pixels = Headless::screenshot(&mut renderer, size, scale, theme.cosmic().bg_color().into()); + std::hint::black_box(pixels); + let render_elapsed = render_started.elapsed(); + + for message in messages { + let _ = app.update(message); + } + + if frame >= 8 { + samples.push(( + layout_elapsed.as_micros(), + draw_elapsed.as_micros(), + render_elapsed.as_micros(), + )); + } + } + + for (stage, index) in [("layout", 0), ("draw_queue", 1), ("render_and_readback", 2)] { + let mut times = samples + .iter() + .map(|sample| match index { + 0 => sample.0, + 1 => sample.1, + _ => sample.2, + }) + .collect::>(); + times.sort_unstable(); + eprintln!( + "backend={backend} page={page:?} stage={stage} median_us={} p95_us={}", + times[times.len() / 2], + times[times.len() * 95 / 100] + ); + } + } + } + } + #[test] fn disabling_auto_keeps_the_effective_appearance() { for appearance in [ diff --git a/shell/ferese-shell/Cargo.toml b/shell/ferese-shell/Cargo.toml index 8dbf911..1cb40e9 100644 --- a/shell/ferese-shell/Cargo.toml +++ b/shell/ferese-shell/Cargo.toml @@ -17,7 +17,7 @@ libc.workspace = true # Keep GPU rendering available as a fallback/override. The shell defaults to # tiny-skia to avoid a second GPU initialization during desktop startup. libcosmic = { workspace = true, features = ["wgpu", "a11y"] } -iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" } +iced_accessibility = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.0" } serde.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/shell/ferese-shell/src/bar.rs b/shell/ferese-shell/src/bar.rs index db2bf90..86462d9 100644 --- a/shell/ferese-shell/src/bar.rs +++ b/shell/ferese-shell/src/bar.rs @@ -2,6 +2,7 @@ use super::{ Background, BarMetrics, Border, Color, Element, FereseShell, Length, Message, ShellSnapshot, ShellTheme, alignment, bar_icon, button, color, color_with_opacity, container, control, motion, row, status_ui, text, theme, window, }; +use cosmic::iced::border::Shape as BorderShape; impl FereseShell { pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> { @@ -327,9 +328,11 @@ pub(super) fn workspace_selector_style( None }, border: Border { + shape: BorderShape::Continuous, color: color_with_opacity(shell_theme.accent, 0.55), width: if active_elsewhere { 1.0 } else { 0.0 }, radius: shell_theme.material_radius.min(14.0).into(), + ..Default::default() }, ..Default::default() } @@ -350,9 +353,11 @@ pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style { container::Style { background: Some(Background::Color(color_with_opacity(theme.border, 0.25))), border: Border { + shape: BorderShape::Continuous, color: color(theme.border), width: 1.0, radius: theme.material_radius.min(16.0).into(), + ..Default::default() }, ..Default::default() } diff --git a/shell/ferese-shell/src/desktop_widgets.rs b/shell/ferese-shell/src/desktop_widgets.rs index a71e599..a3e4b01 100644 --- a/shell/ferese-shell/src/desktop_widgets.rs +++ b/shell/ferese-shell/src/desktop_widgets.rs @@ -4,6 +4,7 @@ use super::{ configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor, set_margin, stacked_clock_digits, text, theme, window, }; +use cosmic::iced::border::Shape as BorderShape; impl FereseShell { pub(super) fn flush_note_changes(&mut self) -> Task { @@ -398,6 +399,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background, border: Border { + shape: BorderShape::Continuous, radius: radius.into(), ..Default::default() }, @@ -613,6 +615,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background, border: Border { + shape: BorderShape::Continuous, radius: radius.into(), ..Default::default() }, diff --git a/shell/ferese-shell/src/notification_ui.rs b/shell/ferese-shell/src/notification_ui.rs index c6fd5af..9090134 100644 --- a/shell/ferese-shell/src/notification_ui.rs +++ b/shell/ferese-shell/src/notification_ui.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::widget::scrollable::{Direction, Scrollbar}; use cosmic::widget::{column, scrollable}; @@ -56,6 +57,23 @@ fn card_button<'a>( .into() } +fn card_surface<'a>( + content: impl Into>>, + history: bool, + style: container::Style, +) -> Element<'a, cosmic::Action> { + container(content) + .id(if history { + "ferese-history-card" + } else { + "ferese-blur-card" + }) + .width(Length::Fill) + .clip_to_border(true) + .class(theme::Container::custom(move |_| style)) + .into() +} + fn icon_control<'a>( source: &'static [u8], label: &'static str, @@ -82,6 +100,7 @@ fn icon_control<'a>( .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(background)), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(12.0).into(), ..Default::default() }, @@ -287,6 +306,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(well)), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(15.0).into(), ..Default::default() }, @@ -313,6 +333,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(badge_fill.scale_alpha(opacity))), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(8.0).into(), ..Default::default() }, @@ -448,15 +469,10 @@ impl FereseShell { .push(actions); } } - let card = container(face) - .id(if history { - "ferese-history-card" - } else { - "ferese-blur-card" - }) - .width(Length::Fill) - .clip(true) - .class(theme::Container::custom(move |_| container::Style { + let card = card_surface( + face, + history, + container::Style { background: if history { Some(Background::Color(Color { a: 0.045 * opacity, @@ -466,14 +482,16 @@ impl FereseShell { (!compositor_material).then_some(Background::Color(base)) }, border: Border { + shape: BorderShape::Continuous, color: color_with_opacity(palette.text_muted, 0.18 * opacity), width: if history { 0.0 } else { 1.0 }, radius: palette.material_radius.into(), + ..Default::default() }, snap: true, ..Default::default() - })); - let card: Element<'_, cosmic::Action> = card.into(); + }, + ); let card: Element<'_, cosmic::Action> = if count > 1 { // With translucent materials, complete backing rectangles show // through the face and multiply its tint. Paint only the exposed @@ -488,6 +506,7 @@ impl FereseShell { ..foreground })), border: Border { + shape: BorderShape::Continuous, radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(), ..Default::default() }, @@ -648,6 +667,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(base)), border: Border { + shape: BorderShape::Continuous, radius: palette.material_radius.into(), ..Default::default() }, @@ -665,6 +685,7 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: Some(Background::Color(well)), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(28.0).into(), ..Default::default() }, @@ -766,9 +787,11 @@ impl FereseShell { .class(theme::Container::custom(move |_| container::Style { background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))), border: Border { + shape: BorderShape::Continuous, color: color_with_opacity(palette.text_muted, 0.18 * opacity), width: 1.0, radius: palette.material_radius.into(), + ..Default::default() }, snap: true, ..Default::default() @@ -803,3 +826,91 @@ impl FereseShell { } } } + +#[cfg(test)] +mod tests { + use super::*; + use cosmic::iced::advanced::renderer::{Headless, Renderer as _}; + use cosmic::iced::advanced::{Layout, layout, mouse, widget::Tree}; + use cosmic::iced::{Font, Pixels, Rectangle, Size}; + + #[test] + fn notification_action_hover_stays_inside_card_corners() { + check_action_hover("tiny-skia"); + } + + #[test] + #[ignore = "requires a GPU or software Vulkan adapter"] + fn gpu_notification_action_hover_stays_inside_card_corners() { + check_action_hover("wgpu"); + } + + fn check_action_hover(backend: &str) { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut renderer = runtime + .block_on(::new( + Font::default(), + Pixels(14.0), + Some(backend), + )) + .unwrap(); + let theme = cosmic::Theme::dark(); + let bounds = Rectangle::with_size(Size::new(368.0, 114.0)); + for history in [false, true] { + for scale in [1.0, 1.25, 1.5] { + let action = card_button( + cosmic::widget::Space::new().height(38), + Message::InvokeNotification(1, "action".into()), + Color::WHITE, + Color::WHITE, + false, + "Action".into(), + ); + let face = column([cosmic::widget::Space::new().height(76).into(), action]); + let mut card = card_surface( + face, + history, + container::Style { + border: Border { + shape: BorderShape::Continuous, + radius: 14.0.into(), + width: if history { 0.0 } else { 1.0 }, + ..Default::default() + }, + snap: true, + ..Default::default() + }, + ); + let mut tree = Tree::new(card.as_widget()); + let node = + card.as_widget_mut() + .layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size())); + renderer.reset(bounds); + card.as_widget().draw( + &tree, + &mut renderer, + &theme, + &Default::default(), + Layout::new(&node), + mouse::Cursor::Available((80.0, 100.0).into()), + &bounds, + ); + let size = Size::new((bounds.width * scale) as u32, (bounds.height * scale) as u32); + let pixels = Headless::screenshot(&mut renderer, size, scale, Color::TRANSPARENT); + let alpha = |x: u32, y: u32| pixels[((y * size.width + x) * 4 + 3) as usize]; + assert_eq!( + alpha(0, size.height - 1), + 0, + "{backend}, scale={scale}, history={history}" + ); + assert!( + alpha(size.width / 2, size.height - 4) > 200, + "the action hover must still render" + ); + } + } + } +} diff --git a/shell/ferese-shell/src/status_ui/bar.rs b/shell/ferese-shell/src/status_ui/bar.rs index 08b3664..f26eed3 100644 --- a/shell/ferese-shell/src/status_ui/bar.rs +++ b/shell/ferese-shell/src/status_ui/bar.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Alignment, Background, Border, Rectangle, alignment}; use cosmic::widget::{button, container, row}; use cosmic::{Element, theme}; @@ -82,6 +83,7 @@ impl FereseShell { move |_| container::Style { background: Some(Background::Color(foreground)), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(2.0).into(), ..Default::default() }, diff --git a/shell/ferese-shell/src/status_ui/controls.rs b/shell/ferese-shell/src/status_ui/controls.rs index 97d1370..fdb4e3f 100644 --- a/shell/ferese-shell/src/status_ui/controls.rs +++ b/shell/ferese-shell/src/status_ui/controls.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::{Alignment, Background, Border, Color, Length}; use cosmic::widget::{button, column, container, row, slider}; use cosmic::{Element, theme}; @@ -32,6 +33,7 @@ pub(super) fn status_summary<'a>( ..badge_color })), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(16.0).into(), ..Default::default() }, @@ -79,6 +81,7 @@ pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element } })), border: Border { + shape: BorderShape::Continuous, radius: motion::radius(2.5).into(), ..Default::default() }, diff --git a/shell/ferese-shell/src/status_ui/mod.rs b/shell/ferese-shell/src/status_ui/mod.rs index 91711f0..8ab006d 100644 --- a/shell/ferese-shell/src/status_ui/mod.rs +++ b/shell/ferese-shell/src/status_ui/mod.rs @@ -11,6 +11,7 @@ mod system; use bar::status_icon; use controls::{control_card, menu_button, toggle_row}; +use cosmic::iced::border::Shape as BorderShape; use cosmic::widget::column; use status::{Action, Snapshot}; @@ -242,9 +243,11 @@ impl FereseShell { text_color: Some(primary), icon_color: Some(primary), border: Border { + shape: BorderShape::Continuous, color: color_with_opacity(theme.border, p), width: 1.0, radius: theme.material_radius.into(), + ..Default::default() }, snap: true, ..Default::default() diff --git a/shell/ferese-shell/src/system_modal.rs b/shell/ferese-shell/src/system_modal.rs index 228b7a3..2168fb9 100644 --- a/shell/ferese-shell/src/system_modal.rs +++ b/shell/ferese-shell/src/system_modal.rs @@ -1,3 +1,4 @@ +use cosmic::iced::border::Shape as BorderShape; use cosmic::iced::widget::Space; use cosmic::widget::column; @@ -620,9 +621,11 @@ impl FereseShell { text_color: Some(palette.text), icon_color: Some(palette.text), border: Border { + shape: BorderShape::Continuous, color: palette.muted.scale_alpha(0.16), width: 1.0, radius: theme.material_radius.into(), + ..Default::default() }, snap: true, ..Default::default()