Match shell squircle outlines with compositor masks
Use shared corner geometry for shell controls, window clipping, materials and blur masks. Preserve circular pills and controls where appropriate, track contour changes in damage, and use tagged ferese-shape and libcosmic dependencies.
This commit is contained in:
1 parent
03a265b97f
commit
d75c93db85
31 files changed
+452
-79
No files matched your search
Generated
+26
-19
@@ -661,7 +661,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "build_helpers"
|
name = "build_helpers"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
]
|
]
|
||||||
@@ -902,7 +902,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1034,7 +1034,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config"
|
name = "cosmic-config"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"atomicwrites",
|
"atomicwrites",
|
||||||
"cosmic-config-derive",
|
"cosmic-config-derive",
|
||||||
@@ -1055,7 +1055,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config-derive"
|
name = "cosmic-config-derive"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -1124,7 +1124,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-theme"
|
name = "cosmic-theme"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"almost",
|
"almost",
|
||||||
"configparser",
|
"configparser",
|
||||||
@@ -1655,6 +1655,7 @@ dependencies = [
|
|||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
|
"ferese-shape",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"image",
|
"image",
|
||||||
@@ -1782,6 +1783,11 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-shape"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/ferese-wm/ferese-shape?tag=v0.1.0#63d7b6dfd520dd882fc493d3272e26029ae0c9b8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ferese-shell"
|
name = "ferese-shell"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2617,7 +2623,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced"
|
name = "iced"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
"dnd",
|
"dnd",
|
||||||
@@ -2639,7 +2645,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_accessibility"
|
name = "iced_accessibility"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_winit",
|
"accesskit_winit",
|
||||||
@@ -2648,13 +2654,14 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_core"
|
name = "iced_core"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
"dnd",
|
"dnd",
|
||||||
|
"ferese-shape",
|
||||||
"glam",
|
"glam",
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
"lilt",
|
"lilt",
|
||||||
@@ -2675,7 +2682,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_debug"
|
name = "iced_debug"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
@@ -2685,7 +2692,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_futures"
|
name = "iced_futures"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
@@ -2699,7 +2706,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_graphics"
|
name = "iced_graphics"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -2720,7 +2727,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_program"
|
name = "iced_program"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
@@ -2729,7 +2736,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_renderer"
|
name = "iced_renderer"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
"iced_tiny_skia",
|
"iced_tiny_skia",
|
||||||
@@ -2741,7 +2748,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_runtime"
|
name = "iced_runtime"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2758,7 +2765,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_tiny_skia"
|
name = "iced_tiny_skia"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cosmic-text",
|
"cosmic-text",
|
||||||
@@ -2775,7 +2782,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_wgpu"
|
name = "iced_wgpu"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
@@ -2807,7 +2814,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_widget"
|
name = "iced_widget"
|
||||||
version = "0.14.2"
|
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 = [
|
dependencies = [
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
@@ -2827,7 +2834,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_winit"
|
name = "iced_winit"
|
||||||
version = "0.14.0"
|
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 = [
|
dependencies = [
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
"cosmic-client-toolkit",
|
"cosmic-client-toolkit",
|
||||||
@@ -3358,7 +3365,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "libcosmic"
|
name = "libcosmic"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"apply",
|
"apply",
|
||||||
"ashpd 0.12.3",
|
"ashpd 0.12.3",
|
||||||
|
|||||||
+2
-1
@@ -26,11 +26,12 @@ license = "MIT"
|
|||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
ferese-shape = { git = "https://github.com/ferese-wm/ferese-shape", tag = "v0.1.0" }
|
||||||
bitflags = "2.9.4"
|
bitflags = "2.9.4"
|
||||||
calloop = { version = "0.14.4", features = ["signals"] }
|
calloop = { version = "0.14.4", features = ["signals"] }
|
||||||
jiff = "0.2.37"
|
jiff = "0.2.37"
|
||||||
libc = "0.2.176"
|
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 = [
|
smithay = { version = "0.7.0", default-features = false, features = [
|
||||||
"backend_drm",
|
"backend_drm",
|
||||||
"backend_gbm",
|
"backend_gbm",
|
||||||
|
|||||||
@@ -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
|
- [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
|
- 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
|
- 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
|
- Desktop widgets, including a clock and sticky notes
|
||||||
- Screenshots with annotation, screen sharing and built-in recording
|
- Screenshots with annotation, screen sharing and built-in recording
|
||||||
- A built-in lock screen and authentication dialogs
|
- A built-in lock screen and authentication dialogs
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-shape.workspace = true
|
||||||
ferese-config = { path = "../crates/ferese-config" }
|
ferese-config = { path = "../crates/ferese-config" }
|
||||||
ab_glyph = "0.2.32"
|
ab_glyph = "0.2.32"
|
||||||
fontdb = "0.23.0"
|
fontdb = "0.23.0"
|
||||||
|
|||||||
@@ -146,12 +146,7 @@ pub(crate) fn physical_rect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
pub(crate) use ferese_shape::Shape as CornerShape;
|
||||||
pub(crate) enum CornerShape {
|
|
||||||
#[default]
|
|
||||||
Circular,
|
|
||||||
Continuous,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Conservative extent of the corner shoulders, in physical pixels.
|
/// Conservative extent of the corner shoulders, in physical pixels.
|
||||||
pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size<i32, Physical>, shape: CornerShape) -> f32 {
|
pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size<i32, Physical>, shape: CornerShape) -> f32 {
|
||||||
|
|||||||
@@ -153,28 +153,50 @@ fn coverage(distance: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shell_roles_and_shader_sources_keep_circular_corners() {
|
fn shared_geometry_matches_the_existing_window_profile() {
|
||||||
use crate::effects::SemanticRole::*;
|
let bounds = [1.25, 2.5, 61.5, 59.0];
|
||||||
|
|
||||||
for role in [Panel, PanelElevated, Popover, Menu, Notification, Hud, Modal] {
|
for shape in [CornerShape::Circular, CornerShape::Continuous] {
|
||||||
assert_eq!(corner_shape_for_role(Some(role)), CornerShape::Circular);
|
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] {
|
for source in [BLUR_SHADER, MATERIAL_SHADER, ROUNDED_TEXTURE_SHADER] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
corner_shader(source),
|
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 {
|
fn renderer() -> GlesRenderer {
|
||||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
|
||||||
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
let devices = EGLDevice::enumerate().unwrap().collect::<Vec<_>>();
|
||||||
|
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() };
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).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]
|
#[test]
|
||||||
#[ignore = "requires an EGL rendering device"]
|
#[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 renderer = renderer();
|
||||||
let mut resources = RenderResources::default();
|
let mut resources = RenderResources::default();
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let circular = rounded_clip_program(&mut resources, &mut renderer).unwrap();
|
let circular = corner_program(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
|
||||||
material_program(&mut resources, &mut renderer).unwrap();
|
material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap();
|
||||||
blur_program(&mut resources, &mut renderer).unwrap();
|
blur_program(&mut resources, &mut renderer).unwrap();
|
||||||
let uniforms = [
|
let uniforms = [
|
||||||
Uniform::new("clip_rect", [0.0f32, 0.0, 64.0, 64.0]),
|
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);
|
let before = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||||
corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap();
|
||||||
material_program_for_corners(&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);
|
let after = rasterize(&mut renderer, &circular.solid, &uniforms);
|
||||||
assert_eq!(before, after);
|
assert_eq!(before, after);
|
||||||
let cache = &resources.contexts[&context];
|
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);
|
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::<i32, Physical>::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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ pub(super) fn rounded_clip_program(
|
|||||||
resources: &mut RenderResources,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
) -> Option<RoundedClipPrograms> {
|
) -> Option<RoundedClipPrograms> {
|
||||||
corner_program(resources, renderer, CornerShape::Circular)
|
corner_program(resources, renderer, CornerShape::Continuous)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn corner_program(
|
pub(super) fn corner_program(
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ pub(super) fn material_program(
|
|||||||
resources: &mut RenderResources,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
) -> Option<MaterialProgram> {
|
) -> Option<MaterialProgram> {
|
||||||
material_program_for_corners(resources, renderer, CornerShape::Circular)
|
material_program_for_corners(resources, renderer, CornerShape::Continuous)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn material_program_for_corners(
|
pub(super) fn material_program_for_corners(
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag");
|
|||||||
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
||||||
|
|
||||||
fn corner_shader(source: &str) -> String {
|
fn corner_shader(source: &str) -> String {
|
||||||
corner_shader_for(source, CornerShape::Circular)
|
corner_shader_for(source, CornerShape::Continuous)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
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 {
|
pub(crate) fn window_corner_shape(_window: &smithay::desktop::Window) -> CornerShape {
|
||||||
// Semantic shell surfaces keep their client-matching circular outlines.
|
// Shell surfaces and application windows use the same shared squircle profile.
|
||||||
corner_shape_for_role(
|
|
||||||
window
|
|
||||||
.toplevel()
|
|
||||||
.and_then(|toplevel| crate::effects::surface_role(toplevel.wl_surface()).map(|(role, _)| role)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn corner_shape_for_role(role: Option<crate::effects::SemanticRole>) -> CornerShape {
|
|
||||||
if role.is_some() {
|
|
||||||
CornerShape::Circular
|
|
||||||
} else {
|
|
||||||
CornerShape::Continuous
|
CornerShape::Continuous
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ void main() {
|
|||||||
float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius);
|
float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius);
|
||||||
float coverage = edge_coverage(sdf);
|
float coverage = edge_coverage(sdf);
|
||||||
if (paint_mode > 0.5) {
|
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 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 sigma = max(shadow_values.x * 0.5, 0.5);
|
||||||
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ void main() {
|
|||||||
|
|
||||||
#ifdef CONTINUOUS_WINDOW_CORNERS
|
#ifdef CONTINUOUS_WINDOW_CORNERS
|
||||||
// Inset the outer distance field along its normals, rather than shrinking
|
// 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);
|
float inner_coverage = edge_coverage(signed_distance + border_width);
|
||||||
#else
|
#else
|
||||||
vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0));
|
vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0));
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ float corner_nearest(vec2 query, float t, vec2 a, vec2 b, vec2 c, vec2 d) {
|
|||||||
vec2 error = point - query;
|
vec2 error = point - query;
|
||||||
float speed_squared = dot(tangent, tangent);
|
float speed_squared = dot(tangent, tangent);
|
||||||
float denominator = max(speed_squared + dot(error, acceleration), speed_squared * 0.25);
|
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;
|
return t;
|
||||||
@@ -71,13 +75,23 @@ float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
|
|||||||
return -min(inset.x, inset.y);
|
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 best_squared = 1.0e20;
|
||||||
float side = 1.0;
|
float side = 1.0;
|
||||||
|
|
||||||
for (int segment = 0; segment < 3; segment++) {
|
for (int segment = 0; segment < 2; segment++) {
|
||||||
vec2 a, b, c, d;
|
vec2 a, b, c, d;
|
||||||
corner_controls(segment, blend, 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++) {
|
for (int seed = 0; seed < 2; seed++) {
|
||||||
float t = corner_nearest(query, float(seed), a, b, c, d);
|
float t = corner_nearest(query, float(seed), a, b, c, d);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jiff.workspace = true
|
|||||||
ferese-theme-model = { path = "../ferese-theme-model" }
|
ferese-theme-model = { path = "../ferese-theme-model" }
|
||||||
libcosmic = { workspace = true, features = ["a11y"] }
|
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]
|
[dev-dependencies]
|
||||||
ferese-config = { path = "../ferese-config" }
|
ferese-config = { path = "../ferese-config" }
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::{Background, Color};
|
use cosmic::iced::{Background, Color};
|
||||||
use cosmic::theme;
|
use cosmic::theme;
|
||||||
use cosmic::widget::button;
|
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 background = composite(fill, native.primary(false).base.into());
|
||||||
let foreground = foreground(background, component.on.into());
|
let foreground = foreground(background, component.on.into());
|
||||||
button::Style {
|
button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
text_color: Some(foreground),
|
text_color: Some(foreground),
|
||||||
icon_color: Some(foreground),
|
icon_color: Some(foreground),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Date/time labels and a calendar grid; callers own navigation and popup behavior.
|
//! 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::iced::{Alignment, Background, Border, Length};
|
||||||
use cosmic::widget::{column, container, row};
|
use cosmic::widget::{column, container, row};
|
||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
@@ -124,6 +125,7 @@ pub fn grid<'a, M: Clone + 'a>(
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: palette.radius.min(12.).into(),
|
radius: palette.radius.min(12.).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::{Background, Border, Color, Vector};
|
use cosmic::iced::{Background, Border, Color, Vector};
|
||||||
use cosmic::widget::{button, container};
|
use cosmic::widget::{button, container};
|
||||||
use cosmic::{theme, widget};
|
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> {
|
pub fn switch<'a, M: Clone + 'a>(enabled: bool, palette: Palette) -> button::Button<'a, M> {
|
||||||
let paint = move |focused| button::Style {
|
let paint = move |focused| button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: None,
|
background: None,
|
||||||
text_color: Some(palette.text),
|
text_color: Some(palette.text),
|
||||||
icon_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 {
|
pub fn settings_input(p: Palette) -> theme::TextInput {
|
||||||
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
|
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(),
|
background: mix(p.card, p.text, if hovered { 0.06 } else { 0.035 }).into(),
|
||||||
border_radius: p.radius.min(7.).into(),
|
border_radius: p.radius.min(7.).into(),
|
||||||
border_width: 1.,
|
border_width: 1.,
|
||||||
@@ -133,9 +138,11 @@ pub fn select<'a, M: 'a>(
|
|||||||
widget::container(content).class(theme::Container::custom(move |_| container::Style {
|
widget::container(content).class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(mix(p.card, p.text, 0.035).into()),
|
background: Some(mix(p.card, p.text, 0.035).into()),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
width: 1.,
|
width: 1.,
|
||||||
color: mix(p.card, p.text, 0.18),
|
color: mix(p.card, p.text, 0.18),
|
||||||
radius: p.radius.min(7.).into(),
|
radius: p.radius.min(7.).into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
@@ -170,6 +177,8 @@ fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32, foc
|
|||||||
};
|
};
|
||||||
|
|
||||||
button::Style {
|
button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
text_color: Some(on),
|
text_color: Some(on),
|
||||||
icon_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)
|
(crate::composite(palette.card, palette.background), palette.muted)
|
||||||
};
|
};
|
||||||
button::Style {
|
button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: Some(Background::Color(fill)),
|
background: Some(Background::Color(fill)),
|
||||||
text_color: Some(foreground),
|
text_color: Some(foreground),
|
||||||
icon_color: Some(foreground),
|
icon_color: Some(foreground),
|
||||||
@@ -231,6 +242,8 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
|
|||||||
let accent = palette.accent;
|
let accent = palette.accent;
|
||||||
let radius = palette.radius.min(10.);
|
let radius = palette.radius.min(10.);
|
||||||
let appearance = move |focused: bool| widget::text_input::Appearance {
|
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(),
|
background: Color::from_rgba(text.r, text.g, text.b, 0.045).into(),
|
||||||
border_radius: radius.into(),
|
border_radius: radius.into(),
|
||||||
border_width: 1.,
|
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 {
|
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 {
|
let appearance = move |focused: bool| widget::text_input::Appearance {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: cosmic::iced::Color::from_rgba(
|
background: cosmic::iced::Color::from_rgba(
|
||||||
(surface.r + 1.) * 0.5,
|
(surface.r + 1.) * 0.5,
|
||||||
(surface.g + 1.) * 0.5,
|
(surface.g + 1.) * 0.5,
|
||||||
@@ -299,6 +314,8 @@ pub fn shell_button(
|
|||||||
pressed: bool,
|
pressed: bool,
|
||||||
) -> button::Style {
|
) -> button::Style {
|
||||||
button::Style {
|
button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
text_color: Some(foreground),
|
text_color: Some(foreground),
|
||||||
icon_color: Some(foreground),
|
icon_color: Some(foreground),
|
||||||
border_radius: radius.into(),
|
border_radius: radius.into(),
|
||||||
@@ -318,6 +335,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
|
|||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: radius.into(),
|
radius: radius.into(),
|
||||||
..Default::default()
|
..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 {
|
pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button {
|
||||||
let style = move |active: bool| button::Style {
|
let style = move |active: bool| button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
text_color: Some(foreground),
|
text_color: Some(foreground),
|
||||||
icon_color: Some(foreground),
|
icon_color: Some(foreground),
|
||||||
background: (active || filled).then_some(Background::Color(hover)),
|
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 {
|
pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32) -> theme::Button {
|
||||||
let paint = move |outline| button::Style {
|
let paint = move |outline| button::Style {
|
||||||
|
shape: Some(BorderShape::Continuous),
|
||||||
|
outline: None,
|
||||||
background: Some(Color { a: opacity, ..fill }.into()),
|
background: Some(Color { a: opacity, ..fill }.into()),
|
||||||
text_color: Some(foreground),
|
text_color: Some(foreground),
|
||||||
icon_color: Some(foreground),
|
icon_color: Some(foreground),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use cosmic::iced::advanced::widget::{Id, Operation, Tree, tree};
|
use cosmic::iced::advanced::widget::{Id, Operation, Tree, tree};
|
||||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer};
|
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::iced::{Event, Length, Rectangle, Size, keyboard};
|
||||||
use cosmic::{Element, Renderer, Theme};
|
use cosmic::{Element, Renderer, Theme};
|
||||||
use ferese_theme_model::Appearance;
|
use ferese_theme_model::Appearance;
|
||||||
@@ -65,6 +66,8 @@ pub fn tile<M: Clone + 'static>(
|
|||||||
|
|
||||||
fn tile_style(palette: crate::Palette, selected: bool) -> cosmic::theme::Button {
|
fn tile_style(palette: crate::Palette, selected: bool) -> cosmic::theme::Button {
|
||||||
let style = move |focused: bool, hovered: bool| cosmic::widget::button::Style {
|
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()),
|
background: Some(if hovered { palette.sidebar } else { palette.card }.into()),
|
||||||
border_width: if selected { 2. } else { 0. },
|
border_width: if selected { 2. } else { 0. },
|
||||||
border_color: palette.accent,
|
border_color: palette.accent,
|
||||||
@@ -395,9 +398,11 @@ impl<M: Clone> Widget<M, Theme, Renderer> for Radio<'_, M> {
|
|||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds: layout.bounds(),
|
bounds: layout.bounds(),
|
||||||
border: cosmic::iced::Border {
|
border: cosmic::iced::Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: style.text_color,
|
color: style.text_color,
|
||||||
width: 2.,
|
width: 2.,
|
||||||
radius: 10.into(),
|
radius: 10.into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
pub fn inner_radius(outer: f32, padding: f32) -> f32 {
|
||||||
(outer - padding).max(0.0)
|
(outer - padding).max(0.0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Shared menu layouts and paint; callers own actions and animation timing.
|
//! Shared menu layouts and paint; callers own actions and animation timing.
|
||||||
use cosmic::Element;
|
use cosmic::Element;
|
||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::{Alignment, Color, Length};
|
use cosmic::iced::{Alignment, Color, Length};
|
||||||
use cosmic::widget::{button, container, row as iced_row};
|
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.,
|
width: 4.,
|
||||||
border: cosmic::iced::Border {
|
border: cosmic::iced::Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: 2.into(),
|
radius: 2.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handle: Handle {
|
handle: Handle {
|
||||||
|
corner_shape: BorderShape::Circular,
|
||||||
shape: HandleShape::Circle { radius: 6. },
|
shape: HandleShape::Circle { radius: 6. },
|
||||||
background: foreground.into(),
|
background: foreground.into(),
|
||||||
border_width: 0.,
|
border_width: 0.,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use cosmic::iced::Color;
|
use cosmic::iced::Color;
|
||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Palette {
|
pub struct Palette {
|
||||||
@@ -68,8 +69,9 @@ impl Palette {
|
|||||||
.text_tint(rgba(self.text).color)
|
.text_tint(rgba(self.text).color)
|
||||||
.accent(rgba(self.accent).color)
|
.accent(rgba(self.accent).color)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
crate::apply(&mut native, self.on_accent);
|
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 {
|
pub fn application_style(self, background: Color) -> cosmic::iced::theme::Style {
|
||||||
@@ -135,7 +137,9 @@ mod tests {
|
|||||||
resolved.accessibility.increase_contrast = high_contrast;
|
resolved.accessibility.increase_contrast = high_contrast;
|
||||||
let palette = Palette::from_resolved(&resolved);
|
let palette = Palette::from_resolved(&resolved);
|
||||||
assert_eq!(palette.high_contrast, high_contrast);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 presentation state. Entering overview inherits the window's visible position
|
||||||
and velocity, including motion from a workspace slide. Resize images use the same
|
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 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
|
### 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.
|
inhibitor. Use `feresectl --help` for runtime control commands.
|
||||||
|
|
||||||
Shell rounding is controlled in Settings → Appearance → Shell corner radius.
|
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
|
Settings → Windows. `theme.geometry.shell-radius` takes precedence over the old
|
||||||
`appearance.corner-radius` and then `theme.geometry.top-bar-radius` keys; when
|
`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
|
none are set, the shell uses 14 px. Legacy clock/note radius fields no longer
|
||||||
|
|||||||
@@ -457,9 +457,11 @@ impl Locker {
|
|||||||
text_color: Some(on),
|
text_color: Some(on),
|
||||||
icon_color: Some(on),
|
icon_color: Some(on),
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
|
shape: cosmic::iced::border::Shape::Continuous,
|
||||||
radius: (avatar_size / 2.).into(),
|
radius: (avatar_size / 2.).into(),
|
||||||
width: 1.,
|
width: 1.,
|
||||||
color: a.accent.scale_alpha(0.3),
|
color: a.accent.scale_alpha(0.3),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(<cosmic::Renderer as Headless>::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<Tree> = 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::<Vec<_>>();
|
||||||
|
times.sort_unstable();
|
||||||
|
eprintln!(
|
||||||
|
"backend={backend} page={page:?} stage={stage} median_us={} p95_us={}",
|
||||||
|
times[times.len() / 2],
|
||||||
|
times[times.len() * 95 / 100]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disabling_auto_keeps_the_effective_appearance() {
|
fn disabling_auto_keeps_the_effective_appearance() {
|
||||||
for appearance in [
|
for appearance in [
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ libc.workspace = true
|
|||||||
# Keep GPU rendering available as a fallback/override. The shell defaults to
|
# Keep GPU rendering available as a fallback/override. The shell defaults to
|
||||||
# tiny-skia to avoid a second GPU initialization during desktop startup.
|
# tiny-skia to avoid a second GPU initialization during desktop startup.
|
||||||
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
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.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::{
|
|||||||
Background, BarMetrics, Border, Color, Element, FereseShell, Length, Message, ShellSnapshot, ShellTheme, alignment,
|
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,
|
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 {
|
impl FereseShell {
|
||||||
pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
|
pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
|
||||||
@@ -327,9 +328,11 @@ pub(super) fn workspace_selector_style(
|
|||||||
None
|
None
|
||||||
},
|
},
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: color_with_opacity(shell_theme.accent, 0.55),
|
color: color_with_opacity(shell_theme.accent, 0.55),
|
||||||
width: if active_elsewhere { 1.0 } else { 0.0 },
|
width: if active_elsewhere { 1.0 } else { 0.0 },
|
||||||
radius: shell_theme.material_radius.min(14.0).into(),
|
radius: shell_theme.material_radius.min(14.0).into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -350,9 +353,11 @@ pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style {
|
|||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(color_with_opacity(theme.border, 0.25))),
|
background: Some(Background::Color(color_with_opacity(theme.border, 0.25))),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: color(theme.border),
|
color: color(theme.border),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: theme.material_radius.min(16.0).into(),
|
radius: theme.material_radius.min(16.0).into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use super::{
|
|||||||
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
|
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
|
||||||
set_margin, stacked_clock_digits, text, theme, window,
|
set_margin, stacked_clock_digits, text, theme, window,
|
||||||
};
|
};
|
||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
|
|
||||||
impl FereseShell {
|
impl FereseShell {
|
||||||
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
|
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
|
||||||
@@ -398,6 +399,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background,
|
background,
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: radius.into(),
|
radius: radius.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -613,6 +615,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background,
|
background,
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: radius.into(),
|
radius: radius.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
||||||
use cosmic::widget::{column, scrollable};
|
use cosmic::widget::{column, scrollable};
|
||||||
|
|
||||||
@@ -56,6 +57,23 @@ fn card_button<'a>(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn card_surface<'a>(
|
||||||
|
content: impl Into<Element<'a, cosmic::Action<Message>>>,
|
||||||
|
history: bool,
|
||||||
|
style: container::Style,
|
||||||
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
|
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>(
|
fn icon_control<'a>(
|
||||||
source: &'static [u8],
|
source: &'static [u8],
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
@@ -82,6 +100,7 @@ fn icon_control<'a>(
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(12.0).into(),
|
radius: motion::radius(12.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -287,6 +306,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(well)),
|
background: Some(Background::Color(well)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(15.0).into(),
|
radius: motion::radius(15.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -313,6 +333,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(badge_fill.scale_alpha(opacity))),
|
background: Some(Background::Color(badge_fill.scale_alpha(opacity))),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(8.0).into(),
|
radius: motion::radius(8.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -448,15 +469,10 @@ impl FereseShell {
|
|||||||
.push(actions);
|
.push(actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let card = container(face)
|
let card = card_surface(
|
||||||
.id(if history {
|
face,
|
||||||
"ferese-history-card"
|
history,
|
||||||
} else {
|
container::Style {
|
||||||
"ferese-blur-card"
|
|
||||||
})
|
|
||||||
.width(Length::Fill)
|
|
||||||
.clip(true)
|
|
||||||
.class(theme::Container::custom(move |_| container::Style {
|
|
||||||
background: if history {
|
background: if history {
|
||||||
Some(Background::Color(Color {
|
Some(Background::Color(Color {
|
||||||
a: 0.045 * opacity,
|
a: 0.045 * opacity,
|
||||||
@@ -466,14 +482,16 @@ impl FereseShell {
|
|||||||
(!compositor_material).then_some(Background::Color(base))
|
(!compositor_material).then_some(Background::Color(base))
|
||||||
},
|
},
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||||
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(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
snap: true,
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
},
|
||||||
let card: Element<'_, cosmic::Action<Message>> = card.into();
|
);
|
||||||
let card: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
let card: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
||||||
// With translucent materials, complete backing rectangles show
|
// With translucent materials, complete backing rectangles show
|
||||||
// through the face and multiply its tint. Paint only the exposed
|
// through the face and multiply its tint. Paint only the exposed
|
||||||
@@ -488,6 +506,7 @@ impl FereseShell {
|
|||||||
..foreground
|
..foreground
|
||||||
})),
|
})),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -648,6 +667,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(base)),
|
background: Some(Background::Color(base)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: palette.material_radius.into(),
|
radius: palette.material_radius.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -665,6 +685,7 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: Some(Background::Color(well)),
|
background: Some(Background::Color(well)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(28.0).into(),
|
radius: motion::radius(28.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -766,9 +787,11 @@ impl FereseShell {
|
|||||||
.class(theme::Container::custom(move |_| container::Style {
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))),
|
background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: palette.material_radius.into(),
|
radius: palette.material_radius.into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
snap: true,
|
snap: true,
|
||||||
..Default::default()
|
..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(<cosmic::Renderer as Headless>::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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::{Alignment, Background, Border, Rectangle, alignment};
|
use cosmic::iced::{Alignment, Background, Border, Rectangle, alignment};
|
||||||
use cosmic::widget::{button, container, row};
|
use cosmic::widget::{button, container, row};
|
||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
@@ -82,6 +83,7 @@ impl FereseShell {
|
|||||||
move |_| container::Style {
|
move |_| container::Style {
|
||||||
background: Some(Background::Color(foreground)),
|
background: Some(Background::Color(foreground)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(2.0).into(),
|
radius: motion::radius(2.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::{Alignment, Background, Border, Color, Length};
|
use cosmic::iced::{Alignment, Background, Border, Color, Length};
|
||||||
use cosmic::widget::{button, column, container, row, slider};
|
use cosmic::widget::{button, column, container, row, slider};
|
||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
@@ -32,6 +33,7 @@ pub(super) fn status_summary<'a>(
|
|||||||
..badge_color
|
..badge_color
|
||||||
})),
|
})),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(16.0).into(),
|
radius: motion::radius(16.0).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -79,6 +81,7 @@ pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
radius: motion::radius(2.5).into(),
|
radius: motion::radius(2.5).into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod system;
|
|||||||
|
|
||||||
use bar::status_icon;
|
use bar::status_icon;
|
||||||
use controls::{control_card, menu_button, toggle_row};
|
use controls::{control_card, menu_button, toggle_row};
|
||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::widget::column;
|
use cosmic::widget::column;
|
||||||
use status::{Action, Snapshot};
|
use status::{Action, Snapshot};
|
||||||
|
|
||||||
@@ -242,9 +243,11 @@ impl FereseShell {
|
|||||||
text_color: Some(primary),
|
text_color: Some(primary),
|
||||||
icon_color: Some(primary),
|
icon_color: Some(primary),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: color_with_opacity(theme.border, p),
|
color: color_with_opacity(theme.border, p),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: theme.material_radius.into(),
|
radius: theme.material_radius.into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
snap: true,
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use cosmic::iced::border::Shape as BorderShape;
|
||||||
use cosmic::iced::widget::Space;
|
use cosmic::iced::widget::Space;
|
||||||
use cosmic::widget::column;
|
use cosmic::widget::column;
|
||||||
|
|
||||||
@@ -620,9 +621,11 @@ impl FereseShell {
|
|||||||
text_color: Some(palette.text),
|
text_color: Some(palette.text),
|
||||||
icon_color: Some(palette.text),
|
icon_color: Some(palette.text),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
shape: BorderShape::Continuous,
|
||||||
color: palette.muted.scale_alpha(0.16),
|
color: palette.muted.scale_alpha(0.16),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: theme.material_radius.into(),
|
radius: theme.material_radius.into(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
snap: true,
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
Reference in new issue
Block a user